/* descent.css — the arrival sequence.
   Tokens come from site.css; only what the sequence needs lives here. */

/* ── theme bindings ────────────────────────────────────────────────
   Content sections follow the site's own tokens, so they flip with the
   light/dark toggle exactly like every other page. */
:root {
  --dsc-plate:   var(--bg);
  --dsc-plate-2: var(--bg2);
  --dsc-ink:     var(--text);
  --dsc-ink-2:   var(--muted);
  /* --muted, not --muted2. --muted2 is #8c7a5c in light, which is 3.42:1 on
     --bg and 3.15:1 on --bg2 — under AA at every size this token is used at.
     An earlier pass had already swapped six individual --dsc-ink-3 uses to
     --dsc-ink-2 for exactly this reason; doing it once at the token finishes
     that job and, more to the point, catches the ones nobody thought to look
     at. Measured on the live homepage in light: .room-slab's captions at 3.42
     (10px) and .next-privacy at 3.76 (13px, and !important, so only a token
     remap reaches it at all) — 18 elements the per-rule approach had missed.
     Safe in dark too: there --muted is #a9adb5 against --muted2's #989ea9, so
     this raises contrast rather than lowering it, and the dark cinematic
     sections below keep their own three ink tiers regardless, because that
     block is class-scoped rather than theme-scoped.
     The three border uses of this token shift with it, from 3.42:1 to 5.47:1
     against --bg — still decorative, and further clear of the 3:1 that WCAG
     1.4.11 asks of a non-text boundary. */
  --dsc-ink-3:   var(--muted);
  --dsc-ink-4:   var(--line-bright);
  --dsc-edge:    var(--line);
  --dsc-edge-2:  var(--line-bright);
  --dsc-card:    var(--panel);
}

/* index.html paints its body beige; this page never did, so overscroll and any
   gap between sections fell through to nothing. Painting it means the ground
   behind the page is the theme's own, in both themes. */
body { background: var(--bg); color: var(--text); }

/* Sections laid OVER photography keep the fixed light-on-dark set. These are
   dark pictures in either theme, so following the toggle here would put dark
   text on a dark photograph in light mode.

   .dsc-hdr belongs here and was missing. It is a direct child of body rather
   than a descendant of .descent, so it inherited the theme-following tokens
   from :root while sitting on the very photograph this rule exists for. In
   light mode that resolved --dsc-ink to #2a2013 and --dsc-ink-2 to #6b5a3e --
   dark brown nav links on a dark cloud photo, measured at 1.21-1.61 against
   the real pixels. The site's own contrast sweep scored that page clean,
   because it resolves a backdrop by walking up for a background-color and a
   photograph has none. */
.descent, .about, .plan, .dsc-hdr {
  /* Exactly the site's own dark values — #f4f4f5 text on #212327, the same
     pair index.html uses. These were #f6f6f7, two points off, which is
     invisible alone but reads as a slightly different white when the two
     pages sit side by side. */
  --dsc-plate:   #212327;
  --dsc-plate-2: #1b1d20;
  --dsc-ink:     #f4f4f5;
  --dsc-ink-2:   rgba(244, 244, 245, .72);
  --dsc-ink-3:   rgba(244, 244, 245, .5);
  --dsc-ink-4:   rgba(244, 244, 245, .22);
  --dsc-edge:    rgba(255, 255, 255, .1);
  --dsc-edge-2:  rgba(255, 255, 255, .22);
  --dsc-card:    rgba(255, 255, 255, .022);
}

/* The nav sits across the brightest band of the hero — the horizon glow, which
   samples around rgb(89,102,112) behind "Notes". At --dsc-ink-2's .72 the links
   measure 3.66–4.26 against those real pixels, under AA, even after the pin
   above. .9 clears it at 4.68 and is indistinguishable at 12px on a photograph.
   Only the header is raised: everywhere else --dsc-ink-2 sits on flat plate,
   where .72 already passes comfortably and is doing deliberate visual work. */
.dsc-hdr { --dsc-ink-2: rgba(244, 244, 245, .9); }

.descent {
  position: relative;
  /* Scroll distance IS the playback speed: the frame index is scroll progress
     across this height, so a taller container means fewer frames per pixel
     scrolled. At 520vh the nine frames changed roughly every half screen,
     which read as hurried. 1200vh gives each of the eight transitions about a
     screen and a third, and descent.js holds each frame for the first and last
     third of its interval so it rests rather than dissolving throughout. */
  height: 1200vh;
  background: var(--dsc-plate);
}

.dsc-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;           /* mobile chrome shrinks the viewport mid-scroll */
  width: 100%;
  overflow: hidden;
  background: var(--dsc-plate);
}

.dsc-canvas { display: block; width: 100%; height: 100%; }

/* A single soft scrim so type stays legible over any frame the scroll lands on.
   Weighted to the bottom, where the identity block sits. */
.dsc-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Two gradients, not one. The bottom wash alone is nearly transparent between
     42% and 70% height, which is exactly where the mid-page beats sit, so white
     copy over the bright interior frames measured 4.63:1 against real canvas
     pixels -- passing, but with no reserve, and invisible to any CSS-based audit
     because the ground is a <canvas> the harness cannot composite.
     The left wash covers the column the copy actually occupies and fades out
     before the middle, so the frames keep their depth on the right. */
  /* The right end no longer reaches transparent. The identity line is 11px
     uppercase on .24em tracking, so it runs far wider than the body copy this
     wash was drawn for: at 1440 its last two words sit past 74%, where the
     wash had already gone, on sky sampling rgb(124,116,101). Measured against
     real pixels, "Est." scored 3.01 and "2026" 3.74 while the same line's
     first words scored 4.48 — a clean left-to-right gradient of failure that
     matches this stop list exactly.
     Holding .10 to the edge instead of vanishing costs the right side very
     little depth, which is what the note below is protecting, and is the same
     correction the phone block further down already makes for the same
     reason. */
  background:
    linear-gradient(to right,
      rgba(12, 13, 15, .56) 0%,
      rgba(12, 13, 15, .42) 28%,
      rgba(12, 13, 15, .24) 46%,
      rgba(12, 13, 15, .16) 62%,
      rgba(12, 13, 15, .10) 100%),
    linear-gradient(to top,
      rgba(12, 13, 15, .74) 0%,
      rgba(12, 13, 15, .30) 42%,
      rgba(12, 13, 15, .06) 70%,
      transparent 100%);
}

.dsc-stage { position: absolute; inset: 0; pointer-events: none; }

/* Every beat starts hidden and is driven only by scroll position in JS.
   Nothing here waits on an image. */
.dsc-beat {
  position: absolute;
  opacity: 0;
  visibility: hidden;
  will-change: opacity, transform;
}
.dsc-beat a, .dsc-beat button { pointer-events: auto; }

/* ── beat 1 · identity ─────────────────────────────────────────── */
.dsc-id {
  left: clamp(1.25rem, 5vw, 5.5rem);
  right: clamp(1.25rem, 5vw, 5.5rem);
  bottom: clamp(3rem, 9vh, 7rem);
  max-width: 46rem;
}

.dsc-label {
  margin: 0 0 1.35rem;
  font: 500 .68rem/1 var(--font-interface, system-ui, sans-serif);
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--dsc-ink-2);
}

.dsc-h1 {
  margin: 0 0 1.15rem;
  font: 500 clamp(2.6rem, 7vw, 6rem)/1.02 var(--font-display, Georgia, serif);
  letter-spacing: -.018em;
  color: var(--dsc-ink);
  text-wrap: balance;
}

.dsc-p {
  margin: 0 0 2.1rem;
  max-width: 34rem;
  font: 300 clamp(1rem, 1.35vw, 1.16rem)/1.62 var(--font-interface, system-ui, sans-serif);
  color: var(--dsc-ink-2);
  text-wrap: pretty;
}

/* The hero carries two paragraphs now: how the apps are made, then why they
   exist. They should read as one thought in two beats, so the gap between them
   is tighter than the 2.1rem .dsc-p leaves before the button.

   Deliberately NO colour change between them: .dsc-p is already --dsc-ink-2,
   the muted ink, and stepping the second one down again would put 300-weight
   body text under the contrast floor. The separation is rhythm, not value.

   The animation delays live further down, immediately after the stagger block
   that sets them — at equal specificity the later rule wins, so overriding
   from up here would be a dead rule. */
.dsc-id .dsc-p:not(.dsc-p-why) { margin-bottom: .95rem; }

.dsc-cta {
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  min-height: 46px;
  padding: 0 1.7rem;
  /* Border and fill must stay distinct — the bulk substitution collapsed the
     .26 border and the .06 fill onto the same token, which flattened the
     button into a solid slab. */
  border: 1px solid var(--dsc-ink-3);
  border-radius: 999px;
  background: color-mix(in srgb, var(--dsc-ink) 8%, transparent);
  backdrop-filter: blur(10px);
  color: var(--dsc-ink);
  text-decoration: none;
  font: 500 .74rem/1 var(--font-interface, system-ui, sans-serif);
  letter-spacing: .13em;
  text-transform: uppercase;
  transition: background .22s cubic-bezier(.23, 1, .32, 1),
              border-color .22s cubic-bezier(.23, 1, .32, 1),
              transform .16s cubic-bezier(.23, 1, .32, 1);
}
.dsc-cta .dsc-arrow { transition: transform .22s cubic-bezier(.23, 1, .32, 1); }

