/* The homepage hero: a letterbox entry. The photograph fills the screen below the
   header; a sliver of the NEXT capsule waits at the right edge, and the capsule's
   name sits bottom-left in Cormorant italic. A frame change grows that peek out of
   its slot until it IS the frame — the choreography lives in decompose.js. Nothing
   here is scroll-driven: the page scrolls straight past to the capsules grid. Dark
   ground throughout; the paper site header sits above, untouched.
   Handoff: design_handoff_letterbox_carousel, 2026-09-17. */
body.home.catalog main.hero#decompose { display: block; padding: 0; margin: 0; gap: 0; min-height: 0; max-width: none; }
#decompose { position: relative; background: #0e0d0c; color: #d4cfc9; font-family: 'Geist Mono', monospace; --peek: 148px; --hdr: 52px; --capx: 48px; --capy: 40px; }
#decompose a { color: inherit; text-decoration: none; }

/* The stage: one screen. 🔴 The handoff's 560px floor is GONE, and the comment that
   used to sit here claimed a property the code did not have: 560px can only exceed
   calc(100dvh - var(--hdr)) when the viewport is under ~608px tall, and every one of
   those heights was inside the `@media (max-height: 640px)` override that set the
   floor back to 0. The pair was dead in every viewport either could reach - measured
   1440x641 (declares 560px, stage is 641) and 1440x600 (computes 0px). A craft review
   found it; no gate could, because a floor that never binds changes no geometry. */
