/*
 * The Press — v1 prototype styles.
 *
 * Mobile-first (390px design width): ~85% of Instagram traffic is phone.
 * Editorial/gallery register — the page has to read as *curated* within two
 * seconds or a cold buyer files it as a dropshipper (spec §1 Beat 2, the
 * highest-leverage trust moment in the funnel).
 *
 * No framework, no build step: must open from file:// and survive being
 * emailed around.
 */

:root {
  --ink: #14110f;
  --ink-soft: #55504b;
  --paper: #f4f1ec;
  --paper-warm: #ebe6dd;
  --line: #d9d2c6;
  --accent: #14110f;
  --shadow: 0 18px 50px -18px rgba(20, 17, 15, 0.45);
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
  --sans: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
  /* System mono stack — no webfont, so it cannot block first paint or fail to
     load on a cold ad click. Used for artist names (Mel, 2026-09-14). */
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
  /* One measure for header, body and footer. They were 1080/1180/1080 and the
     nav visibly sat narrower than the content it capped (Mel, 2026-09-14).
     Change this in ONE place or they drift apart again. */
  --measure: 1180px;
  --gutter: 24px;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

/* The `hidden` attribute only carries UA-level `display: none`, which ANY author
 * `display` rule outranks — and `.wall`, `.room` and `.rack` all set one. Without
 * this, hiding a browse direction with `hidden` silently fails and all four views
 * render stacked down the page. */
[hidden] { display: none !important; }

/* `html` carries the background too, not just `body`: with `viewport-fit=cover`
 * iOS paints the safe-area strip behind the status bar from the root element's
 * background. Setting it only on `body` leaves that strip rendering the
 * browser's own default — the grey band at the top of the page on a notched
 * iPhone. */
html { background: var(--paper); }

body {
  margin: 0;
  font-family: var(--sans);
  background: var(--paper);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  /* Sticky footer, the layout kind: on a short page the main region grows to
     fill the viewport so the footer sits ON the bottom edge; on a long page it
     simply follows the content. NOT position:fixed — that would float a bar
     over the artwork on every scroll. */
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}

body > main { flex: 1 0 auto; }
body > footer.foot { flex-shrink: 0; }

img { display: block; max-width: 100%; }
button { font: inherit; cursor: pointer; }
a { color: inherit; }

/* ---------------------------------------------------------------- load-in
 * Mel's note: "maybe it's even just the load-in that is interesting, then
 * typical after that." Prints are set out one by one like a shopkeeper laying
 * stock on a counter — the delight lives here so the browse itself can stay
 * conventional (unfamiliar mechanics read as "weird site" to cold traffic).
 */
#curtain {
  position: fixed; inset: 0; z-index: 100;
  background: var(--paper);
  display: grid; place-items: center;
  transition: opacity .55s ease, visibility .55s;
}
#curtain.done { opacity: 0; visibility: hidden; }

/* Box matches the artwork ratio (675x878) so the stacked prints are not
 * cropped by `object-fit: cover` on the images inside. */
.curtain-stack { position: relative; width: 190px; height: 247px; }
.curtain-stack img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; border-radius: 2px;
  box-shadow: var(--shadow);
  opacity: 0;
  transform: translateY(26px) rotate(var(--r, 0deg)) scale(.97);
  animation: layout .62s cubic-bezier(.2, .8, .25, 1) forwards;
  animation-delay: var(--d, 0s);
}
@keyframes layout { to { opacity: 1; transform: translateY(0) rotate(var(--r, 0deg)) scale(1); } }

.curtain-word {
  position: absolute; bottom: -54px; left: 0; right: 0; text-align: center;
  font-family: var(--serif); font-size: 13px; letter-spacing: .34em;
  text-transform: uppercase; color: var(--ink-soft);
  /* Starts as the last print lands (deck settles ~0.89s) and completes at 1.24s,
   * leaving ~500ms to read before the curtain lifts at 1.75s. Previously it
   * began at 1.05s and finished at 1.55s — only 200ms on screen, which read as
   * the wordmark being snatched away. */
  opacity: 0; animation: fade .35s ease .89s forwards;
}
@keyframes fade { to { opacity: 1; } }

/* ---------------------------------------------------------------- chrome */
header.top {
  position: sticky; top: 0; z-index: 40;
  background: rgba(244, 241, 236, .93);
  backdrop-filter: saturate(140%) blur(12px);
  border-bottom: 1px solid var(--line);
  /* Notch/rounded-corner safety on phones. */
  padding-top: env(safe-area-inset-top, 0);
}
.top-in {
  max-width: var(--measure); margin: 0 auto; padding: 13px var(--gutter);
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.wordmark {
  font-family: var(--serif); font-size: 17px; letter-spacing: .2em;
  text-transform: uppercase; text-decoration: none; white-space: nowrap;
}
.closes { font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-soft); }

/* view switcher — prototype-only affordance for comparing browse directions */
/* Header links out to Manifold: X and the website. Icon-only, ink-coloured,
 * 44px hit area. These are the only chrome in the header besides the wordmark. */
.top-links { display: flex; align-items: center; gap: 2px; }
.top-links a {
  width: 40px; height: 40px; display: grid; place-items: center;
  color: var(--ink); text-decoration: none;
}
.top-links svg { width: 17px; height: 17px; }
/* BOTH marks are filled glyphs. The external-link path is a closed shape with
   its interior carved out, so stroking it drew hairline outlines with no body
   — the icon looked broken rather than light. */
.top-links a[aria-label="Manifold on X"] svg { fill: currentColor; }
.top-links a[aria-label="manifold.xyz"] svg { fill: currentColor; stroke: none; }

/* Browse-direction switcher: prototype-only comparison control. Hidden from the
 * page entirely — reach the other directions with ?view=room|rack|wall. */
.switch { display: none; }
.switch button {
  border: 0; background: none; border-radius: 999px;
  padding: 5px 10px; font-size: 11px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-soft);
}
.switch button[aria-selected="true"] { background: var(--ink); color: var(--paper); }

/* Four tabs plus a wordmark is tight at 390px. */
@media (max-width: 430px) {
  .top-in { padding: 10px 12px; gap: 8px; }
  .wordmark { font-size: 14px; letter-spacing: .16em; }
}

/* ---------------------------------------------------------------- hero */
.hero { max-width: 1080px; margin: 0 auto; padding: 34px 18px 12px; }
.hero h1 {
  font-family: var(--serif); font-weight: 400;
  font-size: clamp(30px, 8.5vw, 52px); line-height: 1.06; margin: 0 0 12px;
}
.hero p { margin: 0 0 6px; max-width: 46ch; color: var(--ink-soft); font-size: 15px; line-height: 1.55; }
.hero .facts {
  margin-top: 16px; display: flex; flex-wrap: wrap; gap: 8px;
  font-size: 11.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-soft);
}
.hero .facts span { border: 1px solid var(--line); border-radius: 999px; padding: 6px 11px; }

/* ---------------------------------------------------------------- A: rack */
/* Height is driven by the card itself rather than the viewport: a fixed vh box
 * left the nav and hint clipped below the fold on desktop. */
.rack { position: relative; margin: 18px auto 0; max-width: 520px; touch-action: pan-y; }
.rack .card {
  position: absolute; left: 50%; top: 0; width: min(78vw, 320px);
  transform: translateX(-50%);
  transition: transform .42s cubic-bezier(.2, .8, .25, 1), opacity .42s ease;
  box-shadow: var(--shadow); border-radius: 2px; overflow: hidden; background: #fff;
}
/* The first card is taken out of the absolute stack to give .rack its height;
 * it is visually identical because every card shares the same aspect ratio. */
.rack .card:first-child { position: relative; }
/* Artwork ratio is 675x878 (~0.769), not 3/4. `contain` rather than `cover`
 * because the files carry their own paper margin — cropping to fill would
 * shave exactly the padding the artists set. */
.rack .card img { width: 100%; aspect-ratio: 675 / 878; object-fit: contain; }
.rack .card .cap { padding: 11px 13px 14px; }
.rack-hint { text-align: center; font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-soft); margin-top: 14px; }
.rack-nav { display: flex; justify-content: center; gap: 10px; margin-top: 34px; }
.rack-nav button { border: 1px solid var(--line); background: none; border-radius: 999px; width: 42px; height: 42px; }

/* ---------------------------------------------------------------- B: wall */
.wall-scroll { overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; padding: 26px 0 34px; }
.wall {
  position: relative; height: 62vh; min-height: 380px;
  width: max-content; padding: 0 6vw;
  display: flex; align-items: center; gap: clamp(26px, 6vw, 70px);
  background:
    linear-gradient(180deg, rgba(0,0,0,.035), transparent 22%),
    var(--paper-warm);
}
.wall .frame { position: relative; flex: 0 0 auto; cursor: pointer; }
.wall .frame img {
  height: var(--h, 40vh); width: auto;
  background: #fff; padding: 10px;
  box-shadow: 0 22px 40px -22px rgba(20,17,15,.55), 0 2px 0 rgba(0,0,0,.06);
  transition: transform .3s ease;
}
.wall .frame:active img { transform: scale(.985); }
.wall .plaque {
  margin-top: 10px; font-size: 10.5px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--ink-soft); text-align: center;
}
.wall-hint { text-align: center; font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-soft); }