@media (hover: hover) and (pointer: fine) {
  .dsc-cta:hover {
    background: color-mix(in srgb, var(--dsc-ink) 16%, transparent);
    border-color: var(--dsc-ink-2);
  }
  .dsc-cta:hover .dsc-arrow { transform: translateX(3px); }
}
.dsc-cta:active { transform: scale(.98); }
.dsc-cta:focus-visible { outline: 2px solid var(--dsc-ink); outline-offset: 3px; }

/* ── beats 2 and 3 · the two asides ────────────────────────────── */
.dsc-aside {
  top: 50%;
  translate: 0 -50%;
  max-width: min(26rem, 42vw);
}
.dsc-aside-r { right: clamp(1.25rem, 6vw, 7rem); text-align: right; }
.dsc-aside-l { left: clamp(1.25rem, 6vw, 7rem); }
/* Sits low, over the office frame's empty floor rather than across the desk
   and the figure. Not vertically centred like the other two — the point is to
   occupy the dead space at the bottom of that shot. */
.dsc-aside-b {
  top: auto;
  translate: none;
  left: clamp(1.25rem, 6vw, 7rem);
  right: clamp(1.25rem, 6vw, 7rem);
  bottom: clamp(3rem, 10vh, 7rem);
  max-width: min(34rem, 62vw);
}

.dsc-aside p {
  margin: 0;
  font: 300 clamp(1.05rem, 1.7vw, 1.5rem)/1.5 var(--font-display, Georgia, serif);
  color: var(--dsc-ink);
  text-wrap: pretty;
}
.dsc-aside .dsc-label { margin-bottom: .9rem; }

/* ── beat 4 · the close, which holds ───────────────────────────── */
.dsc-close {
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  width: min(46rem, 88vw);
  text-align: center;
}
.dsc-close .dsc-h1 { font-size: clamp(2.2rem, 5.6vw, 4.6rem); margin-bottom: 1.2rem; }
.dsc-close .dsc-label { margin-bottom: 1.1rem; }
.dsc-close-p {
  margin: 0 auto 1.9rem;
  max-width: 34rem;
  color: var(--dsc-ink-2);
  font: 300 clamp(.97rem, 1.25vw, 1.1rem)/1.65 var(--font-interface, system-ui, sans-serif);
  text-wrap: pretty;
}

/* Its own backdrop, because the close can land over any frame. */
.dsc-halo {
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  width: min(74rem, 130vw);
  height: min(46rem, 96vh);
  border-radius: 50%;
  background: radial-gradient(ellipse at center,
    rgba(12, 13, 15, .74) 0%, rgba(12, 13, 15, .40) 48%, transparent 76%);
}

.dsc-play { display: inline-flex; border-radius: 10px; }
.dsc-play img { display: block; height: 52px; width: auto; }
.dsc-play:focus-visible { outline: 2px solid var(--dsc-ink); outline-offset: 4px; }

/* ── mount entrance ────────────────────────────────────────────────
   Pure CSS, staggered in reading order, and deliberately not tied to the
   image load. The h1 travels furthest and slowest because it is the anchor. */
@keyframes dsc-rise { from { opacity: 0; transform: translate3d(0, var(--rise, 28px), 0); }
                        to { opacity: 1; transform: none; } }

.dsc-id .dsc-label,
.dsc-id .dsc-h1,
.dsc-id .dsc-p,
.dsc-id .dsc-cta { animation: dsc-rise both cubic-bezier(.16, 1, .3, 1); }

.dsc-id .dsc-label { --rise: 28px; animation-duration: 1s;   animation-delay: .05s; }

/* The identity line is solid and shadowed at EVERY width, not just on the
   phone. It was scoped to max-width:700 when the phone crop put the horizon
   behind it; scoring 1440 against real pixels afterwards found the same line
   failing there too for a different reason — its last words run past the end
   of the left wash. One line with two ways to land on bright sky is a line
   that should not be carrying .72 alpha anywhere.
   The shadow is the device home.css:469 already uses for a small label over
   photography. It does little for the measured figure, since the audit takes
   the dominant colour across the whole box and .24em tracking leaves most of
   that box as gap, but it is what a reader resolves the glyph edges against. */
.dsc-id .dsc-label {
  color: #f4f4f5;
  text-shadow: 0 1px 6px rgba(12, 13, 15, .8);
}
.dsc-id .dsc-h1    { --rise: 56px; animation-duration: 1.3s; animation-delay: .2s; }
.dsc-id .dsc-p     { --rise: 36px; animation-duration: 1.1s; animation-delay: .4s; }
.dsc-id .dsc-cta   { --rise: 28px; animation-duration: 1s;   animation-delay: .55s; }

/* Must sit AFTER the four rules above, not with the other .dsc-p-why styling
   further up: `.dsc-id .dsc-p-why` and `.dsc-id .dsc-p` are both two classes,
   so the cascade is decided by order alone and an override placed earlier is
   simply dead. The second paragraph rises a beat after the first rather than
   with it, and the button steps back to stay last in the sequence. */
.dsc-id .dsc-p-why { animation-delay: .52s; }
.dsc-id .dsc-cta   { animation-delay: .66s; }

/* ── overlay header ────────────────────────────────────────────── */
.dsc-hdr {
  position: absolute;
  top: 0; left: 0; right: 0;
  /* Above .mm (z-index 11). The header creates a stacking context, so
     .nav-toggle's own z-index:12 was trapped inside it and the 97%-opaque
     menu sheet painted over the very button that closes it — leaving a phone
     with no way out but picking a nav link. */
  z-index: 12;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: clamp(1.1rem, 2.4vw, 1.9rem) clamp(1.25rem, 5vw, 5.5rem);
}
/* A scrim, rather than another alpha bump on the text.
   The hero is a photograph under camera motion, so the brightness behind any
   given nav link differs between loads: two successive samples behind "Notes"
   read rgb(89,102,112) and then rgb(97,109,120), which is the difference
   between 4.68 and 4.26 — between passing AA and failing it. Raising the text
   chases a value that moves; darkening the ground fixes it. Even solid #f4f4f5
   would fail here if the sky sampled a little brighter still.
   It also covers the wordmark and anything added to the nav later, which a
   per-element colour fix would not.
   Absolutely positioned so it does not become a third flex item in a header
   laid out with space-between, pointer-events:none so it cannot eat a click on
   the mark, and z-index:-1 to sit under the header's own content while staying
   inside its stacking context — above the photo, below the links. 140px is
   short of the headline at every breakpoint. */
.dsc-hdr::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 140px;
  background: linear-gradient(to bottom, rgba(0, 0, 0, .28), transparent);
  pointer-events: none;
  z-index: -1;
}
/* The original wordmark typography, restored: Cormorant Garamond at
   1.125rem, mixed case, tracking pulled slightly TIGHT at -.01em, with a 12px
   gap to the mark. The descent header had it as .82rem uppercase interface
   type on +.17em tracking — legible, but a different wordmark, and the studio
   name is the one thing that should not change shape between pages. */
.dsc-mark {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  color: var(--dsc-ink);
  text-decoration: none;
  font: 500 1.125rem/1.2 var(--font-display, Georgia, serif);
  letter-spacing: -.01em;
}
/* The mark keeps its brand gold rather than being recoloured to match the
   wordmark: the hero sequence itself shows this monogram in bronze on the
   building and the office wall, so a white version here would read as a
   different logo.
   The shadow is what makes that safe. Gold measures 2.88:1 against the sky
   frame behind the header — under the 3:1 WCAG asks of a meaningful graphic —
   and the backdrop keeps changing as the sequence plays, so no single colour
   choice is legible against all nine frames. A dark drop-shadow gives the
   silhouette its own edge regardless of what is behind it. */
.dsc-logo {
  width: auto;
  height: 29px;            /* the size the original header used */
  flex: none;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .55));
  /* The original transitioned only width and height here, so its own
     hover scale/rotate snapped instead of easing. Transform is included so the
     movement it was clearly written for actually plays. */
  transition: transform .22s ease-out, filter .22s ease-out;
}

/* The wordmark's 3D tilt, restored. perspective lives on the wrapper and
   preserve-3d on the lockup, so descent.js only has to write two rotations and
   the browser's perspective divide does the rest. */
.dsc-logo-3d { perspective: 600px; }
.dsc-mark { transform-style: preserve-3d; transition: transform .2s ease-out; }
.dsc-wtext { display: inline-block; transform-style: preserve-3d; transition: transform .2s ease-out; }

@media (hover: hover) and (pointer: fine) {
  .dsc-mark:hover .dsc-logo {
    transform: scale(1.08) rotate(-6deg);
    filter: drop-shadow(0 3px 8px rgba(0, 0, 0, .6));
  }
}