.dc-stage { position: relative; height: calc(100dvh - var(--hdr)); overflow: hidden; background: #0e0d0c; touch-action: pan-y; }

/* The photograph IS the way in. The handoff deletes the tiles and the "Enter"
   pill, which left the hero with no door at all - a door with no chrome is what
   it actually asked for, so the frame is the link and the caption below is its
   visible label. It stands down mid-swap: for those 950ms the photograph under
   the pointer is not the one the href names. */
.dc-frame { position: absolute; left: 0; top: 0; right: var(--peek); bottom: 0; overflow: hidden; background: #161412; display: block; cursor: pointer; }
#decompose.dc-swapping .dc-frame { pointer-events: none; }
.dc-frame picture { display: block; height: 100%; }
.dc-frame img { width: 100%; height: 100%; object-fit: cover; filter: grayscale(1) contrast(1.02) brightness(1.03); display: block; transform-origin: 50% 50%; }
/* A floor in the blacks and an open top: he is high-key, not high-contrast, so the
   lift is in the grade and the depth comes from a vignette rather than from crushing
   the shadows. It sits on the STAGE, not the frame, because during a swap the wipe
   is above the frame and a vignette under it would snap on at the end. */
.dc-stage::after { content: ""; position: absolute; left: 0; top: 0; right: var(--peek); bottom: 0; pointer-events: none; z-index: 3; background: radial-gradient(130% 105% at 50% 38%, rgba(14,13,12,0) 42%, rgba(14,13,12,0.30) 100%); }

/* The peek: the next capsule, unnamed, waiting at the edge. Its image is 640px
   wide and CLIPPED by the slot — that crop is the design, not a fit. */
.dc-peek { position: absolute; right: 0; top: 0; bottom: 0; width: var(--peek); overflow: hidden; background: #161412; display: block; padding: 0; margin: 0; border: 0; cursor: pointer; -webkit-appearance: none; appearance: none; }
.dc-peek picture { position: absolute; left: 0; top: 0; bottom: 0; display: block; }
.dc-peek img { width: 640px; height: 100%; object-fit: cover; object-position: 30% center; filter: grayscale(1) contrast(1.02) brightness(0.64); display: block; transition: filter 0.4s cubic-bezier(0.22, 1, 0.36, 1); }
/* 🔴 0.64 at rest, not the handoff's 0.42. The sliver's whole job is the TONAL
   promise of the next photograph, and an art-direction review read it as a near-black
   UI rail rather than a picture - on the Light frame it was the same black column as
   on Earth, i.e. the one thing it exists to say was gone. The ceiling is the sliver's
   own name, which is 10px gold ink sitting directly on it at a 4.5 floor; measured
   8.53:1 at 0.54, so there was headroom and this spends some of it. */
.dc-peek:hover img, .dc-peek:focus-visible img { filter: grayscale(1) contrast(1.02) brightness(0.78); }
/* darkest at the seam, opening toward the edge — it reads as the next photograph
   waiting in shadow instead of a letterboxing bar. ::before, so the name below can
   have ::after and paint OVER it. */
.dc-peek::before { content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 1; background: linear-gradient(to right, rgba(14,13,12,0.82) 0%, rgba(14,13,12,0.28) 55%, rgba(14,13,12,0.10) 100%); }
/* and a foot under the name, because the horizontal gradient is at its WEAKEST
   where the name sits and gold at 10px needs a near-black ground to clear AA */
.dc-peek::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 320px; pointer-events: none; z-index: 1; background: linear-gradient(to top, rgba(14,13,12,0.92) 0%, rgba(14,13,12,0.55) 42%, rgba(14,13,12,0) 100%); }

/* The sliver says what it is. The handoff leaves it unnamed — and then gives the
   button aria-label="Next capsule — Water", so a screen reader is told exactly what
   it is and the person looking at it is not. Set bottom-to-top in the house label
   style; no pill, no border, no new color. */
.dc-peek-name { position: absolute; left: 0; right: 0; bottom: 28px; z-index: 2; margin: 0 auto; width: max-content; writing-mode: vertical-rl; transform: rotate(180deg); font-size: 10px; letter-spacing: 0.24em; text-transform: uppercase; color: #c8a96e; text-shadow: 0 1px 8px rgba(0,0,0,0.85), 0 0 18px rgba(0,0,0,0.6); pointer-events: none; }
.dc-peek-b { opacity: 0; pointer-events: none; }

/* The name, bottom-left, over the photograph. */
/* The name is the second subject of the frame, not a label on it: the number sits
   above it and the title is set at editorial scale. Measured across six names and
   six widths — the ink never comes closer than 20px to the foot of the stage and
   never reaches the peek (worst: Structure at 360px, still 68px clear). */
.dc-caption { position: absolute; left: var(--capx); bottom: calc(var(--capy) + 4px); z-index: 4; display: flex; flex-direction: column; align-items: flex-start; gap: 14px; pointer-events: none; }
/* The handoff gives the caption text-shadows and no ground, and MEASURED over the
   six real photographs that fails the composite contrast gate: 5 of 12 readings
   below the gold number's 4.5 floor and 3 of 12 below the title's 3.0, worst
   Objects at 390px (1.17:1, ink not even detected as painted). The gate wins over
   the locked value. This is the lightest treatment that clears it: ONE radial
   anchored on the ink and falling to zero at the box's top and right edges, so it
   reads as light falling off in the corner rather than the full-width band that
   fogged a third of every frame in #255. Measured after: worst 5.86 (num) and
   8.12 (title) across all six capsules at 390 and 1440. */
/* 🔴 clipped to the FRAME. The vignette starts at the stage's left edge, so an
   uncapped width spills across the seam and tints the peek: at 390px it ran 490px
   wide on a 390px screen, which is how the sliver's own label came out at
   rgb(135,114,76) instead of the gold it is set in. The cap lands its right edge
   exactly on the frame's. It is a treatment for the photograph, not the strip
   beside it. */
.dc-caption::before { content: ""; position: absolute; z-index: -1; pointer-events: none; left: calc(-1 * var(--capx)); bottom: calc(-1 * var(--capy)); width: calc(100% + var(--capx) + 340px); max-width: calc(100vw - var(--peek)); height: calc(100% + var(--capy) + 300px); background: radial-gradient(farthest-side at var(--capx) 100%, rgba(14,13,12,0.96) 0%, rgba(14,13,12,0.82) 60%, rgba(14,13,12,0) 100%); }
/* 🔴 no opacity here. A 0.92 opacity read as a softer gold and cost nothing to
   look at, but ink over a photograph has to be OPAQUE to be measurable, and the
   contrast harness refuses a translucent foreground rather than guessing what it
   composites to. It caught this the first time it ran. Dim the color, never the ink. */
.dc-num { font-size: 10px; letter-spacing: 0.34em; color: #c8a96e; text-shadow: 0 1px 8px rgba(0,0,0,0.6); }
.dc-title { font-family: 'Cormorant Garamond', serif; font-style: italic; font-weight: 300; font-size: clamp(46px, 16vw, 136px); line-height: 0.88; letter-spacing: -0.005em; color: #f0ece6; text-shadow: 0 2px 60px rgba(0,0,0,0.72), 0 1px 10px rgba(0,0,0,0.45); }

/* The incoming photograph during a swap: it opens pixel-identical to the peek and
   grows out of that slot. Every edge is set from JS so the move mirrors in reverse. */
.dc-wipe { position: absolute; right: 0; top: 0; bottom: 0; width: 0; overflow: hidden; display: none; z-index: 3; }
.dc-wipe img { position: absolute; top: 0; left: 0; width: 100vw; height: 100%; object-fit: cover; filter: grayscale(1) contrast(1.05); display: block; transform-origin: 50% 50%; }

/* Inset, because the stage clips its overflow and an outset ring on the peek would
   be cut off at the right edge. TWO TONE on purpose: a single gold ring over a
   high-key photograph can fall under 3:1 and this repo already knows Lighthouse
   cannot measure contrast over a picture, so the dark line is what guarantees the
   gold one is visible whatever is behind it. */
#decompose :focus-visible { outline: 2px solid #c8a96e; outline-offset: -4px; box-shadow: inset 0 0 0 6px rgba(14,13,12,0.9); }
#decompose .dc-index a:focus-visible, #decompose .index-grid a:focus-visible { box-shadow: none; }

/* The capsule list. It is the JS's ONLY source of capsule data, and with JS off it
   is the hero's only way into a capsule — so it is a real nav on the dark ground
   below the stage, at full touch size, and it disappears once the script runs. */
.dc-index { display: flex; flex-wrap: wrap; gap: 0 28px; padding: 20px 48px; border-top: 1px solid rgba(255,255,255,0.07); }
.dc-index a { display: inline-flex; align-items: center; min-height: 44px; font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; color: #d4cfc9; }
.dc-index a:hover { color: #c8a96e; }
#decompose.dc-live .dc-index { display: none; }

/* Collections grid, after the pin releases. */
.td-collections { background: #0e0d0c; color: #d4cfc9; padding: 72px 48px 36px; font-family: 'Geist Mono', monospace; }
#decompose .td-collections-head { background: transparent; display: flex; justify-content: space-between; align-items: baseline; gap: 16px; flex-wrap: wrap; border-bottom: 1px solid rgba(255,255,255,0.07); padding-bottom: 18px; margin-bottom: 2px; }
#decompose .td-collections-head h2 { font-family: 'Cormorant Garamond', serif; font-weight: 300; font-size: 32px; margin: 0; color: #f0ece6; }
#decompose .td-collections-head span { font-size: 10px; letter-spacing: 0.24em; text-transform: uppercase; color: #8f8980; }
#decompose .index-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 2px; margin: 0; padding: 0; }
#decompose .index-grid a.episode-card { padding: 0; position: relative; display: block; aspect-ratio: 3 / 2; overflow: hidden; background: #161412; margin: 0; }
#decompose .index-grid picture { display: block; aspect-ratio: auto; padding: 0; height: 100%; }
/* 🔴 0.86, not the handoff's 0.75. Every photograph in the grid was muddier than
   the hero above it, so the page visibly DEGRADED as you scrolled past the thing it
   is a catalog of. The tile captions sit on these images under their own gradient and
   are contrast targets now, which is what makes raising this safe to do at all. */
#decompose .index-grid img { width: 100%; height: 100%; object-fit: cover; filter: grayscale(1) brightness(0.86); display: block; }
/* The tile answers with a warm-gray glow and a lift in the print - not gold (that is
   the accent's job) and not a zoom, which crops the frame you are choosing. The card
   raises its own stacking context so a 2px-gap neighbor cannot clip the glow. */
#decompose .index-grid a.episode-card { transition: box-shadow 0.35s ease, filter 0.35s ease; z-index: 0; }
#decompose .index-grid a.episode-card:hover, #decompose .index-grid a.episode-card:focus-visible { box-shadow: 0 0 0 1px rgba(212,207,201,0.25), 0 0 20px rgba(212,207,201,0.1); filter: brightness(1.08); z-index: 1; }
#decompose .index-grid .episode-info { border-top: 0; margin: 0; position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: flex-end; gap: 4px; padding: 18px 20px; background: linear-gradient(to top, rgba(10,9,8,0.97) 0%, rgba(10,9,8,0.88) 32%, rgba(10,9,8,0.5) 62%, transparent 100%); pointer-events: none; }
#decompose .index-grid .ep-label { font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: #c8a96e; text-shadow: 0 1px 10px rgba(0,0,0,0.55); }
#decompose .index-grid .ep-title { text-transform: none; letter-spacing: 0; font-family: 'Cormorant Garamond', serif; font-weight: 300; font-size: 26px; line-height: 1.2; color: #f0ece6; text-shadow: 0 1px 14px rgba(0,0,0,0.45); margin: 0; }
#decompose .index-grid .ep-scent { font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: #8f8980; }
#decompose .td-collections-foot { background: transparent; display: flex; justify-content: space-between; align-items: baseline; border-top: 1px solid rgba(255,255,255,0.07); margin-top: 72px; padding: 28px 0 36px; font-size: 10px; letter-spacing: 0.24em; text-transform: uppercase; color: #8f8980; }
#decompose .td-collections-foot .td-name { font-weight: 400; color: #f0ece6; font-size: 15px; font-family: 'Cormorant Garamond', serif; letter-spacing: 0.04em; text-transform: none; }
#decompose .td-collections-foot a { color: #8f8980; padding: 8px 0; display: inline-block; min-height: 24px; }

body.home.catalog header { grid-template-columns: auto 1fr auto; align-items: center; min-height: 52px; padding: 0 36px; gap: 28px; background: var(--bg); border-bottom: 0; }
body.home.catalog .site-name { grid-column: 1; grid-row: 1; font: 400 10px/1 var(--mono); letter-spacing: .24em; text-transform: uppercase; color: var(--text); text-shadow: none; transform: none; text-align: left; }
body.home.catalog header .catalog-index-link, body.home.catalog header .lens-nav a { color: var(--text); }
body.home.catalog .catalog-index-link { grid-column: 2; grid-row: 1; justify-self: start; font: 10px var(--mono); letter-spacing: .24em; }
body.home.catalog .lens-nav { grid-column: 3; grid-row: 1; justify-self: end; }
body.home.catalog .lens-nav a { font: 10px var(--mono); letter-spacing: .24em; }
/* 🔴 THE HEADER FLOATS WHEREVER THE PHOTOGRAPH IS FULL-BLEED. A paper band across
   the top of a full-height black-and-white photograph is the brightest rectangle on
   the page and the only thing on it that is not a picture: it gives the first
   fixation to three navigation labels and puts a hard edge where the frame should
   reach the top of the screen. An art-direction review named it, unprompted, as the
   single largest cost to the page — and the fix is the one this site already solved
   once, on the old home hero.

   The media query is an OR, and it is exactly the set of layouts where a photograph
   is behind the header: wider than 700 is the desktop letterbox, and shorter than
   560 is the landscape overlay. The stacked phone layout is the one case where the
   header sits on black above a 4:5 print, and there it stays on paper.

   The ramp is LIFTED VERBATIM from styles.css's own home header, which was measured
   against a no-scrim control render and re-tuned twice by the operator; its comment
   there records three other shapes that were built and rejected. It is not a new
   value and it should not be re-derived here.

   align-items is BASELINE. `center` is wrong because the box is now ~90px tall and
   centering the ink in it drops the row to y=38 with no scrim above it; `start` is
   wrong because the nav links carry 8px of vertical padding and the wordmark does
   not, so top-aligning them puts their ink on two different lines - the row sweep
   caught that at all twelve widths, .site-name at 24.5 against 42.0.

   🔴 The BOTTOM PADDING is the ramp's length, because the gradient's stops are
   PERCENTAGES OF THE BOX - which is the trap its own comment in styles.css names. At
   56px the box was ~105px, the same geometry that was tuned there, and it still read
   as a BAND with a visible edge: that tuning was done over the old home hero, and this
   one opens on a flat blown sky running the full width, which is the worst case there
   is for a terminal slope. 92px puts the box near 141px, which stretches the same
   curve over 34% more distance and drops the terminal slope by the same factor. The
   type does not move - it sits at 13-31% of the box.

   🔴 And the STOPS were re-shaped, not only stretched. The lifted ramp holds 0.96
   until 45% and then falls 0.90 -> 0.23 across seventeen percent of the box: measured
   on this page that is a 0.029 luminance step per pixel, and over a blown sky running
   the FULL WIDTH - which the old home hero did not have - the composite tracks alpha
   exactly, so the steep middle reads as a hard horizontal edge even though the
   TERMINAL slope it was tuned for is fine. The Mach band moved; it did not go away.
   This curve peaks at 0.90, falls monotonically with no plateau, and its steepest
   step is ~0.006 - the density the type needs is 0.80, not 0.96, and everything above
   that was paying for an edge. */
@media (min-width: 701px), (max-height: 560px) {
  #decompose { --hdr: 0px; }
  body.home.catalog header {
    position: absolute; top: 0; left: 0; right: 0; z-index: 6;
    min-height: 0; align-items: baseline; padding: 20px 36px 92px;
    background: linear-gradient(to bottom,
                rgba(10,9,8,0.90) 0%, rgba(10,9,8,0.84) 28%,
                rgba(10,9,8,0.70) 46%, rgba(10,9,8,0.52) 60%,
                rgba(10,9,8,0.36) 70%, rgba(10,9,8,0.23) 78%,
                rgba(10,9,8,0.14) 85%, rgba(10,9,8,0.07) 90.5%,
                rgba(10,9,8,0.03) 95%, rgba(10,9,8,0.008) 98%,
                transparent 100%);
  }
  body.home.catalog .site-name,
  body.home.catalog header .catalog-index-link,
  body.home.catalog header .lens-nav a { color: #f0ece6; }
}

@media (max-width: 700px) { body.home.catalog header { min-height: 48px; padding: 0 20px; gap: 16px; } body.home.catalog .site-name { font-size: 10px; grid-column: 1; grid-row: 1; text-align: left; text-shadow: none; } body.home.catalog .catalog-index-link { grid-row: 1; grid-column: 2; } body.home.catalog .lens-nav { grid-row: 1; grid-column: 3; } }

/* Collections grid: the catalog's editorial rules for the elemental homepage
   (display sizes, italics, a 2:1 last card, a 10ch title) do not apply here. */
#decompose .td-collections-head { padding: 0 0 18px; align-items: baseline; color: #f0ece6; }
#decompose .td-collections-head h2 { text-transform: none; letter-spacing: 0; max-width: none; }
#decompose .index-grid { background: transparent; padding: 0; gap: 2px; color: #d4cfc9; }
#decompose .index-grid a.episode-card, #decompose .index-grid a.episode-card:nth-of-type(2), #decompose .index-grid a.episode-card:last-of-type:nth-of-type(odd) { grid-column: auto; margin: 0; padding: 0; }
#decompose .index-grid a.episode-card:last-of-type:nth-of-type(odd) picture { aspect-ratio: auto; }
#decompose .index-grid .ep-title, #decompose .index-grid .episode-card:nth-of-type(2) .ep-title, #decompose .index-grid .episode-card:nth-of-type(3) .ep-title, #decompose .index-grid .episode-card[href='/blue-moon-rising'] .ep-title, #decompose .index-grid .episode-card:is([href='/passages'], [href='/structure'], [href='/objects']) .ep-title { font: 300 26px/1.2 'Cormorant Garamond', serif; font-style: normal; text-transform: none; letter-spacing: 0; max-width: none; color: #f0ece6; }

/* The phone's advance control. Hidden wherever the sliver is doing the job. */
.dc-next { display: none; width: 100%; justify-content: space-between; align-items: baseline;
           padding: 16px var(--capx) 26px; border: 0; border-top: 1px solid rgba(255,255,255,0.07);
           background: none; color: #8f8980; font: 10px 'Geist Mono', monospace;
           letter-spacing: 0.24em; text-transform: uppercase; cursor: pointer; min-height: 44px; }
.dc-next-name { color: #c8a96e; }
.dc-next-name::after { content: ' \2192'; }   /* decoration: in CSS so it is never announced */

/* The second frame layer, for the phone's crossfade. */
.dc-frame-b { position: absolute; inset: 0; opacity: 0; pointer-events: none; }
.dc-frame-b img { width: 100%; height: 100%; object-fit: cover; display: block;
                  filter: grayscale(1) contrast(1.02) brightness(1.03); }

/* ─────────────────────────────────────────────────────────────────────────────
   THE PHONE HERO. A 3:2 photograph forced into a full-height 0.37:1 box showed
   24.6% of five of the seven frames — measured, 2026-09-18 — so on a phone the
   print gets a 4:5 crop at full width (53.3%) and the name moves BELOW it onto
   the dark ground. The caption vignette disappears with it: type on black needs
   no scrim, so nothing is dug out of the photograph to make room for a word.
   Desktop is untouched; it was already showing 84–98%.
   ───────────────────────────────────────────────────────────────────────────── */
@media (max-width: 700px) {
  /* 🔴 ONE block per query. There were two, and the later one silently won —
     which is how a base rule shadowed its own media query twice in this file.
     --peek is NOT declared here any more: .dc-peek is display:none below, so a
     96px sliver width was a dead number that FRAME_SIZES still believed. */
  /* --peek is 0 here and says so. The sliver stands down on a phone, so the one
     honest value is zero - and the short-screen block below turns the vignette and
     the caption scrim back on, both of which inset themselves by --peek. Leaving it
     at the desktop's 148 would have carved a sliver-shaped hole out of a photograph
     that fills the whole stage. */
  #decompose { --hdr: 48px; --capx: 20px; --capy: 24px; --peek: 0px; }
  .dc-index { padding: 16px 20px; gap: 0 20px; }
  /* 🔴 the print is FLUSH under the header. Centring the stack put 76.4px of dead
     black above the photograph (measured at 390x844), so the page opened on a
     letterbox bar and the print read as a card floating in a field rather than as
     the thing you came for. The slack belongs BELOW, where the name lives. */
  /* 🔴 minmax(0,1fr), not 1fr. A `1fr` row's min is AUTO, so the print's own
     1.25x100vw height became the row's floor and the frame simply overflowed a
     short stage: at 667x375 it measured 834px tall inside a 327px stage and the
     visitor saw 24.5% of the photograph - the exact number this layout exists to
     eliminate - with the caption sitting on the picture and its scrim switched
     off. The print is height-driven and derives its width, so it can always
     shrink to the room available. */
  /* 🔴 the SLACK row is the last one, not the first. With `minmax(0,1fr) auto auto`
     every spare pixel piled up UNDER the photograph and above its name - 155px of it
     at 390x844 - so the print and the word that names it read as two unrelated things
     with a gap between them. The name belongs against the picture; the quiet belongs
     at the bottom of the screen, above the one control. */
  .dc-stage { display: grid; grid-template-rows: auto auto 1fr; }
  .dc-next { align-self: end; }
  .dc-caption { align-self: start; }
  /* min-height 56% is the print's FLOOR SHARE of the stage, and it binds on a narrow
     tall screen: 4:5 at 320 wide is 400px in a 796px stage - 50.3% - because the crop
     is derived from the WIDTH while the name and the next-row cost a fixed height. The
     shape yields to the share, never the other way round. At 390 and up the 4:5 print
     is already taller than the floor and nothing changes.
     It is written in dvh, not in %, because a PERCENTAGE min-height inside a
     minmax(0,1fr) grid row resolves against an indefinite track and silently becomes
     zero - written as 56% it changed nothing at all and the gate still read 50.3%.
     56dvh - 27px is the same quantity against the stage's own definition.
     max-height is in dvh for the same reason, and it is what stops a 4:5 print from
     overflowing a wide-but-short stage now that row 1 sizes to its content: at 700x844
     the crop would be 875px tall in a 796px stage. 230px is the room the name and the
     next-row need, measured rather than guessed: the title is clamp(40px, 13vw, 110px),
     so at 700 wide the caption is 133px and the next-row 56px - 189 - and a 170px
     reserve squeezed row 1 below the print's own height and put the name ON the
     photograph with the scrim switched off. The gate caught it at 700x844. */
  .dc-frame { position: relative; right: auto; width: 100vw; height: auto; max-height: calc(100dvh - var(--hdr) - 230px); min-height: calc(56dvh - 27px); aspect-ratio: 4 / 5; margin-inline: auto; align-self: start; }
  .dc-frame img { height: 100%; }
  /* the sliver is the desktop's affordance; .dc-next is the phone's */
  .dc-peek { display: none; }
  .dc-stage::after { display: none; }
  /* the grow-out never runs here, and a stage-wide wipe left over from a
     desktop-to-phone resize mid-move would wash across the caption and next-row */
  .dc-wipe { display: none !important; }
  .dc-caption { position: relative; left: auto; bottom: auto; padding: 20px var(--capx) 10px; gap: 10px; }
  .dc-caption::before { display: none; }          /* on black: no scrim, no damage */
  .dc-title { text-shadow: none; font-size: clamp(40px, 13vw, 110px); }
  .dc-num { text-shadow: none; }
  .dc-next { display: flex; }
}

/* 🔴 A SHORT screen wants a different shape. 4:5 is right on a tall phone; on a
   landscape phone the row is ~142px and a 4:5 box becomes 667x142 — a 4.7:1 slot
   that crops a 3:2 photograph down to 27.3% (measured 667x375). Height-driven at
   the photographs' own 3:2 shows the whole frame instead, smaller but entire,
   which is the trade this whole layout was chosen for. */
@media (max-width: 700px) and (max-height: 560px) {
  /* 🔴 A landscape phone does not get the stacked layout at all. Height-driven 3:2
     inside a stacked stage measured 213x143 CSS px on a 667x375 screen - 12% of the
     glass - because the name and the next-row had already eaten the height, and the
     gate could not see it: `visible` measures how much of the PHOTOGRAPH is uncropped,
     never how much of the SCREEN the photograph is, so a pristine postage stamp scores
     100%. This screen's shape is the desktop's shape, so it gets the desktop's
     composition: one full-bleed frame with the name lying on it. */
  /* the 700px block below the float re-asserts a 48px paper-height header and a
     zero bottom padding, so the scrim's box would be 48px with the ink jammed at
     its top. Restated here, where it wins. */
  /* 🔴 --hdr TOO, not just the padding. The float block sets --hdr:0 because the
     header no longer occupies flow; the 700px block below it re-asserts 48px, and at
     ≤700 wide AND ≤560 tall BOTH match, so the stage was subtracting 48px for a header
     that is not in flow. Measured 667x375: the print filled 100% of the stage and
     87.2% of the GLASS, with a black band and the collections hairline showing under
     it - and the new on-glass floor is denominated in the STAGE, so it is structurally
     blind to exactly this. Found by a craft review, not by a gate. */
  #decompose { --hdr: 0px; }
  body.home.catalog header { min-height: 0; padding: 14px 20px 40px; }
  /* 🔴 `.dc-stage { display: block }` USED TO BE HERE and has been deleted, not
     waived. Mutating it to `grid` SURVIVED an 11-mutant run against the full 387-check
     gate, and the reason is that it is inert: in this layout the frame, the caption and
     the next-row are all position:absolute, the sliver and the wipe are display:none,
     and the only other child is a visually-hidden status paragraph - the stage has no
     in-flow children at all, so its display mode cannot move anything. A declaration no
     check can see is not defense; it is a line that will one day be read as one. What
     is load-bearing here is that each of those three is OUT OF FLOW, and the gate
     asserts that directly: the name lies on the print, the print fills the stage, and
     the stage covers the glass. */
  /* height:100%, never auto. With inset:0 and an auto height the frame measured
     667x520 inside a 327px stage - the picture/img pair are both height:100%, so an
     auto height is circular and falls back to the image's own intrinsic box. An
     explicit percentage of a stage with a definite height is not circular. */
  .dc-frame { position: absolute; inset: 0; width: auto; height: 100%;
              max-height: none; aspect-ratio: auto; margin-inline: 0; }
  .dc-stage::after { display: block; }
  .dc-caption { position: absolute; left: var(--capx); bottom: calc(var(--capy) + 4px);
                padding: 0; gap: 8px; z-index: 4; }
  .dc-caption::before { display: block; }   /* the ink is back on the picture */
  .dc-title { font-size: clamp(34px, 9vh, 64px);
              text-shadow: 0 2px 60px rgba(0,0,0,0.72), 0 1px 10px rgba(0,0,0,0.45); }
  .dc-num { text-shadow: 0 1px 8px rgba(0,0,0,0.6); }
  /* the next-row lands ON the photograph here, so it gets what the caption gets:
     a corner vignette and a shadow. Without them it measured as ink over a blown
     sky in the bottom-right of the Earth frame - visible in the render, invisible
     to the contrast gate, which only ever sampled 390 and 1440. */
  /* align-items:flex-end, because the 44px touch floor is not reset here and a
     baseline-aligned row hangs 31px of empty box below its own ink - the name and the
     next-row were given the same `bottom` and their ink still sat 31px apart, with the
     vignette anchored under the empty half. The box keeps its 44px; the ink sits at
     the bottom of it, where the caption's does. */
  .dc-next { position: absolute; right: var(--capx); bottom: var(--capy);
             width: auto; gap: 12px; padding: 0; border: 0; z-index: 5;
             align-items: flex-end; text-shadow: 0 1px 8px rgba(0,0,0,0.6); }
  .dc-next::before { content: ""; position: absolute; z-index: -1; pointer-events: none;
             right: calc(-1 * var(--capx)); bottom: calc(-1 * var(--capy));
             width: calc(100% + var(--capx) + 260px); height: calc(100% + var(--capy) + 130px);
             background: radial-gradient(farthest-side at calc(100% - var(--capx)) 100%,
                         rgba(14,13,12,0.96) 0%, rgba(14,13,12,0.90) 62%, rgba(14,13,12,0) 100%); }
  /* the word NEXT is the LEFTMOST thing in this row and so the farthest from the
     vignette's anchor - it measured 3.91-4.36 against a 4.5 floor on all six
     photographs while the gold name beside it passed. Muted gray is the right
     hierarchy on black; on a photograph it is just dim. */
  .dc-next-say { color: #d4cfc9; }
}

/* THE ARRIVAL. The first two seconds are the whole impression, and the page used
   to simply appear. The photograph rises out of the dark, the peek opens after it,
   and the name settles in last — picture, then the promise of more, then the name.

   🔴 fill-mode is BACKWARDS, never `both` or `forwards`. An animated property beats
   an inline style for as long as the animation applies, so a forwards fill would
   hold transform and filter on the frame's img FOREVER and decompose.js could never
   move them again — the grow-out would run with a dead outgoing photograph. Backwards
   holds the from-state through the delay (which is what the staggering needs) and
   hands every property back the instant the animation ends. */
#decompose .dc-frame img { animation: dc-rise 1900ms cubic-bezier(0.16, 1, 0.3, 1) backwards; }
#decompose .dc-peek { animation: dc-open 1400ms cubic-bezier(0.16, 1, 0.3, 1) 380ms backwards; }
#decompose .dc-caption { animation: dc-settle 1100ms cubic-bezier(0.16, 1, 0.3, 1) 620ms backwards; }
/* 🔴 the next-row arrives LAST, and it has to arrive at all. With no entrance it
   sat at full opacity through the whole 1.9s arrival, so at t=0 the only legible
   words on a phone were "NEXT 02 · WATER" — the page's first sentence to a
   visitor was leave. Measured 2026-09-18: caption opacity 0, next-row opacity 1.
   The name lands before the way out does. */
#decompose .dc-next { animation: dc-settle 900ms cubic-bezier(0.16, 1, 0.3, 1) 1000ms backwards; }
@keyframes dc-rise { from { transform: scale(1.085); filter: grayscale(1) contrast(1.02) brightness(0.42); } }
@keyframes dc-open { from { opacity: 0; transform: translateX(34px); } }
@keyframes dc-settle { from { opacity: 0; transform: translateY(26px); } }

/* reduced motion: nothing arrives, and the frame still changes — with a crossfade
   instead of a growing edge (decompose.js) — and the peek stops easing. */
#decompose.dc-still .dc-peek img { transition: none; }
@media (prefers-reduced-motion: reduce) {
  #decompose .dc-frame img, #decompose .dc-peek, #decompose .dc-caption, #decompose .dc-next { animation: none; }
}

/* The handoff is desktop-only. Three 98px-wide tiles on a 390px screen is not a
   grid, so the collapse below is ours, not the design's. */
@media (max-width: 900px) { #decompose .index-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) { #decompose .index-grid { grid-template-columns: minmax(0, 1fr); } .td-collections { padding: 48px 20px 24px; } }