/* ---------------------------------------------------------------- C: room */
.room { scroll-snap-type: y mandatory; }
.slab {
  scroll-snap-align: start; min-height: 92vh;
  display: grid; grid-template-rows: 1fr auto;
  padding: 22px 18px 34px; max-width: 1080px; margin: 0 auto;
}
.slab .art { display: grid; place-items: center; }
.slab .art img {
  max-height: 66vh; width: auto; background: #fff; padding: 12px;
  box-shadow: var(--shadow);
}
/* Fade-up is progressive enhancement only: JS adds .anim to the container once
 * the observer is wired, so artwork is never hidden by CSS alone. Without this
 * guard a failed observer renders an empty shop — the worst possible failure on
 * the page that has to earn a cold buyer's trust in two seconds. */
.room.anim .slab .art img {
  opacity: 0; transform: translateY(20px);
  transition: opacity .7s ease, transform .7s cubic-bezier(.2,.8,.25,1);
}
.room.anim .slab.seen .art img { opacity: 1; transform: none; }
.slab .meta { margin-top: 20px; display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; }
.slab .meta h2 { font-family: var(--serif); font-weight: 400; font-size: 21px; margin: 0 0 4px; }
.slab .meta .by { font-size: 13px; color: var(--ink-soft); }
.slab .meta .note { margin: 9px 0 0; font-size: 13.5px; line-height: 1.5; color: var(--ink-soft); max-width: 42ch; }

/* ---------------------------------------------------------------- buttons */
.btn {
  border: 0; border-radius: 999px; padding: 15px 22px;
  font-size: 15px; font-weight: 600; letter-spacing: .01em;
  background: var(--ink); color: var(--paper); width: 100%;
}
.btn.pay { background: #000; display: flex; align-items: center; justify-content: center; gap: 7px; }
/* Apple mark drawn as SVG rather than the  private-use glyph: that codepoint
 * only renders on Apple devices and silently collapses to a blank space
 * everywhere else, which is exactly what happened in testing. */
.btn.pay .applemark { width: 17px; height: 17px; fill: #fff; margin-top: -2px; }
/* `.btn` is used on both <button> and <a>; anchors otherwise inherit the global
 * link underline, which made the artist link read as body text, not a control. */
.btn { text-decoration: none; }
.btn.ghost { background: none; border: 1px solid var(--line); color: var(--ink); font-weight: 500; }
.btn.small { width: auto; padding: 11px 18px; font-size: 13.5px; }
/* Tertiary. Three stacked controls with only two weights (solid + outline) read
 * as two primaries and a spare, so the third drops to a plain underlined link
 * (Mel, 8/31). Keeps the 44px target — it is still a button, it just stops
 * competing with "Add to cart" for the same visual slot. */
.btn.link {
  background: none; border: 0; padding: 12px 8px;
  min-height: 44px; font-weight: 500; font-size: 13.5px;
  color: var(--ink-soft); text-decoration: underline;
  text-underline-offset: 3px;
}

/* ---------------------------------------------------------------- RING
 * CSS-only 3D carousel — the "spinning circle of posters" (Mel, 8/25).
 *
 * Why CSS and not three.js: this surface sits at the top of the paid funnel,
 * where a ~600KB WebGL bundle is a measurable conversion cost (cold traffic,
 * mobile-heavy, slow-landing-page is a named CAC killer). Ten textured quads
 * do not need a renderer. Zero dependencies, no WebGL context, no build step,
 * and it degrades to a flat grid by itself when 3D transforms are unsupported.
 *
 * Geometry: 10 sheets (each of the 5 works twice) evenly spaced around a
 * vertical ring, duplicates opposite each other so a work and its twin are
 * never on screen together. Sheets face outward, tilted ~8°, camera outside
 * the ring and slightly above.
 *
 * Depth is carried by shadow and scale, NOT by dimming (Mel, 8/25: the lit
 * version buried the artwork). Every sheet stays at full brightness — the
 * artwork is the product, so it is never the thing we darken. The ring reads
 * as three-dimensional because sheets further from the viewer cast tighter
 * shadows and sit slightly smaller, the way real paper on a carousel does.
 */
.ring-stage {
  perspective: 1500px;
  perspective-origin: 50% 42%;
  /* Height tracks the ring rather than the viewport: a fixed vh box left a band
   * of empty stage under the artwork, pushing the hint far from the art. The
   * ring sets its own height from --sheet-h. */
  height: auto;
  margin: 6px 0 0;
  /* Captions are absolutely positioned below each sheet, so they fall outside
   * the ring's own box and `place-items: center` ignores them entirely. The
   * padding has to cover the full caption overhang (~39px) plus clearance, or
   * `overflow: hidden` clips the labels at the stage edge.
   *
   * The top padding is not symmetry: the arc's far sheets project upward under
   * perspective, so a centred ring pushed artwork ~62px into the hero and got
   * clipped at the stage's top edge. Padding moves the whole ring down into the
   * dead space that was sitting unused below it. */
  /* Measured, not guessed: sampling every 6deg of ring rotation, the far
   * sheets project up to 45px above the ring's own box at the worst angle,
   * while 114px of the stage below the artwork sat empty. Top padding covers
   * the projection plus clearance; the surplus comes back off the bottom, so
   * the stage grows only ~13px overall. Re-measure after any change to
   * --sheet-h, the radius, or perspective-origin. */
  padding-top: 146px;
  padding-bottom: 24px;
  /* No stage colour: the ring sits on the page, not in a panel. An explicit
   * background here read as a separate section and cut the page in half. */
  background: none;
  overflow: hidden;
  display: grid; place-items: center;
  contain: layout paint;
  /* Drag is horizontal; let the browser keep vertical scrolling. */
  touch-action: pan-y;
  cursor: grab;
}
.ring-stage.dragging { cursor: grabbing; }

.ring {
  position: relative;
  width: var(--sheet-w, 150px);
  height: var(--sheet-h, 210px);
  transform-style: preserve-3d;
  /* Rotation is driven by JS (`--rot`) rather than a CSS animation so the same
   * transform can be dragged, paused and resumed without fighting the engine.
   *
   * `will-change` belongs HERE and only here: the ring is the element whose
   * transform actually changes each frame. The leaves are static relative to
   * it, so promoting them individually just pinned ten extra textures in GPU
   * memory and caused eviction thrash on phones. */
  transform: translateZ(calc(var(--radius, 300px) * -1)) rotateX(7deg) rotateY(var(--rot, 0deg));
  will-change: transform;
}

/* Sheet elements are named `.ring-leaf`, not `.sheet`: the product and checkout
 * overlays already own `.sheet`, and their `visibility: hidden` (closed state)
 * silently hid every poster in the ring. Distinct name, no collision. */
.ring .ring-leaf {
  position: absolute;
  /* Buttons do not resolve `inset: 0` into a size the way a div does — their
   * intrinsic sizing wins and the sheet collapses to zero. Size explicitly. */
  left: 0; top: 0;
  width: var(--sheet-w, 150px);
  height: var(--sheet-h, 210px);
  transform: rotateY(var(--a)) translateZ(var(--radius, 300px)) rotateZ(-7deg);
  backface-visibility: hidden;
  cursor: inherit;
  border: 0; padding: 0; background: none;
  -webkit-appearance: none; appearance: none;
  /* No `will-change` here — see .ring. These transforms are static; the parent
   * is what moves. */
  transform-style: preserve-3d;
}

/* Paper: a white border like a real print margin, and a soft cast shadow that
 * does the depth work the lighting used to do.
 *
 * `filter: drop-shadow` rather than `box-shadow`: a box-shadow on a rotating
 * 3D element is re-rasterised every frame (the blur has to be recomputed as the
 * element's screen-space box changes), which is the stutter felt while
 * swiping. drop-shadow is applied once to the layer texture and transformed
 * with it. */
.ring .ring-leaf img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  background: #fff;
  padding: 6px;
  filter: drop-shadow(0 10px 14px rgba(20,17,15,.24));
  backface-visibility: hidden;
  -webkit-user-select: none; user-select: none;
  -webkit-user-drag: none;
}

/* Artist + title caption under each sheet. Rendered flat against the sheet so
 * it rotates with it — a caption that stayed screen-facing would swim against
 * the ring.
 *
 * `backface-visibility` is required here, not just on the sheet and image: it
 * applies per element, so without it the captions on the far arc bleed through
 * the ring mirrored and upside-down. The extra translateZ forces the caption
 * onto its own 3D layer so mobile WebKit honours the rule. */
.ring .ring-leaf .cap {
  position: absolute; left: 0; right: 0; top: calc(100% + 9px);
  text-align: center; pointer-events: none;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  transform: translateZ(0.01px);
}
.ring .ring-leaf .cap b {
  display: block; font-family: var(--serif); font-weight: 400;
  font-size: 13px; letter-spacing: .01em; color: var(--ink);
}
.ring .ring-leaf .cap span {
  display: block; font-size: 10.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-soft); margin-top: 2px;
}