/* The tilt is decorative motion; honour a request to stop it. */
@media (prefers-reduced-motion: reduce) {
  .dsc-mark, .dsc-wtext, .dsc-logo { transition: none; }
  .dsc-mark:hover .dsc-logo { transform: none; }
}
@media (prefers-reduced-transparency: reduce) {
  .dsc-logo { filter: none; }
}
.dsc-nav { display: flex; gap: clamp(1rem, 2.4vw, 2.2rem); }
.dsc-nav a {
  color: var(--dsc-ink-2);
  text-decoration: none;
  font: 400 .78rem/1 var(--font-interface, system-ui, sans-serif);
  letter-spacing: .06em;
  transition: color .2s cubic-bezier(.23, 1, .32, 1);
}
@media (hover: hover) and (pointer: fine) { .dsc-nav a:hover { color: var(--dsc-ink); } }
.dsc-mark:focus-visible, .dsc-nav a:focus-visible { outline: 2px solid var(--dsc-ink); outline-offset: 4px; }
/* ── every word lifts ──────────────────────────────────────────────
   descent.js wraps each word in a .w span so it can answer to the cursor
   individually. inline-block is what makes transform apply at all — inline
   elements ignore it — and because the whitespace between words is left as
   plain text nodes, lines still break exactly where they did before.

   Kept small and quick: 3px over 160ms. A word should feel like it noticed
   the cursor, not like it jumped. */
/* One .wg wraps each split text node so a flex or grid parent still sees a
   single child — otherwise its `gap` lands between every word and the row
   stops wrapping.
   No min-width:0 here: as a flex item that let the wrapper shrink below its
   content, and "Choose what you want to share" broke onto six lines, one word
   each. The default min-width:auto floors it at the longest word instead, so
   the text wraps normally and still cannot force the row wider than the
   viewport. */
.wg { display: inline; }

.w {
  display: inline-block;
  transition: transform .16s cubic-bezier(.23, 1, .32, 1),
              color .16s cubic-bezier(.23, 1, .32, 1);
}
@media (hover: hover) and (pointer: fine) {
  .w:hover { transform: translateY(-3px); }
  /* Headings are already large; they move less so the line does not ripple. */
  h1 .w:hover, h2 .w:hover, .dsc-h1 .w:hover { transform: translateY(-2px); }
}

/* An address is one word, and inline-block cannot wrap OR shrink.
   "support@lyvorianstudio.co.in" is a single token, so the split gives it one
   .w of 187px. Below 800px .foot-in is repeat(2, minmax(0,1fr)), which at 375px
   is a 148px track — and because the span is inline-block it neither breaks
   across lines nor narrows to its track, so it ran 40px into the next column.
   Measured on the live site at 375: the address overlapped the Products
   column's "Medhakeli" link by 16px across and 12px down, and the last 17px of
   the mail link — 9% of its width — activated medhakeli.html instead of opening
   mail. Visible from 320px up to about 390px, so squarely on a phone. It was
   the only one of 1101 word spans on the page that overflowed anything, and
   only on index.html, the one page that runs the footer split.

   `anywhere` and not `break-word`: only `anywhere` lowers the span's min-content
   contribution, and that is exactly what lets an inline-block shrink to its
   track. `break-word` leaves the intrinsic width at 187px and the overflow
   stands. That same property is why this is scoped to mailto and not put on .w
   — a global `anywhere` would drop EVERY word's min-content to one character,
   and the .wg note above records what that costs: min-width:auto floors a flex
   row at its longest word, and without that floor "Choose what you want to
   share" once broke onto six lines of one word each.

   Nothing moves where the address already fits, because overflow-wrap only acts
   when the content genuinely does not — desktop is byte-identical. */
a[href^="mailto:"] .w { max-width: 100%; overflow-wrap: anywhere; }

/* ── hover lift ────────────────────────────────────────────────────
   Links, cards and buttons lift as a whole on top of the per-word motion, so
   a link still reads as one object rather than a row of loose words.

   translateY only, on the same easing as the rest of the page. Gated behind a
   fine pointer so touch devices, which have no hover, never get stuck in the
   lifted state after a tap. */
@media (hover: hover) and (pointer: fine) {
  .dsc-nav a,
  .mm-nav a,
  .foot nav a,
  .dsc-list a,
  .log-item a,
  .room-more a,
  .next-cats li,
  .rule-card,
  .craft-item,
  .dsc-mark,
  .foot .dsc-mark,
  a.inline {
    transition: transform .24s cubic-bezier(.23, 1, .32, 1),
                color .22s cubic-bezier(.23, 1, .32, 1),
                border-color .22s cubic-bezier(.23, 1, .32, 1);
  }

  .dsc-nav a:hover,
  .mm-nav a:hover,
  .foot nav a:hover,
  .dsc-list a:hover,
  .room-more a:hover,
  .dsc-mark:hover,
  a.inline:hover { transform: translateY(-2px); }

  /* Cards carry more weight, so they move a touch further and pick up an edge */
  .rule-card:hover,
  .next-cats li:hover {
    transform: translateY(-3px);
    border-color: var(--dsc-edge-2);
  }
  .craft-item:hover { transform: translateY(-3px); }

  /* Colour lifts with it, so the movement is not the only signal — that
     matters for anyone who cannot perceive the 2px shift. */
  .foot nav a:hover,
  .dsc-list a:hover,
  .mm-nav a:hover { color: var(--dsc-ink); }
}

@media (prefers-reduced-motion: reduce) {
  .w, .dsc-nav a, .mm-nav a, .foot nav a, .dsc-list a, .room-more a,
  .next-cats li, .rule-card, .craft-item, .dsc-mark, a.inline { transition: none; }
  .w:hover, h1 .w:hover, h2 .w:hover, .dsc-h1 .w:hover,
  .dsc-nav a:hover, .mm-nav a:hover, .foot nav a:hover, .dsc-list a:hover,
  .room-more a:hover, .next-cats li:hover, .rule-card:hover,
  .craft-item:hover, .dsc-mark:hover, a.inline:hover { transform: none; }
}

/* ── header tools ──────────────────────────────────────────────── */
.hdr-tools { display: flex; align-items: center; gap: .25rem; }
.dsc-theme {
  display: grid;
  place-items: center;
  width: 44px; height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--dsc-ink-2);
  cursor: pointer;
  transition: color .2s cubic-bezier(.23, 1, .32, 1);
}
@media (hover: hover) and (pointer: fine) { .dsc-theme:hover { color: var(--dsc-ink); } }
.dsc-theme:focus-visible { outline: 2px solid var(--dsc-ink); outline-offset: 2px; border-radius: 8px; }

/* ── scroll progress ───────────────────────────────────────────── */
.dsc-progress {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 2px;
  z-index: 20;
  pointer-events: none;
  background: transparent;
}
/* scaleX, not width.
   The bar is written on every scroll frame, and width is a layout property —
   each write dirtied layout, and the transition animated it again between
   writes. A transform runs on the compositor and touches neither layout nor
   paint. transform-origin:left keeps it growing from the left edge the way a
   width did. */
.dsc-progress i {
  display: block;
  height: 100%;
  width: 100%;
  transform: scaleX(0);
  transform-origin: left center;
  background: var(--gold, #c69b4a);
  transition: transform .1s linear;
}

/* ── back to top ───────────────────────────────────────────────────
   Earns its place here more than on the original: this page is ~13,000px. */
.to-top {
  position: fixed;
  right: clamp(1rem, 3vw, 2rem);
  bottom: clamp(1rem, 3vw, 2rem);
  z-index: 15;
  width: 46px; height: 46px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--text);
  font-size: 1.05rem;
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  transition: opacity .25s cubic-bezier(.23, 1, .32, 1),
              transform .25s cubic-bezier(.23, 1, .32, 1),
              visibility .25s;
}
.to-top.on { opacity: 1; visibility: visible; transform: none; }
.to-top:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }

/* ── lightbox ──────────────────────────────────────────────────── */
.lb {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 1rem;
  padding: clamp(1rem, 4vw, 3rem);
  background: rgba(10, 11, 13, .93);
  backdrop-filter: blur(8px);
}
.lb[hidden] { display: none; }
.lb-img {
  grid-column: 2;
  justify-self: center;
  max-width: min(92vw, 420px);
  max-height: 84vh;
  width: auto; height: auto;
  border-radius: 16px;
  border: 1px solid rgba(255, 255, 255, .16);
}
.lb-cap {
  grid-column: 2;
  grid-row: 2;
  margin: 0;
  text-align: center;
  color: rgba(244, 244, 245, .7);
  font: 500 .68rem/1 var(--font-interface, system-ui, sans-serif);
  letter-spacing: .2em;
  text-transform: uppercase;
}
.lb-x, .lb-nav {
  width: 46px; height: 46px;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .2);
  background: rgba(255, 255, 255, .06);
  color: #f4f4f5;
  font-size: 1.3rem;
  cursor: pointer;
}
.lb-x { position: absolute; top: clamp(1rem, 3vw, 2rem); right: clamp(1rem, 3vw, 2rem); }
.lb-prev { grid-column: 1; }
.lb-next { grid-column: 3; }
.lb-x:focus-visible, .lb-nav:focus-visible { outline: 2px solid #f4f4f5; outline-offset: 3px; }
@media (max-width: 560px) {
  .lb { grid-template-columns: 1fr; }
  /* The grid drops to one column here, but .lb-img and .lb-cap still asked for
     column 2, which created an implicit second track and pushed the screenshot
     off-centre past the right padding. */
  .lb-img, .lb-cap { grid-column: 1; }
  .lb-prev { grid-column: 1; justify-self: start; }
  .lb-next { grid-column: 1; justify-self: end; }
  .lb-prev, .lb-next { position: absolute; bottom: 1.5rem; }
}

/* ── mobile menu ───────────────────────────────────────────────────
   The toggle only exists below 560px, where the link row is hidden. Without
   it the nav renders but is unreachable — six links, none tappable. */
.nav-toggle {
  display: none;
  position: relative;
  width: 44px; height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  z-index: 12;
}
.nav-toggle span,
.nav-toggle::before,
.nav-toggle::after {
  content: '';
  position: absolute;
  left: 11px;
  width: 22px; height: 1.5px;
  background: var(--dsc-ink);
  transition: transform .26s cubic-bezier(.23, 1, .32, 1), opacity .18s linear;
}
.nav-toggle::before { top: 15px; }
.nav-toggle span    { top: 21px; }
/* translate:none because site.css:291 gives .nav-toggle::after a
   translate:-50% -50% under (pointer:coarse) to enlarge the tap target. That
   resolves against THIS 22x1.5px bar, not the button, shifting it 11px left —
   a hamburger with one stroke sticking out, and an X that does not cross. */
.nav-toggle::after  { top: 27px; translate: none; }
.nav-toggle[aria-expanded="true"]::before { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span    { opacity: 0; }
.nav-toggle[aria-expanded="true"]::after  { transform: translateY(-6px) rotate(-45deg); }

.mm {
  position: fixed;
  inset: 0;
  z-index: 11;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1.6rem;
  padding: 5rem clamp(1.5rem, 8vw, 3rem) 3rem;
  background: rgba(20, 21, 24, .97);
  backdrop-filter: blur(14px);
}
.mm[hidden] { display: none; }
.mm-nav { display: flex; flex-direction: column; gap: .2rem; }
.mm-nav a {
  padding: .72rem 0;
  border-bottom: 1px solid var(--dsc-edge);
  color: var(--dsc-ink);
  text-decoration: none;
  font: 400 1.35rem/1.3 var(--font-display, Georgia, serif);
}
.mm-mail {
  color: var(--dsc-ink-2);
  text-decoration: none;
  font: 300 .9rem/1.5 var(--font-interface, system-ui, sans-serif);
}

@media (max-width: 560px) {
  .dsc-nav { display: none; }
  .nav-toggle { display: block; }
}
@media (min-width: 561px) { .mm { display: none !important; } }

/* ── about, on the wall ────────────────────────────────────────────
   The photograph carries the studio name in bronze on its left half, so the
   copy is pinned to the right half and the two never collide. Below 900px the
   wall drops to a backdrop and the copy takes the full width. */
.about {
  position: relative;
  isolation: isolate;
  background: var(--dsc-plate);
  color: var(--dsc-ink);
  overflow: hidden;
}
.about-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* 91%, not 50%. object-position does not set where the window is CENTRED —
     it sets what fraction of the surplus width is trimmed off the LEFT, so the
     visible window is [(1-v)X, (1-v)X + v] for a visible fraction v.
     The sign band runs 29%-97%, and solving that for every width from 1120px
     up gives X >= 91%, tightest exactly at 1120px where the only value that
     works is 91% and the window lands on 29%-97% precisely. Wider screens have
     less surplus to trim, so the same value keeps the sign whole and simply
     shows more wall to its left.
     At 50% the window was 15%-91% at 1280px: the end of the wordmark cut. */
  object-position: 91% 50%;
  z-index: -2;
}
/* Measured, not guessed: the bronze lettering is a band spanning 29%-97% of
   the plate's width at 69%-74% of its height. It is far too wide to sit beside
   the copy, so the separation is vertical — copy in the upper half, sign left
   alone in the lower third. The gradient is weighted to match: heavy at the
   top where the words are, lifting over the band so the metal keeps contrast. */
.about::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  /* Weighted right, not down. The copy sits top-right and the mounted mark
     top-left, so a top-heavy scrim dimmed the mark two to three times harder
     than the wordmark lower down and broke the match between them. Darkening
     across instead keeps the copy legible and leaves the left wall lit. */
  background:
    linear-gradient(to bottom,
      rgba(18, 19, 22, .42) 0%,
      rgba(18, 19, 22, .30) 46%,
      rgba(18, 19, 22, .10) 68%,
      rgba(18, 19, 22, .26) 100%),
    linear-gradient(to right,
      rgba(18, 19, 22, 0) 0%,
      rgba(18, 19, 22, 0) 34%,
      rgba(18, 19, 22, .52) 68%,
      rgba(18, 19, 22, .70) 100%);
}
.about-inner {
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  min-height: clamp(32rem, 84vh, 50rem);
  padding: clamp(3rem, 9vh, 6rem) clamp(1.25rem, 5vw, 5.5rem);
}
.about-copy { max-width: 30rem; }
.about-h2 {
  margin: 0 0 1.3rem;
  font: 500 clamp(1.8rem, 3.4vw, 2.9rem)/1.1 var(--font-display, Georgia, serif);
  letter-spacing: -.015em;
  text-wrap: balance;
}
.about-copy p:not(.dsc-label) {
  margin: 0 0 1.15rem;
  color: var(--dsc-ink-2);
  font: 300 clamp(.97rem, 1.2vw, 1.08rem)/1.68 var(--font-interface, system-ui, sans-serif);
  text-wrap: pretty;
}
.about-copy .dsc-cta { margin-top: .9rem; }

/* Below 1120px the wall STOPS being a backdrop and becomes a picture above the
   copy. This is not a taste call — it is forced by the sign.

   The bronze lettering is a band across 29%-97% of the plate's width. With
   object-position at 50% and object-fit:cover, showing all of it needs the box
   to be at least ~1.68 wide-to-tall, essentially the image's own 1.79. Measured, not assumed: the box height is driven by the COPY, not the
   min-height, so at 1280 it is 1249x918 = 1.36 and the sign is cut there too
   — which is why this threshold is 1120px and not 900px. A 320px phone against a 40rem min-height is
   304x770 = 0.40, which crops to 22% of the image width — the middle third of
   the wordmark and nothing else. No object-position fixes it; a 22% window
   cannot contain a 68% band.

   So on narrow screens the image sits in flow at its own aspect ratio, whole,
   and the copy sits beneath it on the plate. Both survive intact. */
@media (max-width: 1120px) {
  .about-bg {
    position: static;
    width: 100%;
    height: auto;
    aspect-ratio: 1920 / 1072;
    object-fit: cover;       /* no crop: the box now matches the source ratio */
    object-position: 50% 50%;
    z-index: auto;
  }
  /* the scrim existed to keep copy legible ON the photo; there is no copy on
     the photo any more, and leaving it would just grey the picture */
  .about::after { content: none; }
  .about-inner {
    justify-content: flex-start;
    align-items: flex-start;
    min-height: 0;
    padding-top: clamp(2rem, 6vh, 3rem);
  }
  .about-copy { max-width: none; }
}

/* ── inside the studio ─────────────────────────────────────────── */
.craft {
  background: var(--dsc-plate);
  color: var(--dsc-ink);
  padding: clamp(4rem, 11vh, 8rem) clamp(1.25rem, 5vw, 5.5rem);
}
.craft-head { max-width: 46rem; margin: 0 auto clamp(2.2rem, 5vh, 3.5rem); text-align: center; }
.craft-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(1.1rem, 2.2vw, 1.9rem);
  max-width: 78rem;
  margin: 0 auto;
}
.craft-wide { grid-column: span 2; }
.craft-item { margin: 0; }
.craft-item img {
  display: block;
  width: 100%;
  height: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 14px;
  border: 1px solid var(--dsc-edge);
}
.craft-wide img { aspect-ratio: 21 / 9; }
.craft-item figcaption {
  margin-top: .95rem;
  color: var(--dsc-ink-2);
  font: 300 .92rem/1.55 var(--font-interface, system-ui, sans-serif);
  text-wrap: pretty;
}
.craft-item figcaption strong { color: var(--dsc-ink); font-weight: 500; }

@media (max-width: 760px) {
  .craft-grid { grid-template-columns: 1fr; }
  .craft-wide { grid-column: span 1; }
  .craft-wide img { aspect-ratio: 16 / 9; }
}