.ring .ring-leaf:focus-visible img { outline: 2px solid var(--ink); outline-offset: 3px; }

/* Hint copy swaps at 700px — the same breakpoint press.js uses to decide
 * `mobile`, so the wording and the ring geometry never disagree. */
.ring-hint .hint-swipe { display: none; }
@media (max-width: 700px) {
  .ring-hint .hint-tap { display: none; }
  .ring-hint .hint-swipe { display: inline; }
}

.ring-hint {
  text-align: center; font-size: 11px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink-soft);
  /* The ring is centred in the stage, leaving ~46px of dead space below the
   * lowest sheet — but each sheet's caption hangs ~39px into it, so a negative
   * margin large enough to close the visual gap collides with the labels.
   * Sit just clear of the captions instead. */
  margin-top: 6px;
}

/* Entrance: sheets rise into position, staggered, ~1.2s total. */
.ring.enter .ring-leaf { animation: leaf-rise .62s cubic-bezier(.2,.8,.25,1) both; animation-delay: var(--delay-in, 0s); }
@keyframes leaf-rise {
  from { opacity: 0; transform: rotateY(var(--a)) translateZ(var(--radius, 300px)) rotateZ(-7deg) translateY(70px); }
  to   { opacity: 1; transform: rotateY(var(--a)) translateZ(var(--radius, 300px)) rotateZ(-7deg) translateY(0); }
}

/* No 3D support → flat grid. The section must never render blank. */
@supports not (transform-style: preserve-3d) {
  .ring-stage { perspective: none; height: auto; padding: 18px; }
  .ring { width: auto; height: auto; transform: none; animation: none;
          display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
  .ring .ring-leaf { position: static; transform: none; }
  .ring .ring-leaf[data-dupe="1"] { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .ring { animation-play-state: paused; }
}

/* Print tiers (Mel, 8/27, restructured 8/28): pick the price first, read the
 * spec second.
 *
 * A segmented row of three name+price cells, then one detail panel describing
 * whichever is selected. The earlier version stacked all three tiers with their
 * full specs visible at once, which made the buyer compare nine facts before
 * choosing. Leading with price turns it into one decision, and the panel then
 * answers "what do I get for that". */
.tiers {
  /* Column count follows the number of tiers rather than a hardcoded 3, so
     removing or adding a tier in data.js cannot leave an empty cell (Mel
     removed Premium, 8/31). */
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 6px;
  margin: 20px 0 0;
}
.tier {
  display: grid; gap: 2px; justify-items: center; text-align: center;
  border: 1px solid var(--line); border-radius: 12px;
  padding: 11px 8px; cursor: pointer;
  transition: border-color .18s ease, background .18s ease;
}
.tier:hover { border-color: rgba(20,17,15,.30); }
/* Selected state is carried by a class rather than :has() for browser reach. */
.tier.on { border-color: var(--ink); background: var(--paper-warm); }
.tier input { position: absolute; opacity: 0; pointer-events: none; }
.tier-name {
  font-size: 10.5px; letter-spacing: .10em; text-transform: uppercase;
  color: var(--ink-soft); line-height: 1.3;
}
.tier.on .tier-name { color: var(--ink); }
.tier-price { font-family: var(--serif); font-size: 21px; line-height: 1.1; }

/* Spec panel for the selected tier. Same rows every time, so the values change
 * in place rather than the panel changing shape as you switch tiers. */
.tier-spec {
  margin: 10px 0 0; padding: 14px 16px;
  background: var(--paper-warm); border-radius: 12px;
  display: grid; gap: 9px; font-size: 13px;
}
.tier-spec > div { display: flex; justify-content: space-between; gap: 16px; }
.tier-spec dt { color: var(--ink-soft); margin: 0; flex: none; }
.tier-spec dd { margin: 0; text-align: right; }

/* On a phone the sticky buy bar occupies ~170px of a 393px-wide screen, so a
 * tall spec panel ends up partly underneath it. Tighter rows and type, plus a
 * narrower label column, keep the wordy tiers (Limited's paper and authenticity both
 * wrap) readable in the gap above the bar. */
@media (max-width: 700px) {
  .tier-spec { padding: 12px 14px; gap: 7px; font-size: 12.5px; }
  .tier-spec > div { gap: 10px; }
  .tier-spec dd { line-height: 1.35; }
}

/* Secondary line under a checkout row — names the chosen tier without
 * repeating a price. */
.co-line.sub { margin-top: -8px; font-size: 12.5px; color: var(--ink-soft); }

/* ---------------------------------------------------------------- product */
/* Sheets are full-viewport overlays, not in-flow sections: `position:fixed` +
 * its own scroll container so the shop behind cannot bleed through or scroll.
 * `visibility` (not transform alone) keeps a closed sheet out of the tab order
 * and away from assistive tech while it waits off-screen. */
.sheet {
  position: fixed; inset: 0; z-index: 60; background: var(--paper);
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  transform: translateY(100%);
  visibility: hidden;
  transition: transform .42s cubic-bezier(.2,.8,.25,1), visibility .42s;
}
.sheet.open { transform: none; visibility: visible; }

/* Swipe-to-dismiss: JS drives --drag while the finger is down, so the sheet
 * tracks the gesture instead of animating on a fixed curve. */
.sheet.dragging { transition: none; transform: translateY(var(--drag, 0px)); }

/* The print "flies" out of the ring: its contents rise and fade in behind the
 * tapped sheet's position, which makes the transition feel like the object
 * opened rather than a panel appearing over it. */
.sheet.open .pdp-art img {
  animation: pdp-land .58s cubic-bezier(.16,.9,.28,1) both;
}
@keyframes pdp-land {
  from { opacity: 0; transform: translateY(26px) scale(.86) rotate(-3deg); }
  to   { opacity: 1; transform: none; }
}
.sheet.open .pdp-head,
.sheet.open .spec-list,
.sheet.open .buy,
.sheet.open .mock {
  animation: pdp-follow .5s ease both;
}
.sheet.open .pdp-head { animation-delay: .12s; }
.sheet.open .mock     { animation-delay: .18s; }
.sheet.open .spec-list{ animation-delay: .22s; }
.sheet.open .buy      { animation-delay: .28s; }
@keyframes pdp-follow {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* Close control: a plain X, 44px, no chrome.
 *
 * It sits TOP-RIGHT, which is where a close control is looked for. It used to
 * be top-left (a back-arrow position inherited from the mobile sheet), which
 * reads as "back" and made people hunt for the way out. */
.close-x {
  width: 44px; height: 44px; border: 0; background: none; padding: 0;
  display: grid; place-items: center;
  margin-left: auto; margin-right: -10px;
  border-radius: 50%; cursor: pointer;
}
.close-x:hover { background: rgba(0,0,0,.06); }
.close-x:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.close-x svg { width: 19px; height: 19px; }
.close-x svg path { stroke: var(--ink); stroke-width: 1.6; fill: none; stroke-linecap: round; }

/* Drag handle — only meaningful on the mobile partial sheet, hidden on desktop
 * where the sheet is a full overlay and the X is the only control. */
.grabber { display: none; }

/* Scrim over the strip of shop left visible by a partial sheet. Lives outside
 * the sheets (see index.html) and is never shown on desktop. */
.scrim { display: none; }

/* ------------------------------------------------------------- partial sheet
 * On phones the sheet opens as a card that stops short of the top, leaving a
 * strip of the shop visible (Mel, 8/25). That strip does two jobs: it says
 * "you can get back" without a control, and it keeps the drop in view so the
 * product reads as one of a set rather than a destination page.
 *
 * Scrolling past the first ~40px promotes it to full height; scrolling back to
 * the top demotes it. Both states share one element — only `--top` changes —
 * so the sheet never re-renders mid-gesture.
 */
@media (max-width: 700px) {
  .sheet {
    top: var(--top, 84px);
    border-radius: 16px 16px 0 0;
    box-shadow: 0 -8px 40px -12px rgba(20,17,15,.42);
    transition:
      transform .42s cubic-bezier(.2,.8,.25,1),
      top .34s cubic-bezier(.2,.8,.25,1),
      border-radius .34s ease,
      visibility .42s;
  }
  /* Promoted: full height, square corners, reads as a page. */
  .sheet.full { --top: 0px; border-radius: 0; }

  .grabber {
    display: block; width: 38px; height: 4px; border-radius: 99px;
    background: rgba(20,17,15,.18);
    margin: 9px auto 2px;
  }
  .sheet-in { padding-top: 0; }
  .sheet-bar { top: 0; }

  .scrim {
    display: block;
    position: fixed; left: 0; right: 0; top: 0; height: 84px;
    background: rgba(20,17,15,.30);
    opacity: 0; pointer-events: none;
    z-index: 59;
    transition: opacity .3s ease;
  }
  .scrim.on { opacity: 1; }
}

.sheet-in {
  max-width: 1080px; margin: 0 auto; padding: 0 18px 130px;
  /* Fill the overlay so the page behind cannot show through beneath short
   * content (the thank-you state is only a few lines tall). */
  min-height: 100%;
  background: var(--paper);
}
.sheet-bar {
  position: sticky; top: 0; z-index: 5; background: var(--paper);
  display: flex; justify-content: space-between; align-items: center;
  padding: 13px 0; border-bottom: 1px solid var(--line); margin-bottom: 22px;
}
.sheet-bar button {
  border: 0; background: none; font-size: 13px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-soft);
  /* Back is the escape hatch on a phone — it needs a real tap target, not just
   * the height of its own text. */
  min-height: 44px; padding: 0 12px 0 0; margin-left: -2px;
}

.pdp-art { display: grid; place-items: center; background: var(--paper-warm); border-radius: 3px; padding: 26px 18px; }
.pdp-art img { max-height: 54vh; width: auto; background: #fff; padding: 12px; box-shadow: var(--shadow); }

/* in-room mock — spec §1 Beat 4: the single highest-leverage asset on the page.
 * People buy a thing on a wall, not a JPEG.
 * Fixed height rather than aspect-ratio + percentage child: a percentage height
 * inside an aspect-ratio box resolves against an indefinite height in Safari and
 * WebKit-derived engines, collapsing the framed print to nothing. */
.mock {
  margin-top: 12px; position: relative; border-radius: 3px; overflow: hidden;
  background: linear-gradient(178deg, #e7e1d6 0%, #ddd5c8 62%, #d3cabb 100%);
  height: 300px; display: grid; place-items: center;
}
.mock::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 26%;
  background: linear-gradient(180deg, rgba(0,0,0,.055), rgba(0,0,0,.12));
}
/* A dark frame with a white mount: the previous all-white treatment vanished
 * against the wall, so the mock did not read as framed at all. */
.mock img {
  /* `box-sizing: border-box` is global, so a fixed height minus 12px padding and
   * 7px border on each side left the content box at ratio 0.788 against the
   * artwork's 0.769 — a visible 2.5% stretch. `content-box` makes the declared
   * height the ARTWORK's height, so the mount and frame sit outside it and the
   * print keeps its true proportions. */
  box-sizing: content-box;
  height: 190px; width: auto;
  background: #fff;
  padding: 12px;
  border: 7px solid #2e2823;
  box-shadow:
    0 2px 3px rgba(20,17,15,.30),
    0 26px 44px -20px rgba(20,17,15,.75);
  position: relative; z-index: 1;
}
.mock .scale { position: absolute; right: 12px; bottom: 12px; z-index: 2; font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; color: rgba(20,17,15,.5); }

.pdp-head { margin-top: 24px; }
.pdp-head h2 { font-family: var(--serif); font-weight: 400; font-size: 27px; margin: 0 0 5px; }
.pdp-head .by { color: var(--ink-soft); font-size: 14.5px; }
.spec-list { margin: 18px 0 0; padding: 0; list-style: none; border-top: 1px solid var(--line); }
.spec-list li { display: flex; justify-content: space-between; gap: 14px; padding: 11px 0; border-bottom: 1px solid var(--line); font-size: 14px; }
.spec-list li span:last-child { color: var(--ink-soft); text-align: right; }

/* buy block sits above the artist section: purchase decision first, artist
 * discovery as the scroll reward, social links well below the buy zone so they
 * cannot leak intent (spec §1 Beat 4+5). */
.buy { margin-top: 22px; display: grid; gap: 9px; }
.buy .price { font-family: var(--serif); font-size: 27px; }
.buy .assure { font-size: 12.5px; color: var(--ink-soft); text-align: center; }

.artist-block { margin-top: 46px; padding-top: 26px; border-top: 1px solid var(--line); }
/* When the about-work section precedes it, that section already drew the rule —
 * a second one right after its short paragraph reads as a stray line. */
.about-work + .artist-block { margin-top: 34px; padding-top: 0; border-top: 0; }
.artist-block h3 { font-family: var(--serif); font-weight: 400; font-size: 20px; margin: 0; }
.artist-block p { color: var(--ink-soft); font-size: 14.5px; line-height: 1.6; margin: 0 0 14px; max-width: 58ch; }

/* The work's own description, between the buy block and the artist block.
 * Matches the artist block's rule-and-heading treatment so the sheet reads as
 * two stacked chapters — this work, then who made it. The copy is set larger
 * than the artist bio: it is the artist's line about the piece, not metadata. */
.about-work { margin-top: 46px; padding-top: 26px; border-top: 1px solid var(--line); }
.about-work h3 {
  font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-soft); font-weight: 600; margin: 0 0 10px;
}
.about-work p {
  font-family: var(--serif); font-size: 17px; line-height: 1.55;
  margin: 0; max-width: 46ch;
}

/* ------------------------------------------------------------------- GRID
 * The explorer grid under the ring. Two columns on a phone, three from 720px.
 *
 * Two columns rather than one: at 393px a single column shows one work per
 * screen, which is the ring's job already. Two lets someone compare and scan,
 * which is the entire reason this view exists.
 *
 * Tiles carry no border or card chrome — the prints sit directly on the paper
 * background with generous gutters, the way a print shop hangs work.
 */
/* 18px matches .hero, .head-in and .slab — the page's horizontal rhythm.
 * (Was `var(--pad)`, a variable this sheet never defines, so the whole
 * declaration was invalid and the grid bled to the screen edge.) */
.grid-sec { max-width: 1080px; margin: 62px auto 0; padding: 0 18px; }
.grid-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 14px; margin-bottom: 18px;
  padding-bottom: 12px; border-bottom: 1px solid var(--line);
}
.grid-head h2 { font-family: var(--serif); font-weight: 400; font-size: 22px; margin: 0; }
.grid-count { font-size: 11.5px; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-soft); }