/* ── roadmap ───────────────────────────────────────────────────── */
.road, .log {
  background: var(--dsc-plate);
  color: var(--dsc-ink);
  padding: clamp(4rem, 11vh, 8rem) clamp(1.25rem, 5vw, 5.5rem);
}
.road-head, .log-head { max-width: 46rem; margin: 0 auto clamp(2.2rem, 5vh, 3.2rem); text-align: center; }
.road-list { list-style: none; margin: 0 auto; padding: 0; max-width: 46rem; }
.road-item {
  display: grid;
  grid-template-columns: 1.6rem 1fr;
  gap: .2rem 1rem;
  padding: 1.15rem 0;
  border-top: 1px solid var(--dsc-edge);
}
.road-item:last-child { border-bottom: 1px solid var(--dsc-edge); }
/* the dot carries the state: filled = shipped, ringed = in flight, faint = later */
.road-dot {
  width: 10px; height: 10px;
  margin-top: .42rem;
  border-radius: 50%;
  justify-self: center;
}
.road-item[data-state="done"]  .road-dot { background: #8fe09f; }
/* Token, not a hardcoded near-white: .road is not in the forced-dark list,
   so in light mode this ring composited to ~1.08:1 on the beige plate and
   the in-flight item was the only invisible one. */
.road-item[data-state="next"]  .road-dot { background: transparent; border: 2px solid var(--dsc-ink-2); }
.road-item[data-state="later"] .road-dot { background: var(--dsc-ink-4); }
.road-title {
  margin: 0 0 .3rem;
  color: var(--dsc-ink);
  font: 500 1rem/1.35 var(--font-interface, system-ui, sans-serif);
}
.road-title em {
  font-style: normal;
  margin-left: .5rem;
  color: var(--dsc-ink-2);
  font-size: .78rem;
  letter-spacing: .04em;
}
.road-desc {
  margin: 0;
  color: var(--dsc-ink-2);
  font: 300 .9rem/1.6 var(--font-interface, system-ui, sans-serif);
}

/* ── studio notes ──────────────────────────────────────────────── */
.log-list { max-width: 46rem; margin: 0 auto; }
.log-item {
  display: grid;
  grid-template-columns: 7rem 1fr;
  gap: 1rem;
  padding: 1.05rem 0;
  border-top: 1px solid var(--dsc-edge);
}
.log-item:last-child { border-bottom: 1px solid var(--dsc-edge); }
.log-item > span:not(.w):not(.wg) {   /* the date only — a bare `span` also caught every
                                word span inside the <p>, shrinking the note
                                body to .7rem with .1em letter-spacing. */
  color: var(--dsc-ink-2);
  font: 500 .7rem/1.6 var(--font-interface, system-ui, sans-serif);
  letter-spacing: .1em;
}
.log-item p {
  margin: 0;
  color: var(--dsc-ink-2);
  font: 300 .92rem/1.6 var(--font-interface, system-ui, sans-serif);
}
.log .room-more, .room .room-more { text-align: center; margin-top: 1.8rem; }
@media (max-width: 560px) { .log-item { grid-template-columns: 1fr; gap: .25rem; } }

/* ── footer ────────────────────────────────────────────────────── */
.foot {
  background: var(--dsc-plate-2);
  color: var(--dsc-ink-2);
  padding: clamp(3rem, 7vh, 4.5rem) clamp(1.25rem, 5vw, 5.5rem) 2rem;
  border-top: 1px solid var(--dsc-edge);
}
.foot-in {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) repeat(3, minmax(0, 1fr));
  gap: clamp(1.5rem, 3vw, 3rem);
  max-width: 72rem;
  margin: 0 auto;
}
/* Sits under the studio blurb in the wide first column rather than becoming a
   fifth column — four link columns plus icons is more grid than the content
   deserves, and it would squeeze the others. */
.foot-social { grid-column: 1; margin-top: .4rem; }
.foot-social ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
}
.foot-social a {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  min-height: 44px;              /* tap target */
  padding: 0 .85rem;
  border-radius: 999px;
  border: 1px solid var(--dsc-edge);
  color: var(--dsc-ink-2);
  text-decoration: none;
  font: 400 .82rem/1 var(--font-interface, system-ui, sans-serif);
  transition: border-color .22s cubic-bezier(.23, 1, .32, 1),
              color .22s cubic-bezier(.23, 1, .32, 1);
}
.foot-social svg {
  width: 17px; height: 17px; flex: none;
  transition: transform .28s cubic-bezier(.23, 1, .32, 1);
}

/* ── brand colour on hover ─────────────────────────────────────────
   Every icon draws in currentColor, so changing the link's `color` repaints
   the mark — no second icon, no sprite. Instagram is the exception: its mark
   is a gradient, so its three stops animate instead. Gradient stops sit
   OUTSIDE the currentColor mechanism, which is why they need their own rules.

   Resting state stays monochrome on purpose: four saturated logos sitting in
   the footer at all times would pull more attention than the products do. */
.foot-social .ig-1, .foot-social .ig-2, .foot-social .ig-3 {
  stop-color: currentColor;
  transition: stop-color .34s cubic-bezier(.23, 1, .32, 1);
}

@media (hover: hover) and (pointer: fine) {
  .foot-social a:hover { border-color: currentColor; }
  .foot-social a:hover svg { transform: scale(1.14) rotate(-4deg); }

  .foot-social a[data-social="instagram"]:hover { color: #E1306C; }
  .foot-social a[data-social="instagram"]:hover .ig-1 { stop-color: #F9CE34; }
  .foot-social a[data-social="instagram"]:hover .ig-2 { stop-color: #EE2A7B; }
  .foot-social a[data-social="instagram"]:hover .ig-3 { stop-color: #6228D7; }

  .foot-social a[data-social="reddit"]:hover   { color: #FF4500; }
  .foot-social a[data-social="whatsapp"]:hover { color: #25D366; }
  /* X's mark is black on light and white on dark — the one brand whose
     "original" colour depends on the theme, so it takes the ink token. */
  .foot-social a[data-social="x"]:hover        { color: var(--dsc-ink); }
}

.foot-social a:focus-visible { outline: 2px solid var(--dsc-ink); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .foot-social a, .foot-social svg,
  .foot-social .ig-1, .foot-social .ig-2, .foot-social .ig-3 { transition: none; }
  .foot-social a:hover svg { transform: none; }   /* colour still changes */
}
@media (max-width: 800px) { .foot-social { grid-column: 1 / -1; } }
.foot-in > div > p, .foot nav a {
  font: 300 .87rem/1.6 var(--font-interface, system-ui, sans-serif);
}
/* index.html sets this line at full --text, not muted. Matching it, or the two
   footers read as different weights of the same sentence. */
.foot-in > div > p { margin: .8rem 0 0; max-width: 22rem; color: var(--dsc-ink); }
.foot-in > div > p + p { color: var(--dsc-ink-2); }
.foot nav { display: flex; flex-direction: column; gap: .5rem; }
.foot nav .dsc-label { margin-bottom: .35rem; }
.foot nav a { color: var(--dsc-ink-2); text-decoration: none; }
@media (hover: hover) and (pointer: fine) { .foot nav a:hover { color: var(--dsc-ink); } }

/* ── footer touch targets ───────────────────────────────────────────
   The footer links are .87rem on 1.6 line-height = 22px, on a .5rem gap, so
   the pitch is 30px. That PASSES WCAG 2.2 AA: 2.5.8 wants 24x24, and its
   spacing exception is satisfied at 30px. It fails 2.5.5 AAA, which wants 44.

   Raising the target alone is not enough — at a 30px pitch, 44px hit areas
   would overlap and later siblings would swallow earlier ones' taps, which is
   the same trap the Project Office rail had. So the gap collapses to 0 as the
   target grows: 44px box on a 44px pitch, adjacent but never overlapping. The
   text does not move relative to itself; only the tappable box around it grows.

   Coarse pointers only. A mouse is precise, and on desktop the tighter 30px
   rhythm is what makes the columns read as lists rather than menus. */
@media (pointer: coarse) {
  .foot nav { gap: 0; }
  /* Scoped to the nav columns, which is what the 22px-on-a-30px-pitch finding
     was actually about. The earlier `.foot a:not([data-social])` was too broad
     in two ways, both of them visible on a phone:

     Its own carve-out below it could never win. `.foot a:not([data-social])` is
     (0,2,1) and `.foot .dsc-mark` is (0,2,0) — equal class count, but the first
     also carries a type selector, so it takes the wordmark regardless of source
     order. The lockup became a full-width 148px block instead of a shrink-
     wrapped 29px one, which is the opposite of what the comment claimed.

     And it caught the support@ mailto link, an inline link inside a paragraph.
     WCAG 2.5.8 exempts targets whose size is constrained by the line-height of
     surrounding text for exactly this reason; forcing it to a 44px flex box
     turned a run of prose into a block and doubled the paragraph's line box.

     Restricting to `nav` fixes both, because neither the wordmark nor the mail
     link is inside one. :not([data-social]) still matters — .foot-social is
     itself a <nav>, so without it the four icon pills would be caught too. */
  .foot nav a:not([data-social]) {
    display: flex;
    align-items: center;
    min-height: 44px;
  }
}
/* The studio signature. The art is rendered on its own near-black ground, so it
   is masked to fade into the footer plate at every edge rather than sitting in
   a visible rectangle — the seam is what would give it away as a pasted image. */
.foot-sig {
  max-width: 72rem;
  margin: clamp(2rem, 5vh, 3.5rem) auto 0;
  padding: 0;
  min-width: 0;
}
.foot-sig img {
  display: block;
  width: 100%;
  height: auto;
  /* No blend mode. `screen` looked right in principle — drop the art's black
     ground into the plate — but the letters are themselves dark grey, so
     screening them against a dark footer erased the whole thing.

     The mask is VERTICAL, not radial. A radial one centred at 50% faded the
     figure into near-invisibility, because she stands at x≈0.11 — right in the
     falloff. Measured: her trousers are (227,213,200) in the source, so the
     art was never the problem, the mask was.

     The fade is ONLY a short fog along the top edge, reaching full opacity by
     16%. An earlier version ramped to opaque at 42%, which is halfway down the
     figure — so she rendered as a washed-out watermark. Everything below the
     fog is the render at full strength. Horizontal is left alone: the art's
     ground (24,23,28) already sits within a few points of the plate (var(--dsc-plate-2)),
     so the side edges have no seam to hide. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.5) 7%, #000 16%, #000 100%);
          mask-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.5) 7%, #000 16%, #000 100%);
}
@supports not (mask-image: radial-gradient(#000, transparent)) {
  .foot-sig img { border-radius: 14px; }
}
@media (max-width: 560px) { .foot-sig { margin-top: 1.5rem; } }
.foot-base {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .8rem 1.5rem;
  max-width: 72rem;
  margin: clamp(1.4rem, 3vh, 2rem) auto 0;
  padding-top: 1.4rem;
  border-top: 1px solid var(--dsc-edge);
  color: var(--dsc-ink-2);
  font: 300 .8rem/1.5 var(--font-interface, system-ui, sans-serif);
}
.foot-live { display: inline-flex; align-items: center; gap: .5rem; color: var(--dsc-ink-2); }
.foot-live i { width: 7px; height: 7px; border-radius: 50%; background: #8fe09f; }
@media (max-width: 800px) { .foot-in { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ── how we work ───────────────────────────────────────────────── */
.rules {
  background: var(--dsc-plate);
  color: var(--dsc-ink);
  padding: clamp(4rem, 11vh, 8rem) clamp(1.25rem, 5vw, 5.5rem);
}
.rules-head { max-width: 46rem; margin: 0 auto clamp(2.4rem, 6vh, 4rem); text-align: center; }
.rules-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1rem, 2vw, 1.6rem);
  max-width: 72rem;
  margin: 0 auto;
}
.rule-card {
  min-width: 0;
  padding: clamp(1.5rem, 2.4vw, 2.1rem);
  border-radius: 14px;
  border: 1px solid var(--dsc-edge);
  background: var(--dsc-card);
}
.rule-num {
  display: block;
  margin-bottom: 1.5rem;
  color: var(--dsc-ink-2);
  font: 500 .72rem/1 var(--font-interface, system-ui, sans-serif);
  letter-spacing: .12em;
  font-variant-numeric: tabular-nums;
}
.rule-card h3 {
  margin: 0 0 .75rem;
  font: 500 clamp(1.05rem, 1.5vw, 1.28rem)/1.25 var(--font-display, Georgia, serif);
  letter-spacing: -.008em;
  color: var(--dsc-ink);
}
.rule-card p {
  margin: 0;
  color: var(--dsc-ink-2);
  font: 300 .93rem/1.62 var(--font-interface, system-ui, sans-serif);
  text-wrap: pretty;
}
@media (max-width: 860px) { .rules-grid { grid-template-columns: 1fr; } }

/* ── shape what comes next ─────────────────────────────────────── */
.next {
  background: var(--dsc-plate);
  color: var(--dsc-ink);
  padding: clamp(4rem, 11vh, 8rem) clamp(1.25rem, 5vw, 5.5rem);
}
.next-head { max-width: 46rem; margin: 0 auto clamp(2.4rem, 6vh, 3.6rem); text-align: center; }
.next-panel {
  display: grid;
  grid-template-columns: minmax(0, .82fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 3.5vw, 3.5rem);
  max-width: 68rem;
  margin: 0 auto;
  padding: clamp(1.6rem, 3vw, 2.6rem);
  border-radius: 18px;
  border: 1px solid var(--dsc-edge);
  background: var(--dsc-card);
}
.next-intro { min-width: 0; }
.next-title {
  margin: 0 0 .9rem;
  font: 500 clamp(1.2rem, 2vw, 1.6rem)/1.2 var(--font-display, Georgia, serif);
  color: var(--dsc-ink);
}
.next-intro p {
  margin: 0 0 1.3rem;
  color: var(--dsc-ink-2);
  font: 300 .93rem/1.62 var(--font-interface, system-ui, sans-serif);
}
.next-steps { list-style: none; margin: 0 0 1.3rem; padding: 0; }
.next-steps li {
  display: flex;
  align-items: baseline;
  gap: .7rem;
  padding: .42rem 0;
  color: var(--dsc-ink-2);
  font: 300 .9rem/1.5 var(--font-interface, system-ui, sans-serif);
}
/* Must exclude .w: descent.js wraps every word in a <span class="w"> for the
   hover bump, and those land as direct children of the <li> alongside the
   number marker. A bare `.next-steps span` turned the word "reply" into a
   1.35rem grid box, which pushed the page 8px wide on a 375px phone. */
.next-steps li > span:not(.w):not(.wg) {
  flex: none;
  width: 1.35rem; height: 1.35rem;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid var(--dsc-edge-2);
  font-size: .68rem;
  color: var(--dsc-ink-2);
}
.next-privacy { font-size: .84rem !important; color: var(--dsc-ink-3) !important; }

.next-cats { min-width: 0; }
.next-q {
  margin: 0 0 1rem;
  color: var(--dsc-ink-2);
  font: 500 .66rem/1 var(--font-interface, system-ui, sans-serif);
  letter-spacing: .18em;
  text-transform: uppercase;
}
.next-cats ul {
  list-style: none;
  margin: 0 0 1.6rem;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .7rem;
}
.next-cats li {
  min-width: 0;
  padding: .85rem 1rem;
  border-radius: 11px;
  border: 1px solid var(--dsc-edge-2);
  color: var(--dsc-ink-2);
  font: 300 .8rem/1.45 var(--font-interface, system-ui, sans-serif);
}
.next-cats li strong {
  display: block;
  margin-bottom: .22rem;
  color: var(--dsc-ink);
  font-weight: 500;
  font-size: .9rem;
}
.next-hint {
  margin: .85rem 0 0;
  color: var(--dsc-ink-2);
  font: 300 .82rem/1.5 var(--font-interface, system-ui, sans-serif);
}
@media (max-width: 860px) {
  .next-panel { grid-template-columns: 1fr; }
  .next-cats ul { grid-template-columns: 1fr; }
}

/* ── design & planning ─────────────────────────────────────────────
   Two columns that swap to stacked below 900px. The room photograph is the
   full height of the row on desktop so it reads as a window into the space
   rather than a thumbnail beside the text. */
/* Full-bleed room with the copy laid over it. The earlier two-column version
   cropped the picture to fill its cell — the figure at the right edge lost an
   arm to `object-fit: cover`. Here the image keeps its own aspect at width
   100%, so nothing is cut, and the text sits on top. */
.plan {
  position: relative;
  isolation: isolate;
  background: var(--dsc-plate);
  color: var(--dsc-ink);
}
.plan-bg {
  display: block;
  width: 100%;
  height: auto;               /* no cover, no crop */
}
/* Weighted to the bottom, where the copy lands, and left, away from the
   bronze signage on the right of the plate. */
.plan::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(to top,
      rgba(12, 13, 15, .93) 0%, rgba(12, 13, 15, .78) 34%,
      rgba(12, 13, 15, .30) 62%, transparent 88%),
    linear-gradient(to right,
      rgba(12, 13, 15, .55) 0%, rgba(12, 13, 15, .12) 46%, transparent 72%);
}
.plan-copy {
  position: absolute;
  z-index: 2;
  left: clamp(1.25rem, 5vw, 5.5rem);
  right: clamp(1.25rem, 5vw, 5.5rem);
  bottom: clamp(1.5rem, 5vh, 3.5rem);
  top: clamp(1.5rem, 5vh, 3.5rem);
  /* Bottom-anchored alone let the block grow upward out of the section when it
     outgrew the picture. Pinning top AND bottom bounds it to the image, and
     flex-end keeps it sitting low inside those bounds. */
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  max-width: 62rem;
}
.plan-copy > p:not(.dsc-label) {
  margin: 0 0 1.8rem;
  color: var(--dsc-ink-2);
  font: 300 clamp(.97rem, 1.2vw, 1.08rem)/1.68 var(--font-interface, system-ui, sans-serif);
  text-wrap: pretty;
}
.plan-copy .room-h2 { text-align: left; margin-bottom: 1.1rem; }

/* Four across when laid over the picture — stacked would push the copy up over
   the room and cover the thing it is describing. */
.plan-steps {
  list-style: none;
  margin: 1.4rem 0 0;
  padding: 1.2rem 0 0;
  border-top: 1px solid var(--dsc-edge-2);
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1.4rem;
}
.plan-steps li {
  min-width: 0;
  color: var(--dsc-ink-2);
  font: 300 .86rem/1.5 var(--font-interface, system-ui, sans-serif);
}
.plan-steps li > span:not(.w):not(.wg) {   /* the "01" marker only — not the word spans */
  display: block;
  margin-bottom: .4rem;
  color: var(--dsc-ink-3);
  font-size: .7rem;
  letter-spacing: .12em;
  font-variant-numeric: tabular-nums;
}
.plan-steps strong {
  display: block;
  color: var(--dsc-ink);
  font-weight: 500;
  font-size: .97rem;
  margin-bottom: .2rem;
}

/* The overlay is only safe while the picture is TALLER than the copy sitting
   on it. The image height is width x 1072/1920 (0.558), and the copy needs
   ~683px, so the overlay needs about 1230px of width. Below that the copy —
   anchored to the section's bottom — overflowed upward straight into the
   studio section above. Measured at 952px: section 514px tall, copy 683px.
   The old 900px breakpoint was set by eye at 1440px, where it happened to fit. */
@media (max-width: 1280px) {
  .plan::after {
    background: linear-gradient(to top, rgba(12, 13, 15, .55) 0%, transparent 60%);
  }
  .plan-copy {
    /* relative, not static: the base rule's z-index:2 is inert on a static
       box, so .plan::after (z-index:1) painted its scrim over the steps and
       dropped them from ~14.3:1 to ~3.8:1 — below AA on every phone.
       inset:auto with it, because on a relative box the base rule's
       left/right stop being edges and become OFFSETS — they shoved the block
       20px right and off the screen. */
    position: relative;
    inset: auto;
    display: block;              /* undo the flex bounding used by the overlay */
    padding: clamp(1.75rem, 5vw, 2.75rem) clamp(1.25rem, 5vw, 3rem) clamp(2.5rem, 7vh, 4rem);
    max-width: none;
  }
  .plan-steps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 520px) { .plan-steps { grid-template-columns: 1fr; } }

/* ── the product room ──────────────────────────────────────────────
   The room itself is now project-office.html — a seven-room walkthrough with
   its own splash and scroll choreography. What lives here is the doorway to
   it, so the inlined 3D deck (and the lightbox that only existed to enlarge
   its 150px slabs) has been removed rather than left running alongside. */
.room {
  background: var(--dsc-plate);
  color: var(--dsc-ink);
  padding-block: clamp(4.5rem, 12vh, 9rem);
  overflow: hidden;
}
.room-head {
  max-width: 46rem;
  margin: 0 auto clamp(2.5rem, 6vh, 4.5rem);
  padding-inline: clamp(1.25rem, 5vw, 3rem);
  text-align: center;
}
.room-h2 {
  margin: 0 0 1rem;
  font: 500 clamp(1.9rem, 4.4vw, 3.4rem)/1.08 var(--font-display, Georgia, serif);
  letter-spacing: -.016em;
  text-wrap: balance;
}
.portal {
  display: flex;
  justify-content: center;
  padding-inline: clamp(1.25rem, 5vw, 3rem);
}

/* Deliberately a door, not a button: wide, quiet, and it opens rather than
   submits. The glow sits behind the label on ::before so it reads as light
   coming from the far side of it.

   It was too quiet, and on a phone it was invisible. Every affordance -- the
   light, the lifted frame, the arrow sliding -- lived in the
   `@media (hover: hover) and (pointer: fine)` block below. A touch device
   matches neither query, so most visitors only ever saw the resting state:
   ::before at opacity 0, a 20% hairline, and #2b2d31 on #212327, ten levels
   apart per channel. Visitors reported being unable to find the entrance to
   the tour; on a phone there was nothing to find.

   So the resting state now carries the affordance and hover deepens it. This
   does not turn the door into a button -- it turns the light on. The lift stays
   on the BOTTOM edge where the doorway metaphor puts it, which also keeps the
   label's backdrop dark: .portal-label is ink-2 chosen against this exact fill
   (see its note below), and brightening behind it would undo that. */
.portal-cta {
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "label arrow" "title arrow";
  align-items: center;
  gap: .45rem 1.5rem;
  width: min(46rem, 100%);
  padding: clamp(1.6rem, 4vw, 2.4rem) clamp(1.5rem, 4vw, 2.6rem);
  /* Not edge-2 (20%): that is the token for quiet dividers, and this is the
     entrance to the whole tour. .32 is roughly what the hero's own primary CTA
     already carries, so the door is no louder than the button above it. */
  border: 1px solid rgba(255, 255, 255, .32);
  border-radius: 18px;
  background: var(--dsc-card);
  color: var(--dsc-ink);
  text-decoration: none;
  overflow: hidden;
  transition: border-color .3s cubic-bezier(.23, 1, .32, 1),
              transform .3s cubic-bezier(.23, 1, .32, 1);
}
.portal-cta::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(120% 140% at 50% 120%,
    color-mix(in srgb, var(--dsc-ink) 18%, transparent) 0%, transparent 68%);
  /* Visible at rest, not only on hover -- this is the only cue a touch visitor
     ever gets. Hover still takes it to 1, so a pointer keeps the full effect. */
  opacity: .5;
  transition: opacity .3s cubic-bezier(.23, 1, .32, 1);
}
.portal-label {
  grid-area: label;
  font: 500 .68rem/1 var(--font-interface, system-ui, sans-serif);
  letter-spacing: .24em;
  text-transform: uppercase;
  /* ink-2, not ink-3: at .68rem this is small text needing 4.5:1, and ink-3
     against the portal's card fill measured 3.76:1 in light mode. Every other
     .dsc-label on the page already uses ink-2, which measures 5.47:1. */
  color: var(--dsc-ink-2);
}
.portal-title {
  grid-area: title;
  font: 500 clamp(1.5rem, 3.2vw, 2.3rem)/1.1 var(--font-display, Georgia, serif);
  letter-spacing: -.014em;
}
.portal-arrow {
  grid-area: arrow;
  display: grid;
  place-items: center;
  width: 3rem; height: 3rem;
  /* The one part that has to read as pressable at a glance, so it carries the
     only accent the dark theme actually has.

     site.css is explicit that there is "no gold in dark: the accent is light,
     not warm" -- --gold is remapped to a neutral there. That leaves --ok
     (#8fe09f) as the one chromatic colour available, and its meaning, live and
     go, is the right one for a door you walk through. It is already on this
     page seventeen times: the roadmap's done dots and the footer's live pip.

     Tinted rather than solid at rest, so the door stays quiet and the handle
     is the thing that catches the eye. Hover and press fill it in. */
  border: 1px solid color-mix(in srgb, var(--ok, #8fe09f) 55%, transparent);
  background: color-mix(in srgb, var(--ok, #8fe09f) 12%, transparent);
  border-radius: 50%;
  font-size: 1.1rem;
  color: var(--ok, #8fe09f);
  transition: transform .3s cubic-bezier(.23, 1, .32, 1),
              border-color .3s cubic-bezier(.23, 1, .32, 1),
              color .3s cubic-bezier(.23, 1, .32, 1);
}
@media (hover: hover) and (pointer: fine) {
  .portal-cta:hover { border-color: var(--dsc-ink-3); transform: translateY(-3px); }
  .portal-cta:hover::before { opacity: 1; }
  .portal-cta:hover .portal-arrow {
    transform: translateX(4px);
    border-color: var(--ok, #8fe09f);
    background: var(--ok, #8fe09f);
    /* dark glyph on the filled mint: #8fe09f is a light colour, so the arrow
       has to invert or it disappears into its own button. */
    color: #17281c;
  }
}
.portal-cta:focus-visible { outline: 2px solid var(--dsc-ink); outline-offset: 4px; }
/* Touch never matches the hover block above, so the press state is the only
   feedback a phone gets that the door registered the tap. */
.portal-cta:active .portal-arrow {
  background: var(--ok, #8fe09f);
  border-color: var(--ok, #8fe09f);
  color: #17281c;
}
.portal-cta:focus-visible .portal-arrow {
  background: var(--ok, #8fe09f);
  border-color: var(--ok, #8fe09f);
  color: #17281c;
}
.portal-arrow svg { display: block; }
@media (prefers-reduced-motion: reduce) {
  .portal-cta, .portal-cta::before, .portal-arrow { transition: none; }
  .portal-cta:hover { transform: none; }
}
/* Below 460px the circle and the serif title compete for the same row, so the
   gap tightens, the circle shrinks and the title steps down a size -- but the
   arrow STAYS on the right.

   It used to stack: label, title, then the circle on its own row. That left it
   stranded at the bottom-left with the whole width of the card empty beside it,
   and the result read as a panel with a stray button rather than a door with a
   handle. Keeping the base "label arrow" / "title arrow" areas gives a
   full-width bar with the handle where a handle belongs.

   The widths work at 360px, the narrowest phone worth designing for: 40px of
   padding leaves 320, the circle and gap take 56, and the title at 1.35rem sits
   near 227px in the remaining 264. */
@media (max-width: 460px) {
  .portal-cta {
    gap: .4rem .9rem;
    padding: 1.5rem 1.25rem;
  }
  .portal-arrow { width: 2.6rem; height: 2.6rem; }
  .portal-title { font-size: 1.35rem; }
}

.room-sub {
  margin: 0;
  color: var(--dsc-ink-2);
  font: 300 clamp(.95rem, 1.2vw, 1.06rem)/1.6 var(--font-interface, system-ui, sans-serif);
  text-wrap: pretty;
}

.room-stage { perspective: 1500px; perspective-origin: 50% 45%; padding-inline: 1rem; }
.room-deck {
  position: relative;
  height: clamp(300px, 46vh, 500px);
  transform-style: preserve-3d;
  transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: transform .5s cubic-bezier(.23, 1, .32, 1);
}

.room-slab {
  position: absolute;
  left: 50%;
  top: 50%;
  width: clamp(112px, 13vw, 178px);
  transform:
    translate3d(calc(-50% + var(--x)), -50%, var(--z))
    rotateY(var(--r)) scale(var(--s, 1));
  transform-style: preserve-3d;
  text-decoration: none;
  transition: transform .45s cubic-bezier(.23, 1, .32, 1);
}
.room-slab img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 14px;
  border: 1px solid var(--dsc-edge-2);
  box-shadow: 0 40px 70px -30px rgba(0, 0, 0, .9);
}
.room-slab > span:not(.w):not(.wg):not(.wg) {      /* the caption only — not the word spans in it */
  display: block;
  margin-top: .85rem;
  text-align: center;
  color: var(--dsc-ink-3);
  font: 500 .62rem/1 var(--font-interface, system-ui, sans-serif);
  letter-spacing: .2em;
  text-transform: uppercase;
  transition: color .25s cubic-bezier(.23, 1, .32, 1);
}
.room-hero { width: clamp(140px, 16.5vw, 224px); }
.room-hero img { border-color: var(--dsc-edge-2); }

@media (hover: hover) and (pointer: fine) {
  .room-slab:hover { --s: 1.06; }
  .room-slab:hover span { color: var(--dsc-ink); }
}
.room-slab:focus-visible { outline: 2px solid var(--dsc-ink); outline-offset: 8px; border-radius: 16px; }

.room-rest {
  max-width: 46rem;
  margin: clamp(3rem, 8vh, 5.5rem) auto 0;
  padding-inline: clamp(1.25rem, 5vw, 3rem);
}
.room-rest .dsc-list li { border-color: var(--dsc-edge); }
.room-rest .dsc-list a { color: var(--dsc-ink); }
.room-rest .dsc-n { color: var(--dsc-ink-3); }
.room-rest .dsc-list em { color: var(--dsc-ink-2); }
.room-more { margin: 1.6rem 0 0; }
.room-more .inline { color: var(--dsc-ink-2); }

/* Stack out of 3D entirely on small screens — a five-deep Z stack on a 375px
   viewport is unreadable, and the parallax has no room to read as depth. */
@media (max-width: 760px) {
  .room-stage { perspective: none; padding-inline: 0; }
  .room-deck {
    height: auto;
    transform: none;
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1.5rem 1rem;
    padding-inline: 1.25rem;
  }
  .room-slab {
    position: static;
    width: 100%;
    transform: none;
  }
  .room-hero { grid-column: span 2; justify-self: center; max-width: 210px; }
}

@media (prefers-reduced-motion: reduce) {
  .room-deck, .room-slab { transition: none; }
}

/* ── the page resuming underneath ──────────────────────────────── */
.dsc-after { padding-block: clamp(4.5rem, 11vh, 8rem); }
.dsc-list { list-style: none; margin: 2.5rem 0 2rem; padding: 0; }
.dsc-list li {
  display: flex;
  align-items: baseline;
  gap: clamp(.9rem, 2.4vw, 2rem);
  padding: 1.15rem 0;
  border-top: 1px solid var(--line);
}
.dsc-list li:last-child { border-bottom: 1px solid var(--line); }
.dsc-n {
  flex: none;
  color: var(--muted2);
  font: 400 .74rem/1 var(--font-interface, system-ui, sans-serif);
  letter-spacing: .1em;
  font-variant-numeric: tabular-nums;
}
.dsc-list a {
  color: var(--text);
  text-decoration: none;
  font: 500 clamp(1.15rem, 2.4vw, 1.7rem)/1.2 var(--font-display, Georgia, serif);
  letter-spacing: -.01em;
}
@media (hover: hover) and (pointer: fine) { .dsc-list a:hover { text-decoration: underline; text-underline-offset: 5px; } }
.dsc-list em {
  margin-left: auto;
  text-align: right;
  color: var(--muted);
  font: 400 .8rem/1.3 var(--font-interface, system-ui, sans-serif);
  font-style: normal;
}

/* ── scroll hint ───────────────────────────────────────────────── */
.dsc-hint {
  position: absolute;
  left: 50%;
  bottom: 1.5rem;
  translate: -50% 0;
  width: 1px;
  height: 46px;
  overflow: hidden;
  background: var(--dsc-ink-4);
  pointer-events: none;
}
.dsc-hint::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--dsc-ink);
  animation: dsc-trace 2.4s cubic-bezier(.6, 0, .2, 1) infinite;
}
@keyframes dsc-trace {
  0%   { transform: translateY(-100%); }
  55%  { transform: translateY(100%); }
  100% { transform: translateY(100%); }
}

@media (max-width: 720px) {
  .dsc-aside { max-width: min(20rem, 76vw); top: 42%; }
  .dsc-aside-r, .dsc-aside-l { right: 1.25rem; left: 1.25rem; text-align: left; }
  .dsc-id { bottom: 4.5rem; }
  .dsc-play img { height: 46px; }
}

@media (prefers-reduced-motion: reduce) {
  .dsc-id .dsc-label, .dsc-id .dsc-h1, .dsc-id .dsc-p, .dsc-id .dsc-cta {
    animation: none;
  }
  .dsc-hint { display: none; }        /* nothing to hint at once the scrub is gone */
  .dsc-cta, .dsc-cta .dsc-arrow { transition: none; }

  /* The scroll-jack itself is the problem, not the animation inside it: five
     viewports of scrolling with a pinned page is a classic vestibular trigger.
     Collapse it to one screen and let the page scroll normally — descent.js
     paints a single still and skips its loop to match. */
  .descent { height: 100vh; height: 100svh; }
  .dsc-sticky { position: relative; }
  .dsc-scrim {
    background:
      linear-gradient(to right,
        rgba(12, 13, 15, .50) 0%, rgba(12, 13, 15, .30) 24%,
        rgba(12, 13, 15, .08) 42%, transparent 56%),
      linear-gradient(to top,
        rgba(12, 13, 15, .78) 0%, rgba(12, 13, 15, .34) 46%, transparent 82%);
  }
}


/* ── the deck's store buttons ───────────────────────────────────────
   Same pair finance-tracker.html uses, restated here because that page's
   rules live in a stylesheet this one does not load. */
.deck-head { margin-top: clamp(3.5rem, 9vh, 6rem); }
.dl-row.deck-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 1rem 1.4rem;
  margin-top: clamp(2.2rem, 5vh, 3.2rem);
  padding-inline: clamp(1.25rem, 5vw, 3rem);
}
.deck-actions .dl-badge { display: inline-flex; border-radius: 9px; }
.deck-actions .dl-badge img { display: block; height: 52px; width: auto; }
.deck-actions .dl-badge:focus-visible { outline: 2px solid var(--dsc-ink); outline-offset: 4px; }
.deck-actions .dl-alt {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  min-height: 52px;             /* matches the badge so the row sits level */
  padding: 0 1.5rem;
  border: 1px solid var(--dsc-ink-3);
  border-radius: 999px;
  color: var(--dsc-ink);
  text-decoration: none;
  font: 500 .74rem/1 var(--font-interface, system-ui, sans-serif);
  letter-spacing: .13em;
  text-transform: uppercase;
  transition: background .22s cubic-bezier(.23, 1, .32, 1),
              border-color .22s cubic-bezier(.23, 1, .32, 1);
}
@media (hover: hover) and (pointer: fine) {
  .deck-actions .dl-alt:hover {
    background: color-mix(in srgb, var(--dsc-ink) 12%, transparent);
    border-color: var(--dsc-ink-2);
  }
}
.deck-actions .dl-alt:focus-visible { outline: 2px solid var(--dsc-ink); outline-offset: 3px; }

/* ── print ──────────────────────────────────────────────────────
   Save-as-PDF walked through roughly a dozen blank pages before reaching any
   content: .descent reserves 1200vh of scroll, .dsc-sticky pins a blank plate
   across it, and every .dsc-beat is hidden until the rAF loop paints one —
   which never happens in a print context. Collapse the scroll-jack, drop the
   painted layers, and let the beats print as ordinary stacked text. */
@media print{
  .descent{height:auto}
  .dsc-sticky{position:relative;height:auto;overflow:visible}
  .dsc-canvas,.dsc-scrim,.dsc-hint,.dsc-progress{display:none}
  .dsc-beat{position:static;opacity:1!important;visibility:visible!important;
    transform:none!important}
}


/* On a phone the beat copy runs nearly the full width, so the left wash has to
   reach past the halfway point or the ends of lines sit unprotected.

   The vertical wash also had a weak band. At 411px the hero crops so the
   bright horizon lands around 60% up, which is exactly where the identity
   label sits — measured against real device pixels it read rgb(113,117,119)
   behind the label and scored 3.03, under AA. At 1440 the same label sits on
   rgb(68,76,81) and scores 5.05, so no desktop run could surface it.

   Colour alone cannot fix this one. Against rgb(113,117,119) the luminance
   needed for 4.5 is 0.965; solid #f4f4f5 is 0.905 and reaches only 4.23, and
   pure #ffffff only 4.65. The ground has to come down, so the two stops that
   cover 46–72% are raised and the label goes solid. Measured together that
   lands near 5.3. */
@media (max-width: 700px) {
  .dsc-scrim {
    background:
      linear-gradient(to right,
        rgba(12, 13, 15, .52) 0%, rgba(12, 13, 15, .40) 45%,
        rgba(12, 13, 15, .18) 74%, rgba(12, 13, 15, .06) 100%),
      linear-gradient(to top,
        rgba(12, 13, 15, .78) 0%, rgba(12, 13, 15, .50) 46%,
        rgba(12, 13, 15, .28) 72%, transparent 100%);
  }
  /* Solid, not --dsc-ink-2's .72: this line is 11px uppercase on wide
     tracking, the least forgiving type on the page, sitting on its
     brightest ground.

     The literal, not var(--dsc-ink). Written as the token first, this rule
     parsed, sat in the matching @media beside the .dsc-scrim rule that DID
     apply, matched the element by querySelector — and the engine's matched
     list still showed only .dsc-label from line 145. An identical rule
     injected at runtime applied immediately. #f4f4f5 is the value --dsc-ink
     already resolves to on .descent, so nothing changes but the indirection,
     and this is the value the hero's h1 and wordmark are painted with.

     Moved to the top of this file: the desktop pass found the same line
     failing at 1440 as well, so it is no longer a phone-only correction and
     does not belong in a phone-only block. */
}