.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 26px 16px; }

.tile {
  display: block; width: 100%; text-align: left;
  border: 0; padding: 0; background: none; cursor: pointer;
  -webkit-appearance: none; appearance: none;
  font: inherit; color: inherit;
}
/* The white margin belongs to the paper, not the tile, so the image sits on a
 * white field the way a mounted print does. */
.tile-art {
  display: block; background: #fff; padding: 9px;
  box-shadow: 0 1px 2px rgba(20,17,15,.10), 0 12px 22px -14px rgba(20,17,15,.45);
  transition: transform .18s ease, box-shadow .18s ease;
}
.tile-art img { display: block; width: 100%; aspect-ratio: 675 / 878; object-fit: contain; }
.tile:hover .tile-art,
.tile:focus-visible .tile-art {
  transform: translateY(-3px);
  box-shadow: 0 2px 4px rgba(20,17,15,.12), 0 20px 30px -16px rgba(20,17,15,.55);
}
.tile:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

.tile-cap { display: block; margin-top: 11px; }
.tile-title { display: block; font-family: var(--serif); font-size: 16px; line-height: 1.25; }
.tile-by { display: block; font-size: 13px; color: var(--ink-soft); margin-top: 2px; }
.tile-price { display: block; font-size: 12px; color: var(--ink-soft); margin-top: 5px; letter-spacing: .04em; }

@media (min-width: 720px) {
  .grid { grid-template-columns: repeat(3, 1fr); gap: 40px 26px; }
  .grid-sec { margin-top: 84px; }
  .grid-head h2 { font-size: 26px; }
}

/* Avatar + name lockup: a face does more for trust on a cold page than any
 * amount of copy about provenance. */
.artist-id { display: flex; align-items: center; gap: 13px; margin-bottom: 14px; }
.artist-id img {
  width: 54px; height: 54px; border-radius: 50%; object-fit: cover;
  background: var(--paper-warm); flex: none;
}
.artist-id h3 { margin: 0; }
.artist-more { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 14px; }
.artist-more img { width: 100%; aspect-ratio: 675 / 878; object-fit: contain; border-radius: 2px; }
.artist-links { margin-top: 16px; display: flex; gap: 10px; flex-wrap: wrap; }

/* ---------------------------------------------------------------- checkout
 * Deliberately the least creative surface in the build (Mel: "checkout should
 * be straightforward and less creative"). Express pay first — the biggest
 * single mobile conversion lever available to us.
 */
.co-line { display: flex; justify-content: space-between; gap: 12px; padding: 10px 0; font-size: 14.5px; }
.co-line.total { border-top: 1px solid var(--line); margin-top: 6px; padding-top: 14px; font-weight: 600; }
.co-sep { display: flex; align-items: center; gap: 10px; margin: 18px 0; color: var(--ink-soft); font-size: 12px; letter-spacing: .1em; text-transform: uppercase; }
.co-sep::before, .co-sep::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.field { display: block; margin-bottom: 11px; }
.field input {
  width: 100%; padding: 13px 14px; font-size: 16px; /* 16px: prevents iOS zoom-on-focus */
  border: 1px solid var(--line); border-radius: 8px; background: #fff; color: var(--ink);
}
/* Added-to-cart confirmation. Replaces the buy actions in place rather than
 * appearing as a toast: the next step should be the only thing competing for
 * attention once a print is in the cart (PR #16, Act1Page.tsx L203-220). */
.added-head {
  display: flex; align-items: center; gap: 8px;
  font-size: 15px; font-weight: 500; margin-bottom: 10px;
}
.added-check {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--ink); color: var(--paper); font-size: 12px;
}
.added-detail { margin: 0 0 14px; display: grid; gap: 5px; }
.added-detail > div { display: flex; justify-content: space-between; gap: 12px; }
.added-detail dt { font-size: 13px; color: var(--ink-soft); }
.added-detail dd { margin: 0; font-size: 13px; text-align: right; }

/* Cart button in the header. Hidden while empty (the `hidden` attribute), so
 * it costs nothing until there is something in it.
 *
 * It used to be a bare icon on a transparent background, which disappeared
 * against the artwork — on a paid-traffic funnel the one control that leads to
 * checkout cannot be the least visible thing on the page. It is now a solid
 * pill: filled, outlined, and readable over any image behind it. */
.cart-btn {
  position: relative; display: inline-flex; align-items: center; gap: 7px;
  min-width: 44px; min-height: 44px; padding: 0 13px; margin-left: auto;
  background: var(--ink); color: var(--paper);
  border: 0; border-radius: 999px; cursor: pointer;
  box-shadow: 0 1px 3px rgba(0,0,0,.18);
  transition: transform .12s ease, box-shadow .12s ease;
}
.cart-btn:hover { box-shadow: 0 2px 7px rgba(0,0,0,.24); }
.cart-btn:active { transform: translateY(1px); }
.cart-btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.cart-btn svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.7; }
/* The count sits ON the dark pill, so it inverts: light chip, dark numeral. */
.cart-btn span {
  min-width: 18px; height: 18px; padding: 0 5px; box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--paper); color: var(--ink);
  border-radius: 9px; font-size: 11.5px; font-weight: 700; line-height: 1;
}
.top-links { margin-left: 12px; }

/* Quantity stepper. 44px minimum on every control — PR #16 found the source's
 * 28x26px steppers and 11px Remove link below the touch target minimum. */
.stepper {
  display: inline-flex; align-items: center;
  border: 1px solid var(--line); border-radius: 2px; background: #fff;
}
.stepper button {
  min-width: 44px; min-height: 44px; padding: 0;
  background: none; border: 0; cursor: pointer;
  font-size: 17px; line-height: 1; color: var(--ink);
}
.stepper button:disabled { opacity: .3; cursor: default; }
.stepper span {
  min-width: 30px; text-align: center; font-size: 15px;
  font-variant-numeric: tabular-nums;
}
.stepper-sm button { min-width: 44px; min-height: 44px; font-size: 15px; }
.stepper-sm span { min-width: 24px; font-size: 14px; }

.buy-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px; margin-bottom: 12px;
}
.buy-row .price { margin: 0; }

/* Order lines in checkout. The grid restacks the controls onto their own row so
 * the stepper and Remove each own a full-height target (PR #16, M4). */
.co-items { margin-bottom: 6px; }
.co-item {
  display: grid; gap: 4px 12px; padding: 12px 0;
  grid-template-columns: 52px 1fr auto;
  grid-template-areas: "thumb main price" "thumb controls controls";
  border-bottom: 1px solid var(--line);
}
.co-thumb {
  grid-area: thumb; width: 52px; height: 68px;
  object-fit: cover; background: #fff; border: 1px solid var(--line);
}
.co-item-main { grid-area: main; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.co-item-main strong { font-size: 15px; font-weight: 500; }
.co-item-main span { font-size: 12.5px; color: var(--ink-soft); }
.co-item-price { grid-area: price; font-size: 15px; font-weight: 500; white-space: nowrap; }
.co-item-controls {
  grid-area: controls; display: flex; align-items: center;
  justify-content: space-between; gap: 12px; margin-top: 2px;
}
.co-remove {
  min-height: 44px; padding: 0 4px; background: none; border: 0;
  cursor: pointer; font: inherit; font-size: 13px;
  color: var(--ink-soft); text-decoration: underline;
}
.co-addmore {
  min-height: 44px; padding: 0; margin-bottom: 10px;
  background: none; border: 0; cursor: pointer;
  font: inherit; font-size: 13.5px; color: var(--ink-soft);
}

/* Checkout address form. Ported from manifoldxyz/victor-drop PR #16.
 *
 * 16px inputs are not a taste call: iOS Safari zooms the viewport when a
 * focused input is under 16px, which throws the buyer out of the layout
 * mid-checkout. Everything tappable clears 44px for the same reason.
 */
.field-set { display: grid; gap: 12px; margin-top: 14px; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.field .flabel {
  display: block; font-size: 12px; letter-spacing: .04em;
  color: var(--ink-soft); margin-bottom: 5px;
}
.field .flabel em { font-style: normal; opacity: .65; }
.field input, .field select {
  width: 100%; box-sizing: border-box;
  font: inherit; font-size: 16px; line-height: 1.3;
  min-height: 44px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 2px;
  background: #fff; color: var(--ink);
  -webkit-appearance: none; appearance: none;
}
.field select { background-image: none; }
.field input:focus, .field select:focus {
  outline: 2px solid var(--ink); outline-offset: -1px; border-color: var(--ink);
}
.field.has-error input, .field.has-error select { border-color: #a3231d; }
.ferror { display: block; margin-top: 5px; font-size: 12.5px; color: #a3231d; }

.consent { display: flex; gap: 11px; align-items: flex-start; margin: 18px 0 6px; padding: 14px; background: var(--paper-warm); border-radius: 8px; }
.consent input { margin-top: 3px; width: 19px; height: 19px; flex: 0 0 auto; }
.consent label { font-size: 13.5px; line-height: 1.5; }

.done-state { text-align: center; padding: 60px 0; }
.done-state h2 { font-family: var(--serif); font-weight: 400; font-size: 27px; margin: 0 0 10px; }
.done-state p { color: var(--ink-soft); font-size: 14.5px; max-width: 40ch; margin: 0 auto; }

/* ---------------------------------------------------------------- footer
 * Reduced to a legal line (Mel, 8/25). The shipping, returns and contact copy
 * that used to live here now sits on the product page, where it is read at the
 * decision point rather than after it.
 *
 * NOTE: the trust burden it carried has not gone away — a cold buyer still
 * needs a real returns policy and a contact route somewhere reachable before
 * launch. Terms/Privacy are placeholder links.
 */
footer.foot { border-top: 1px solid var(--line); margin-top: 50px; padding: 26px var(--gutter) 46px; }
.foot-in {
  /* Full-bleed to the viewport (Mel, 2026-09-14), unlike the header and body
     which share --measure. The footer is a baseline rule, so pinning the
     copyright and the legal links to the actual page edges reads as deliberate
     rather than as a third, different content width. */
  max-width: none; margin: 0;
  /* Copyright left, legal links right, on one line. Wraps to stacked rows on
   * narrow phones rather than letting the two collide. */
  display: flex; flex-wrap: wrap; gap: 6px 18px;
  justify-content: space-between; align-items: baseline;
  font-size: 12.5px; color: var(--ink-soft);
}
.foot-legal a { color: var(--ink-soft); text-decoration: none; }

/* ---------------------------------------------------------------- mobile
 * Explicit phone pass (Mel, 8/25). Priorities in order: nothing important below
 * a thumb-reach line, no accidental horizontal scroll, tap targets ≥44px, and
 * the buy action never more than a thumb away on the product page.
 */
@media (max-width: 700px) {
  .hero { padding: 26px 16px 8px; }
  .hero h1 { font-size: clamp(28px, 9vw, 40px); }
  .hero p { font-size: 14.5px; }
  .hero .facts { gap: 6px; }
  .hero .facts span { padding: 5px 9px; font-size: 10.5px; }

  .slab { padding: 16px 16px 26px; min-height: 88vh; }
  .slab .art img { max-height: 58vh; padding: 8px; }
  .slab .meta { flex-direction: column; align-items: stretch; gap: 12px; }
  .slab .meta .btn.small { width: 100%; padding: 14px; font-size: 15px; }

  .sheet-in { padding: 0 16px 120px; }
  .pdp-art { padding: 18px 12px; }
  .pdp-art img { max-height: 46vh; padding: 8px; }
  .mock { height: 230px; }
  .mock img { height: 150px; }
  .pdp-head h2 { font-size: 23px; }
  .artist-more { grid-template-columns: repeat(3, 1fr); gap: 6px; }

  /* Ring on phones: taller stage so the larger sheets are not cropped
   * vertically. Less top padding than desktop — the smaller hero needs less
   * clearance, and 78px pushed the artwork unnecessarily far down. */
  /* 64px top: at 40px the highest sheet in the 3D arc clipped under the
   * sticky header on a phone (Mel, 8/31). The arc's apex sits above the
   * ring's own box, so the stage needs clearance the ring height does
   * not account for. */
  /* Mobile stage: a fixed 58vh box clipped the arc's projection at the top the
   * same way the desktop stage did. Height is left to the content so the
   * padding actually creates clearance instead of being absorbed. */
  .ring-stage { height: auto; min-height: 0; padding-top: 116px; padding-bottom: 30px; }

  .foot-in { font-size: 12.5px; }

  /* Every interactive element clears the 44px minimum. */
  .switch button { min-height: 30px; }
  .rack-nav button { width: 46px; height: 46px; }
  .btn { padding: 16px 22px; }
}

/* Buy bar pinned within thumb reach: on a phone the price and the pay button
 * should never require a scroll-back once the artwork has been seen. */
@media (max-width: 700px) {
  .buy {
    position: sticky; bottom: 0; z-index: 4;
    /* Opaque from the top edge. The previous gradient started fully transparent
     * and only reached paper at 22%, so whatever sat behind the bar — the tier
     * spec panel — bled through its upper band and read as overlapping text. */
    background: var(--paper);
    box-shadow: 0 -10px 14px -8px rgba(244,241,236,.95);
    padding: 12px 0 calc(12px + env(safe-area-inset-bottom, 0));
    margin-top: 18px;
  }
  .buy .price { font-size: 24px; }
}

/* ---------------------------------------------------------------- desktop */
@media (min-width: 860px) {
  .hero { padding-top: 54px; }
  .slab { grid-template-columns: 1fr; }
  .sheet-in { padding-bottom: 90px; }
  .pdp-grid { display: grid; grid-template-columns: 1.15fr .85fr; gap: 34px; align-items: start; }
  .artist-more { grid-template-columns: repeat(4, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}


/* ── Phase 0: pre-launch waitlist ──────────────────────────────────────────
   Sits above the browse rather than replacing it: the prints are the reason
   anyone gives us an email, so they stay visible. Uses the existing palette
   and type scale — this is the same shop, one phase earlier.               */

.wl {
  padding: 0 var(--gut, 20px);
  margin: 0 0 8px;
}

.wl-card {
  max-width: 640px;
  margin: 0 auto;
  padding: 26px 24px 22px;
  background: var(--paper-warm);
  border: 1px solid rgba(20, 17, 15, 0.14);
  border-radius: 14px;
}

.wl-kicker {
  margin: 0 0 8px;
  font-family: var(--serif);
  font-size: 11px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.wl-title {
  margin: 0 0 8px;
  font-family: var(--serif);
  font-weight: 400;
  font-size: 25px;
  line-height: 1.2;
  letter-spacing: -0.01em;
}

.wl-sub {
  margin: 0 0 18px;
  max-width: 46ch;
  color: var(--ink-soft);
  font-size: 15px;
  line-height: 1.55;
}

/* Countdown. Tabular figures so the seconds column does not jitter the
   layout every tick — a shifting row reads as broken, not as urgent. */
.wl-count {
  display: flex;
  gap: 10px;
  margin: 0 0 20px;
  font-variant-numeric: tabular-nums;
}

.wl-unit {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 58px;
  padding: 8px 6px;
  background: var(--paper);
  border: 1px solid rgba(20, 17, 15, 0.1);
  border-radius: 8px;
}

.wl-unit b {
  font-family: var(--serif);
  font-size: 24px;
  font-weight: 400;
  line-height: 1;
}

.wl-unit i {
  margin-top: 4px;
  font-style: normal;
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.wl-closed {
  font-family: var(--serif);
  font-size: 18px;
  color: var(--ink-soft);
}

.wl-label {
  display: block;
  margin: 0 0 6px;
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.wl-row {
  display: flex;
  gap: 8px;
}

.wl-input {
  flex: 1 1 auto;
  min-width: 0; /* or the input refuses to shrink inside the flex row */
  /* 16px minimum: anything smaller makes iOS Safari zoom the viewport on
     focus, which on a paid landing page reads as the page breaking. */
  font-size: 16px;
  font-family: var(--sans);
  padding: 12px 14px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid rgba(20, 17, 15, 0.22);
  border-radius: 9px;
}

.wl-input:focus {
  outline: 2px solid var(--ink);
  outline-offset: 1px;
}

.wl-input:disabled {
  opacity: 0.6;
}

.wl-btn {
  flex: 0 0 auto;
  min-height: 44px; /* thumb-sized tap target */
  padding: 12px 18px;
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 600;
  color: var(--paper);
  background: var(--ink);
  border: 0;
  border-radius: 9px;
  cursor: pointer;
}

.wl-btn:disabled {
  opacity: 0.55;
  cursor: default;
}

.wl-consent {
  display: flex;
  gap: 9px;
  align-items: flex-start;
  margin: 12px 0 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ink-soft);
  cursor: pointer;
}

.wl-consent input {
  margin-top: 2px;
  width: 17px;
  height: 17px;
  flex: 0 0 auto;
  accent-color: var(--ink);
}

.wl-msg {
  margin: 10px 0 0;
  min-height: 18px;
  font-size: 13.5px;
}

.wl-ok {
  color: #2f6b4f;
}

.wl-err {
  color: #a33a26;
}

@media (max-width: 430px) {
  .wl-card { padding: 20px 18px 18px; }
  .wl-title { font-size: 21px; }
  .wl-row { flex-wrap: wrap; }
  .wl-btn { width: 100%; }
  .wl-unit { min-width: 0; flex: 1 1 0; }
}

@media (prefers-reduced-motion: reduce) {
  .wl * { transition: none !important; }
}

/* Waitlist redesign — 2026-09-14.
 * Two columns: the ask on the left, a rotating stack of prints on the right.
 * The split is load-bearing — single-column left the form stretching the full
 * measure and a dead right half under the headline. */
.wl { margin: 0; padding: 0; }

.wl-split {
  max-width: var(--measure);
  margin: 0 auto;
  padding: clamp(40px, 6vh, 80px) var(--gutter) clamp(56px, 8vh, 96px);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 420px);
  gap: clamp(32px, 5vw, 80px);
  align-items: center;
}

.wl-card {
  min-width: 0;
  max-width: 540px;
  margin: 0;
  padding: 0;
  display: grid;
  align-content: center;
  background: transparent;
  border: 0;
  border-radius: 0;
}

.wl-lead { max-width: 100%; }

.wl-kicker {
  margin: 0 0 18px;
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.wl-title {
  max-width: 13ch;
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(40px, 4.6vw, 68px);
  font-weight: 400;
  line-height: .98;
  letter-spacing: -.035em;
}

.wl-title span {
  display: block;
  margin-top: .1em;
  color: var(--ink-soft);
  font-style: italic;
  /* Keeps "Oct 13 at 12p PT" on one line — it otherwise breaks after "12p" and
     leaves "PT" orphaned. */
  white-space: nowrap;
}

/* The description is now a sentence about the run, so it gets the serif and a
   longer measure; the window date below stays mono and quiet. */
.wl-sub {
  margin: 18px 0 0;
  max-width: 46ch;
  color: var(--ink);
  font-family: var(--mono);
  font-size: clamp(12.5px, 1.3vw, 14px);
  letter-spacing: .01em;
  line-height: 1.5;
}

.wl-window {
  margin: 10px 0 0;
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .04em;
  color: var(--ink-soft);
}

/* Countdown: no rules, no boxes. Left-aligned with the headline it belongs to
   rather than centred on a page it no longer spans. */
.wl-count {
  display: flex;
  justify-content: flex-start;
  width: auto;
  gap: clamp(18px, 2.4vw, 32px);
  margin: 0 0 clamp(26px, 4vh, 44px);
  border: 0;
  font-variant-numeric: tabular-nums;
}

.wl-unit {
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
}

.wl-unit b {
  font-family: var(--serif);
  font-size: clamp(26px, 3.2vw, 38px);
  font-weight: 400;
  line-height: .95;
  letter-spacing: -.03em;
}

.wl-unit i {
  margin-top: 8px;
  font-family: var(--mono);
  font-size: 8.5px;
  font-style: normal;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.wl-open,
.wl-closed {
  padding: 8px 0;
  font-family: var(--serif);
  font-size: 22px;
}

/* Capped at 440px: an email field wider than this reads as a search bar, and
   the extra width buys nothing — addresses are short. */
.wl-form {
  width: min(100%, 440px);
  margin-top: clamp(22px, 3.4vh, 32px);
}

.wl-label {
  margin-bottom: 8px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .18em;
}

/* Hidden visually, still announced. clip-path + 1px box is the technique that
   survives; display:none would remove it from the accessibility tree. */
.wl-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
  border: 0;
}

.wl-row { display: flex; gap: 0; }

.wl-input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 52px;
  padding: 13px 15px;
  background: rgba(255, 255, 255, .5);
  border: 1px solid var(--ink);
  border-right: 0;
  border-radius: 0;
  /* 16px minimum: below that iOS Safari zooms the viewport on focus, which on
     a paid landing page reads as the page breaking. */
  font-size: 16px;
  font-family: var(--mono);
  letter-spacing: .01em;
}

.wl-input::placeholder {
  color: var(--ink-soft);
  opacity: 1; /* Firefox dims placeholders by default. */
}

.wl-input:focus {
  position: relative;
  z-index: 1;
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.wl-btn {
  flex: 0 0 auto;
  min-height: 52px;
  padding: 13px 22px;
  border: 1px solid var(--ink);
  border-radius: 0;
  font-family: var(--mono);
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.wl-consent {
  width: auto;
  max-width: 44ch;
  margin: 11px 0 0;
  color: var(--ink-soft);
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .02em;
  line-height: 1.5;
}

/* ── The print stack ──────────────────────────────────────────────────────
   Each work is mounted on a paper sheet with a generous mat, so it reads as
   "a print of something" rather than as a product tile. Print-shop cue, not a
   catalogue: no price, no size, no buy affordance anywhere near it. */
.wl-stage {
  min-width: 0;
  display: grid;
  justify-items: center;
}

.wl-posters {
  position: relative;
  width: 100%;
  max-width: 372px;
  /* Every source is 1092x1423. Matching the sheet to that ratio (plus the mat
     and caption) keeps the mat even on all four sides instead of leaving a
     thick band above and below a portrait image in a squarer box. Reserving
     the height also means rotation never reflows the page. */
  aspect-ratio: 300 / 430;
}

.wl-poster {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  /* Breathing room between the sheet and the artist's name (Mel, 2026-09-15). */
  gap: 18px;
  /* Opacity, not display: display:none defers lazy images forever and the
     next slide would pop in blank on first rotation.
     
     visibility MUST carry a delay equal to the fade. It is not an animatable
     property, so listing it without one flipped the outgoing slide to hidden
     on frame 1 — there was no crossfade at all, just a hard cut out followed
     by a fade in, which is what read as jitter. Delaying it to the end of the
     fade lets the outgoing slide actually fade, then drop out of the a11y tree
     and stop catching clicks. */
  opacity: 0;
  visibility: hidden;
  transition: opacity 900ms cubic-bezier(.33, 0, .2, 1), visibility 0s linear 900ms;
  text-decoration: none;
  color: inherit;
  /* Promote to its own layer so a crossfade of two large images does not
     repaint the whole column each frame. */
  will-change: opacity;
}

.wl-poster.is-active {
  opacity: 1;
  visibility: visible;
  transition: opacity 900ms cubic-bezier(.33, 0, .2, 1), visibility 0s linear 0s;
}

/* Static hand-laid tilt on the SHEET, not an animated transform on the slide.
   Rotating each slide into place meant two sheets tilting opposite ways during
   every crossfade — the wobble Mel saw. The tilt is now a fixed property of
   each sheet, so the transition is a pure dissolve with nothing in motion. */
.wl-poster:nth-child(odd) .wl-sheet { transform: rotate(-0.5deg); }
.wl-poster:nth-child(even) .wl-sheet { transform: rotate(0.5deg); }

.wl-sheet {
  min-height: 0;
  display: grid;
  place-items: center;
  /* The mat. Paper white against the page's warm stock, with a real edge.
     Kept tight: at 30px the artwork rendered ~290px inside a 380px sheet and
     read as a small picture in a big frame rather than as a print. */
  padding: clamp(12px, 1.6vw, 16px);
  background: #fff;
  border: 1px solid var(--line);
  box-shadow: 0 14px 34px -22px rgba(20, 17, 15, .55);
}

.wl-sheet img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  /* contain here, unlike the old thumbnails: at this size the whole piece is
     visible, so cropping it would misrepresent the work rather than swatch it. */
  object-fit: contain;
}

/* The announcement slide is a print too, so it keeps the same mat, border and
   shadow as the artwork sheets. Nothing dashed, nothing "empty state".
   Applies to both the <video> and its reduced-motion <img> fallback. */
.wl-more-art {
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  /* The clip is a cream wall; without this it sits on white with a faint seam. */
  background: #fff;
}

/* Captions cross-dissolve FASTER than the sheets and with no overlap window.
   Two artist names fading through each other at 50/50 produced superimposed
   text that read as a rendering fault, not as a transition. The outgoing name
   is gone before the incoming one arrives. */
.wl-poster-cap {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-align: center;
  color: var(--ink-soft);
  opacity: 0;
  transition: opacity 260ms ease 0s;
}

.wl-poster.is-active .wl-poster-cap {
  opacity: 1;
  /* Waits for the outgoing caption to clear before fading in. */
  transition: opacity 340ms ease 380ms;
}

.wl-stage-cap {
  max-width: 40ch;
  /* Wider gap under the artist name before the roster line, so the two mono
     blocks read as separate information rather than one paragraph. */
  margin: clamp(30px, 4.2vh, 44px) 0 0;
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.6;
  text-align: center;
  color: var(--ink);
}

.wl-stage-cap a {
  color: inherit;
  text-underline-offset: 3px;
}

.wl-stage-note {
  max-width: 36ch;
  /* Set apart from the roster line above it: this is a caveat, not part of the
     credit. Lighter than --ink-soft so it recedes to a footnote. */
  margin: 22px 0 0;
  font-size: 10.5px;
  line-height: 1.5;
  text-align: center;
  color: color-mix(in srgb, var(--ink) 38%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  /* No dissolve, no tilt. The stack does not rotate for these visitors anyway
     (startPosters bails out), so this only covers the first paint. */
  .wl-poster { transition: none; }
  /* Must match the specificity of the .wl-poster:nth-child(n) .wl-sheet rules
     above, or the tilt wins and these visitors still get rotated sheets. */
  .wl-poster:nth-child(odd) .wl-sheet,
  .wl-poster:nth-child(even) .wl-sheet { transform: none; }
}

@media (max-width: 900px) {
  .wl-split {
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(40px, 7vh, 64px);
    padding: 40px 20px 72px;
    align-items: start;
  }

  .wl-card { max-width: none; }
  .wl-posters { max-width: min(70vw, 300px); }
  .wl-stage { justify-items: center; }
}

@media (max-width: 560px) {
  .wl-split { padding: 32px 18px 64px; }
  .wl-title { font-size: clamp(34px, 10.4vw, 52px); }
  .wl-count { gap: 20px; margin-bottom: 26px; }
  .wl-unit b { font-size: clamp(26px, 8vw, 34px); }

  .wl-row { flex-wrap: wrap; gap: 8px; }
  .wl-input { flex-basis: 100%; border-right: 1px solid var(--ink); }
  .wl-btn { width: 100%; }
}

/* ── Scene states ──────────────────────────────────────────────────────────
   One class on <html> decides which shop the visitor sees. CSS does the
   hiding rather than JS removing nodes: the markup stays identical across
   states, so switching is instant and nothing has to be rebuilt.        */

/* PRELAUNCH — no product catalogue or buy affordances. Portfolio examples from
   confirmed participants appear only inside the clearly labelled artist area. */
.scene-prelaunch #viewRing,
.scene-prelaunch #viewGrid,
.scene-prelaunch #viewRoom,
.scene-prelaunch #viewRack,
.scene-prelaunch #viewWall,
.scene-prelaunch #curtain,
.scene-prelaunch .hero,
.scene-prelaunch .cart-btn,
.scene-prelaunch .switch {
  display: none !important;
}

/* The header X link stays in prelaunch (Mel, 2026-09-14) — it is the one place
   a visitor who is not ready to hand over an email can still follow along.
   Only the manifold.xyz arrow is dropped: it points at the corporate site,
   which is off-message on a print storefront. */
.scene-prelaunch .top-links a[aria-label="manifold.xyz"] {
  display: none !important;
}

/* LIVE — the shop as built. The waitlist card steps aside for the browse;
   sold-out and restock capture reappear inside the product sheet. */
.scene-live .wl { display: none; }

/* ENDED — closed for good, and it must look deliberate rather than broken.
   Artwork stays visible: it is the proof of what the drop was, and it is what
   makes someone join the list for the next one. Buying is gone. */
.scene-ended .cart-btn,
.scene-ended .switch { display: none !important; }

.scene-ended #viewRing,
.scene-ended #viewGrid {
  /* Present but clearly past tense. */
  opacity: 0.72;
  filter: saturate(0.82);
}

/* ── Design-only scene switcher ────────────────────────────────────────────
   Renders only with ?design=1. Never visible to a buyer.              */

.scenebar {
  position: fixed;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  z-index: 9999;
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: calc(100vw - 24px);
  padding: 7px 9px 7px 12px;
  background: rgba(20, 17, 15, 0.94);
  color: #f4f1ec;
  border-radius: 999px;
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.28);
  font-family: var(--sans);
  /* Sits above the mobile drawer but must not block a tap on the page behind
     it when the reviewer is looking at something underneath. */
  backdrop-filter: blur(6px);
}

.scenebar-tag {
  font-size: 9.5px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  opacity: 0.62;
  flex: 0 0 auto;
}

.scenebar-tabs {
  display: flex;
  gap: 3px;
  background: rgba(255, 255, 255, 0.09);
  border-radius: 999px;
  padding: 3px;
}

.scenebar-tabs button {
  min-height: 30px;
  padding: 5px 13px;
  font-size: 12.5px;
  font-family: inherit;
  color: rgba(244, 241, 236, 0.72);
  background: transparent;
  border: 0;
  border-radius: 999px;
  cursor: pointer;
}

.scenebar-tabs button[aria-current='true'] {
  background: #f4f1ec;
  color: #14110f;
  font-weight: 600;
}

.scenebar-note {
  font-size: 11.5px;
  opacity: 0.66;
  max-width: 30ch;
  line-height: 1.3;
}

@media (max-width: 620px) {
  /* The note is the first thing to go: the tabs are the tool. */
  .scenebar-note { display: none; }
  .scenebar { bottom: 10px; padding: 6px 8px 6px 10px; }
}


/* ── Order confirmation page ───────────────────────────────────────────────
   Shares the document with the shop: deep routes fall back to index.html, so
   /shop/order/<ref> is this stylesheet hiding the shop rather than a second
   HTML entry point duplicating the head, fonts and analytics bootstrap.

   `.route-order` is set by scene.js in <head>, before first paint. Doing it in
   JS after load would flash the whole shop first. */
html.route-order body > *:not(#orderRoot) { display: none !important; }
html.route-order #orderRoot { display: block; }
html.route-order { background: var(--paper); }

.order-root[hidden] { display: none; }
html.route-order .order-root[hidden] { display: block; }

.order-root {
  max-width: 640px;
  margin: 0 auto;
  padding: 48px 20px 80px;
}

.order-card {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 30px 28px;
}

.order-h {
  margin: 0 0 8px;
  font-size: 26px;
  line-height: 1.2;
  letter-spacing: -0.02em;
}

.order-sub {
  margin: 0 0 22px;
  color: var(--ink-soft);
  font-size: 15.5px;
  line-height: 1.6;
}

/* The reference is what a buyer quotes in an email, so it is the one thing on
   this page that must be findable without scrolling and legible when retyped. */
.order-ref {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 14px 16px;
  margin: 0 0 22px;
  background: var(--paper);
  border-radius: 9px;
}
.order-ref-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-soft);
}
.order-ref-value {
  font-family: ui-monospace, Menlo, monospace;
  font-size: 20px;
  letter-spacing: 0.04em;
}

.order-items { list-style: none; margin: 0 0 20px; padding: 0; }
.order-item {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 16px;
  padding: 14px 0;
  border-bottom: 1px solid var(--line);
}
.order-item-main { display: flex; flex-direction: column; gap: 2px; }
.order-item-title { font-weight: 600; }
.order-item-artist { font-size: 14px; color: var(--ink-soft); }
.order-item-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 3px;
  white-space: nowrap;
}

/* The edition number IS the product for a limited print — it earns emphasis. */
.order-edition {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.order-edition.pending {
  font-weight: 400;
  font-style: italic;
  color: var(--ink-soft);
  max-width: 190px;
  white-space: normal;
  text-align: right;
}
.order-qty { font-size: 13px; color: var(--ink-soft); }
.order-price { font-size: 15px; }

.order-totals { margin: 0 0 24px; }
.order-total-row {
  display: flex;
  justify-content: space-between;
  padding: 6px 0;
  font-size: 15px;
  color: var(--ink-soft);
}
.order-total-row.strong {
  color: var(--ink);
  font-weight: 700;
  font-size: 17px;
  border-top: 1px solid var(--line);
  margin-top: 6px;
  padding-top: 12px;
}

.order-back {
  display: inline-block;
  color: var(--ink);
  font-size: 15px;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Checkout failure message. Errors belong next to the button that failed, not
   in a toast that disappears before it is read. */
.co-error {
  margin: 0 0 10px;
  padding: 10px 12px;
  border-radius: 8px;
  background: #fbf3f1;
  border-left: 3px solid #a33a26;
  color: #7d2c1d;
  font-size: 14px;
  line-height: 1.5;
}

@media (max-width: 560px) {
  .order-root { padding: 28px 16px 60px; }
  .order-card { padding: 22px 18px; }
  .order-h { font-size: 22px; }
}


/* ── Checkout: two columns on desktop ──────────────────────────────────────
   Mel, 9/2: the checkout was one long column, so after filling in an address
   the totals had scrolled off the top and the only number left on screen was
   inside the button — "it's not clear how much you're paying for what".

   Two columns fixes the cause rather than the symptom: the order and its
   totals sit in their own sticky column and simply never leave the viewport
   while the address is being typed. */
.co-grid { display: block; }

@media (min-width: 860px) {
  .co-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
    gap: 38px;
    align-items: start;
  }
  /* Sticky, with its own scroll allowance: a tall cart must still be reachable
     rather than clipped at the viewport edge. */
  .co-summary {
    position: sticky;
    top: 18px;
    max-height: calc(100vh - 120px);
    overflow-y: auto;
    padding: 20px 22px;
    background: var(--paper-warm, #efece6);
    border-radius: 13px;
  }
  .co-pay { padding-top: 2px; }
  /* The express button leads the payment column, so it needs no top margin. */
  .co-pay .btn.pay { margin-top: 0; }
}

/* On mobile the summary stays inline and above, which is the correct reading
   order: what am I buying, then how do I pay. The sticky bar below carries the
   total once the form is long. */
@media (max-width: 859px) {
  .co-summary {
    padding: 15px 16px;
    background: var(--paper-warm, #efece6);
    border-radius: 12px;
    margin-bottom: 18px;
  }
}

/* Field hint under an input — used by the ZIP lookup. Reserves no space when
   hidden, so the form does not jump as it appears. */
.field-hint {
  display: block;
  margin-top: 5px;
  font-size: 12.5px;
  color: var(--ink-soft);
}
.field-hint[hidden] { display: none; }

/* ── Desktop dialog treatment ──────────────────────────────────────────────
   Mel, 9/2: "kind of hard to click out of on desktop".

   She was right, and the reason was structural rather than cosmetic: the sheet
   was `position: fixed; inset: 0`, a full-viewport overlay. There was no
   "outside" to click — the panel WAS the whole screen. Escape worked, but a
   mouse had nowhere to go.

   On desktop it now becomes a real centred dialog with a dimmed backdrop, so
   clicking away from it does the obvious thing. Mobile keeps the sheet, which
   is correct there — a phone has no room for a backdrop worth clicking. */
@media (min-width: 860px) {
  .sheet {
    inset: 0;
    display: grid;
    grid-template-rows: 1fr;
    place-items: center;
    background: rgba(20, 17, 15, .46);
    /* The backdrop fades; it must not inherit the panel's slide-up. */
    transform: none;
    opacity: 0;
    transition: opacity .22s ease, visibility .22s;
    padding: 32px 20px;
    overflow-y: auto;
  }
  .sheet.open { opacity: 1; transform: none; }

  /* The panel. The markup is .sheet > .sheet-bar + .sheet-in, so the two are
     glued into one visual card by pinning the bar to the top of .sheet-in's
     column and giving both the same background. */
  .sheet > .sheet-bar,
  .sheet > .sheet-in {
    width: min(980px, 100%);
    background: var(--paper);
    position: relative;
  }
  .sheet > .sheet-bar {
    border-radius: 16px 16px 0 0;
    padding: 12px 22px 0;
    box-shadow: 0 -14px 40px rgba(0,0,0,.16);
    display: flex;
    justify-content: flex-end;
  }
  .sheet > .sheet-in {
    border-radius: 0 0 16px 16px;
    padding: 4px 30px 32px;
    max-height: min(84vh, 880px);
    overflow-y: auto;
    box-shadow: 0 22px 50px rgba(0,0,0,.26);
  }
}
