/* ==========================================================================
   NNP RAISINS — HOME PAGE VARIANT 3
   Built 2026-08-26. His brief after seeing v2: keep the motion and the purple
   hero, drop the video globe for the real WebGL one, rebuild the hero as a
   diorama, add the grape journey, and put the sections in his order.

   ⚠ THIS FILE IS LOADED BY ONE PAGE, index.html. site.css loads first and is
   NOT edited — it carries the tokens, the masthead, the footer, the caliber
   belt, the buttons, the process grid, the certification cards and the globe's
   own layout. Everything here is either new or a repaint for a dark hero.

   ⚠ DO NOT MOVE A RULE FROM HERE INTO site.css. Sixteen pages sit behind that
   file and this page is not one of them.

   NO NEW COLOUR IS DEFINED HERE. Every value resolves to a site.css token.
   ========================================================================== */

/* ==========================================================================
   1. THE HERO — the invitation as a diorama
   ========================================================================== */

/* ⚠ THE BOTTOM OF THE HERO IS THE TRANSITION INTO THE GLOBE, and that is why
   the gradient ends on #02040a rather than on violet. That value is the globe
   scene's own space black — the planet's background — so the hero does not end
   and the globe begin; the sky simply keeps darkening until there are stars in
   it. It is the same move `.dark-dusk` makes further down the page, which was
   his ask on 2026-08-18, applied to the band that now sits above the globe.
   Change one and the seam appears. */
/* ⚠ THE PAGE STARTS UNDER THE BAR ON THIS PAGE, AND ONLY ON THIS PAGE.
   site.css gives `body` a `padding-top: var(--mast-h)` so the fixed masthead
   never covers anything — correct for sixteen pages that open on paper, and
   fatal to a glass bar: what sits behind glass is then 90px of bare `--paper`,
   above a dark hero, in a strip the width of the window.

   It is also why the hero was double-padded before this rule existed. `.v3-hero`
   already reserves `--mast-h` in its own padding-top; the body's copy of it was
   pushing the eyebrow 90px further down than the design called for.

   ⚠ IF YOU RESTORE THE BODY PADDING, THE TONE WATCHER STOPS TOO. home.js
   samples what crosses y=44, and with the padding back nothing does until the
   page has scrolled past it — the bar would sit on its default violet at rest
   and snap to glass a moment after the first wheel click. */
body { padding-top: 0; }

.v3-hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  color: var(--chalk);
  padding: calc(var(--mast-h) + clamp(18px, 1.9vw, 32px)) 0 0;
  /* ⚠ height, NOT min-height. His note on the night hero: the monuments
     *"supposed to be fully visible and stand on the bottom border"*. The
     panorama is anchored to the hero's floor, so that is only true while the
     hero's floor IS the fold — and with `min-height` the hero grows whenever
     the copy is a line taller than expected, taking the floor with it and
     sinking the madrasah and the tower below the screen. A fixed screen height
     is the guarantee. It is affordable because the copy was cut to a headline,
     one sentence, the show block and the buttons; if anything is ever added
     back here, check this at 1440x800 before believing it fits. */
  height: 100svh;
  /* ⚠ CENTRED IN THE SPACE ABOVE THE ARC, NOT PINNED UNDER THE BAR. The arc is
     anchored to the hero's floor and its height is a fixed fraction of the
     WIDTH (32.6vw), while the hero's height follows the viewport — so on a tall
     window the two are not related at all and top-aligned copy leaves a dead
     band four hundred pixels deep between the CTAs and the vineyards. The
     padding-top still holds the floor under the bar; this only spends what is
     left over. */
  display: flex; flex-direction: column; justify-content: center;
}

.v3-sky {
  position: absolute; inset: 0; z-index: -2;
  background:
    /* the low sun, sitting between Samarkand and Paris */
    radial-gradient(52% 40% at 68% 42%, rgba(226,183,255,0.20) 0%, rgba(226,183,255,0) 68%),
    radial-gradient(120% 90% at 10% 6%, rgba(75,30,120,0.9) 0%, rgba(55,18,94,0) 60%),
    linear-gradient(180deg,
      var(--violet-dp) 0%,
      #301053 34%,
      #1d0937 62%,
      #0c0619 84%,
      #02040a 100%);
}

/* ---------------------------------------------------------------- the arc -- */
/* One image, full-bleed, sitting on the floor of the hero. Its own top edge IS
   the tip of the Eiffel spire — the file was trimmed to the content bounding
   box — so anything the hero puts above that line is guaranteed clear of the
   tower, and the whole question of the headline colliding with the spire is
   answered by geometry rather than by a magic number.

   ⚠ 106% WIDE AND CENTRED, NOT 100%. The pointer parallax drifts it a few
   pixels each way; at exactly 100% that drift opens a bare violet gap at the
   left or right edge on every mouse move. The 3% of overhang on each side is
   what absorbs it. Do not "tidy" this back to 100%.

   ⚠ 1x ASSET at 1915px. Past about 1800px of viewport it is being upscaled, and
   past ~2200 it shows. Capped rather than stretched, and the sky carries the
   rest of the width. */
/* ⚠ THE HORIZON IS FLAT NOW, AND THAT CHANGED THE GEOMETRY MORE THAN IT LOOKS.
   The first two renders put the scene on a curve — "the horizon of a small
   planet". His call on 2026-08-26, after seeing the ends of that curve sink out
   of the bottom of the hero: *"dont make it curvy, just flat"*. He was right,
   and it is not a taste fix. On a curved strip the two landmarks sit at the two
   ENDS, which is exactly where a curve is lowest, so any vertical squeeze took
   Samarkand and Paris out first and left the empty middle.

   Measured on the flat file: the skyline runs at **81–87% of the image height**
   right across the frame. Only two things break it — the madrasah at 55% and the
   Eiffel at 10% — and they stand at x≈12% and x≈90%. The centred copy column
   lives between 26% and 74%, so it can never meet either of them, and the
   `bottom` offset that the curved version needed to keep CTAs off the vineyards
   is gone. Nothing is cropped, at any window size.

   ⚠ THE FILE IS 78% TRANSPARENT and that is deliberate — the sky is ours, not
   the render's. The night in this hero is `.v3-sky`'s violet, and the image
   contributes only lit buildings, lamps and road.

   ⚠ 106% WIDE AND CENTRED, NOT 100%. The pointer drift shifts it a few pixels
   each way; at exactly 100% that opens a bare violet gap at one edge on every
   mouse move. The 3% of overhang each side absorbs it.

   ⚠ NO max-width. A cap made a full-bleed landscape stop short of the edges on
   anything wider than the cap and put violet gutters either side. */
.v3-arc { position: absolute; left: 50%; bottom: 0; z-index: -1;
  width: 106%; transform: translateX(-50%);
  pointer-events: none; line-height: 0; }

.v3-arc-img { display: block; width: 100%; height: auto; will-change: transform;
  /* ⚠ NO drop-shadow. The daylight version had one, to sit the land on the
     hero floor. This scene is lit from inside — glowing windows, street lamps,
     a floodlit tower — and a dark blur under a self-lit object reads as a bad
     cut-out, which is precisely what he called out on the previous pass. */ }

/* ---------------------------------------------------------------- the type -- */
/* ⚠ CENTRED, and that is a decision about the picture underneath rather than a
   taste for centred heroes. His note on the arc, 2026-08-26: *"move it to above
   tower, just adjusting"*. The arc is symmetrical and the two things standing
   tall in it are at the far left and the far right, so a column parked in the
   middle sits in the empty bowl of the curve and clears both — the Eiffel at
   ~90% of the width and the madrasah at ~10%. Left-aligned copy, which is what
   the diorama hero used, would now sit on top of Samarkand.

   ⚠ THE MAX-WIDTH IS THE COLLISION RULE. 760px inside a 1280 spine keeps the
   column between roughly 26% and 74% of the frame. Widen it and the lead's last
   line reaches Paris. */
.v3-hero-wrap { position: relative; z-index: 1; max-width: var(--max);
  text-align: center; }
.v3-hero-wrap > * { margin-inline: auto; }

/* Room for the land. The arc's height is 32.6vw (1915x624), and the countryside
   horizon across the middle of it sits at about 45% of that — so the copy has
   to stop roughly 15vw above the floor or the CTAs land in a vineyard. */
.v3-hero-wrap { padding-bottom: clamp(130px, 13vw, 260px); }

/* ⚠ `.v3-eyebrow`, `.v3-dot`, `.v3-route`, `.v3-route-end` and `.v3-route-line`
   were deleted here on 2026-08-26 with the markup they styled. Do not restore
   them from git without reading the note in index.html — all three elements
   were removed on his review, for reasons, not by accident.

   ⚠ `.v3-h1` WENT WITH THEM BY MISTAKE and is restored below. The deletion was
   done by cutting from the first eyebrow rule to the first lead rule, and the
   headline sat between the two. It did not look broken, which is the reason to
   write this down: site.css's generic `h1` caught the fall and painted it at a
   fixed 66px in the right family, so the only symptom was a headline that had
   quietly stopped scaling with the viewport. **Delete CSS by rule, not by
   range.** */

.v3-h1 { font-family: var(--display); font-weight: 800; color: var(--chalk);
  font-size: clamp(40px, 5.6vw, 78px); line-height: 0.94; letter-spacing: -0.03em;
  margin: 20px 0 0; }

/* ---------------------------------------------------- the rotating claim -- */
/* ⚠ REPAINT ONLY. The rotator is v1's component: `.h1-fixed`, `.rotator`,
   `.rot-sizer`, `.rot-line`, the grid that stacks the lines in one cell, the
   enter and exit transitions and the reduced-motion stop all live in site.css,
   and the six phrases are advanced by site.js's shared `heroClock`. The rules
   here change colour and vertical room for a dark hero and nothing else. Do
   not fork the component.

   ⚠ `--violet` IS THE ONE COLOUR THAT CANNOT STAY. site.css paints `.rot-line`
   in packaging violet because v1's hero is paper; on this hero that is violet
   type on a violet sky. `--violet-lt` is the accent every dark band on the site
   already swaps to. */
.v3-h1 .rot-line { color: var(--violet-lt); }

/* ⚠ THE ROTATOR NEEDS A TALLER LINE BOX THAN THE HEADLINE ABOVE IT, and the
   negative margin gives the space back. `.rotator` is `overflow: hidden` —
   that clip is what makes the lines slide in and out of a slot rather than
   appear — so anything the glyphs push past the line box is cut off, and this
   headline runs at `line-height: 0.94`. At 0.94 the phrase lost its lower half.
   1.12 clears the descender in "Naturally Dried"; the -0.14em keeps the two
   lines as tight as they look at rest. */
.v3-h1 .rotator { line-height: 1.12; margin-top: -0.14em;
  justify-items: center; text-align: center; }

/* site.css keeps `white-space: nowrap` on the lines so a phrase never breaks
   across two — which means a narrow phone has to solve it with type size
   instead. "Fourfold Sorted" is the widest of the seven at this weight. */
@media (max-width: 620px) {
  .v3-h1 { font-size: clamp(30px, 8.4vw, 40px); }
}

/* ⚠ THE MEASURE IS SET SO THE TWO SENTENCES GET A LINE EACH. 30ch was right
   for "Sun-dried raisins from Samarkand. Come and meet us at SIAL Paris." and
   wrong the moment the first sentence grew — it threw a lone "Paris." onto a
   third line. 48ch plus `text-wrap: balance` lets the browser find the break
   between the sentences rather than mid-clause. **Re-check this if the lead is
   ever rewritten again**: it is a two-line block by design, and a third line
   pushes the show notice and both buttons down a hero that is pinned to exactly
   one screen.

   ⚠ THE BREAK IS A `<br>` IN THE MARKUP, AND THE MEASURE ONLY HAS TO STAY OUT
   OF ITS WAY. Two other ways were tried and both put the break mid-clause —
   "…from the heart of / Samarkand. Come and meet…". `text-wrap: balance` evens
   the lines by LENGTH, which is the wrong criterion for two sentences; and
   tuning `max-width` in `ch` does not work either, because `ch` is the width of
   a zero and this face's average character is a good deal narrower, so 44ch
   still swallowed the first five letters of the second sentence. The `<br>` is
   the only one of the three that says what is actually meant.

   52ch is wide enough that the first sentence never wraps on its own on a
   desktop. On a phone `max-width` goes to `none` below 900px and both sentences
   wrap normally — the `<br>` just keeps the fold between them. */
.v3-lead { color: var(--chalk-dim); line-height: 1.55;
  font-size: clamp(16px, 1.35vw, 19px);
  max-width: 52ch; margin: 18px auto 0; }

/* The show block is the only place on this page that reads as an official
   notice rather than as marketing, which is why the SIAL mark belongs in it
   and not floating somewhere in the sky. Logo, then the divider, then the
   dates and the stand — the same order the poster uses. */
/* ⚠ `order: -1` IS LOAD-BEARING. The countdown block is written into this
   element by JS at `insertBefore(firstChild)`, so anything authored in the
   markup — this logo included — ends up AFTER the number no matter where it is
   typed. The mark has to lead: it is what tells you at a glance that the block
   is a show notice rather than a promotion. Moving the <img> in the HTML will
   not achieve this; only the order does. */
.v3-counter-logo { display: block; order: -1; flex: none;
  width: clamp(96px, 8.6vw, 132px); height: auto;
  padding-right: 20px; border-right: 1px solid rgba(242,237,248,0.18); }
.v3-counter { margin: 18px 0 0; padding: 16px 22px; border-radius: var(--r);
  border: 1px solid rgba(242,237,248,0.18); background: rgba(12,6,25,0.5);
  display: inline-flex; align-items: center; gap: 20px; flex-wrap: wrap;
  text-align: left; }
.v3-counter-days { display: flex; align-items: baseline; gap: 10px; margin: 0; flex: none; }
.v3-counter-n { font-family: var(--display); font-weight: 800; font-size: 46px;
  line-height: 1; color: var(--chalk); letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums; }
.v3-counter-u { font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--violet-lt); }
.v3-counter-static { margin: 0; display: flex; flex-direction: column; gap: 5px;
  font-size: 13px; letter-spacing: 0.06em; }
.v3-counter-static strong { color: var(--chalk); font-weight: 500; letter-spacing: 0.1em; }
.v3-counter-static span { color: var(--chalk-dim); }
.v3-counter.is-past .v3-counter-days .v3-counter-n { display: none; }

.v3-cta { display: flex; flex-wrap: wrap; justify-content: center; gap: 14px; margin: 22px 0 0; }

/* ⚠ IT SITS ABOVE THE SKYLINE, AND THE OFFSET HAS TO SCALE WITH THE WIDTH.
   His note, 2026-08-26: *"move this a bit higher so it doesnt block the image"*
   — parked 54px off the floor it was sitting on the road, among the street
   lamps and the headlights.

   The space under it is not a constant. The panorama's height is a fraction of
   the VIEWPORT WIDTH (625/2170 of a box that is 106vw), so the skyline stands
   4.3–4.9vw above the hero's floor — 62px at 1280, 92px at 1885. A fixed pixel
   offset clears it at one width and buries the cue at another. `5.2vw + 52px`
   holds a steady ~57px of dark sky under the pill at every size.

   The hero's top edge is y=0 (home.css removes site.css's masthead reserve
   on this page), so the fold is simply 100svh in hero coordinates, and
   `height: 100svh` above guarantees the hero's floor IS the fold. */
.v3-scrollcue { position: absolute; left: 50%; transform: translateX(-50%);
  top: calc(100svh - 5.2vw - 52px); bottom: auto; z-index: 1;
  display: inline-flex; align-items: center; gap: 10px; text-decoration: none;
  font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--chalk); padding: 10px 16px; border-radius: 999px;
  /* The plate went to 78% black while the cue still sat on the lit road, where
     glass disappeared into the street lamps. Now that it clears the skyline it
     is back over plain night sky, and a near-opaque slab there reads as a hole
     punched in the picture. Half strength plus the hairline holds the type
     without announcing itself. */
  background: rgba(6,2,14,0.5);
  border: 1px solid rgba(242,237,248,0.14);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px); }
.v3-scrollcue svg { width: 16px; height: 16px; }
.v3-scrollcue:hover { color: var(--chalk); }

/* Ghost button for dark bands. site.css owns .btn and .btn-primary; this
   inherits every size, radius and focus rule from .btn. */
.btn-ghost-light { background: transparent; color: var(--chalk);
  border: 1px solid rgba(242,237,248,0.38); }
.btn-ghost-light:hover { border-color: var(--chalk); background: rgba(242,237,248,0.08); }

/* -------------------------------------------------------------- masthead -- */
/* THE BAR READS WHAT IS BEHIND IT. His ask, 2026-08-26: *"in dark areas header
   should turn glass, in white areas its purple"*. Every top-level section on
   this page carries `data-mast="dark"` or `"light"`, home.js watches which
   one is under the bar, and writes the answer to `data-tone` on the header.

   ⚠ BOTH TONES ARE DARK BEHIND THE TYPE. Glass over a dark section, packaging
   violet over a paper one — so the brand mark, the links and the CTA are white
   in both states and only the plate behind them changes. That is why the type
   rules below key off `[data-tone]` with no value rather than off one state.

   ⚠ THE ELEMENT SELECTOR IS DELIBERATE. `header.v3-mast[data-tone]` is one
   element more specific than site.css's `.masthead.is-stuck`, which paints the
   bar flat violet the moment the page scrolls. Equal specificity would leave
   this working only by source order, which is true today and is one stylesheet
   reshuffle away from not being.

   ⚠ NO-JS RESTING STATE STAYS VIOLET. With `data-tone` never written the rule
   below is all that applies, the bar is deep violet over the dark hero, and the
   page looks the way it did before any of this — no invisible white-on-paper
   nav to fall into. */
.v3-mast { background: var(--violet-dp); border-bottom-color: rgba(242,237,248,0.14); }
.v3-mast .brand { color: #fff; }

/* Over anything dark: glass. */
header.v3-mast[data-tone="dark"] {
  background: rgba(14,7,28,0.42);
  -webkit-backdrop-filter: blur(9px);
  backdrop-filter: blur(9px);
  border-bottom-color: rgba(242,237,248,0.12);
}
/* Firefox before 103 and every locked-down webview: no backdrop-filter, so the
   glass would be a 42% veil with the page legible straight through the nav.
   The fallback is not "no blur", it is "not translucent". */
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  header.v3-mast[data-tone="dark"] { background: rgba(14,7,28,0.86); }
}

/* Over anything on paper: the packaging violet, flat. */
header.v3-mast[data-tone="light"] {
  background: var(--violet);
  border-bottom-color: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

header.v3-mast[data-tone] .brand { color: #fff; }
header.v3-mast[data-tone] .navtoggle { color: #fff; border-color: rgba(255,255,255,0.35); }

/* ⚠ 1001px, NOT 901. Below the nav collapse point the links live in a paper
   dropdown under the bar and must keep their ink colours — the same boundary
   site.css guards, and for the same reason. */
@media (min-width: 1001px) {
  header.v3-mast[data-tone] .mainnav a:not(.btn) { color: var(--chalk-dim); }
  header.v3-mast[data-tone] .mainnav a:not(.btn):hover,
  header.v3-mast[data-tone] .mainnav a:not(.btn)[aria-current="page"] { color: #fff; }
  header.v3-mast[data-tone] .btn-primary { background: var(--chalk); color: var(--violet-dp); }
  header.v3-mast[data-tone] .btn-primary::before { background: #fff; }
  header.v3-mast[data-tone] :focus-visible { outline-color: #fff; }
}

/* ==========================================================================
   2. HEADING STAGGER — shared with the grape section
   ⚠ The resting state is set by JS, never here. If the hidden state lived in
   CSS, a heading would be invisible whenever the script failed to run — the
   exact failure this page is built to survive.
   ========================================================================== */
.v3-word { display: inline-block; overflow: hidden; vertical-align: top; }
.v3-word > span { display: inline-block; will-change: transform; }

.v3-h2 { font-family: var(--display); font-weight: 800; color: var(--ink);
  font-size: clamp(30px, 3.8vw, 54px); line-height: 0.98; letter-spacing: -0.025em;
  margin: 0 0 20px; }
.v3-h2-light { color: var(--chalk); }

/* ==========================================================================
   2b. THE DESCENT — space to vineyard, five planes
   ⚠ EVERY RESTING STATE HERE IS THE *ARRIVED* STATE. The layers are written in
   their final positions and home.js moves them BACKWARDS to the start of the
   scrub before it plays. That order matters: with JavaScript off, with GSAP
   unavailable or under reduced motion, what is on screen is the finished
   composite — Samarkand under a dawn sky, framed by vine leaves — rather than a
   stack of planes parked off-frame waiting for a script that never came.
   ========================================================================== */
/* ⚠ THE GLOBE'S 72px BOTTOM MARGIN IS CANCELLED HERE, AND ONLY HERE. site.css
   gives `.globe3d` a `margin-bottom: var(--sec)` and its comment says exactly
   why: on v1 the section below carries `padding-top: 0`, so without the gap the
   paper starts on the pixel the space ends — and *"the gap has to BE paper"*,
   which is why it is a margin and not padding.

   On v3 the section below is this one, and it is black. A paper margin between
   two black bands is a white stripe across the page in the middle of outer
   space — which is what it looked like. The v1 reasoning is untouched and still
   correct on the page it was written for; it just cannot survive a dark
   neighbour. If anything dark is ever put after the globe on another page, it
   needs this line too. */
.globe3d { margin-bottom: 0; }

.plx { position: relative; background: #02040a; }
/* The scroll track.
   ⚠ 520vh, AND IT WAS 300vh UNTIL 2026-08-27. His pick out of
   `design/descent-options.html`, direction B, against the feedback that the
   beats are hard to read: the fix is time, not shorter copy. A beat's hold is
   0.11 of the timeline either way — what changes is what 0.11 is worth. The
   scene pins for 100vh, so the track travels `height - 100vh`; at 300vh that
   made the hold 198px of scroll at a 900px viewport, two wheel notches for a
   25-word sentence. At 520vh the hold is 0.15 and worth 567px.

   ⚠ IT SLOWS THE PLANES TOO, AND THAT IS WANTED. Every plane tween is
   normalised to the timeline, so the sky, the vineyard and the branch all
   stretch by the same 1.73x. This is also the fix STATUS flagged for leg one
   reading rushed — more scroll, never a shorter box.

   ⚠ THREE NUMBERS MOVE TOGETHER. This, the beat parameters in `home.js`,
   and the screen count in the note on `.c3` below.
   ⚠ `.js` only: without the script there is no scrub, so this track would be
   four empty screens. */
.js .plx { height: 520vh; }

.plx-scene { position: relative; height: 100vh; overflow: hidden;
  background: #02040a; isolation: isolate; }
.js .plx-scene { position: sticky; top: 0; }

.plx-layer { position: absolute; inset: 0; will-change: transform; }
.plx-layer img { display: block; }

/* ⚠ 300% TALL, AND THE NUMBER IS PAIRED WITH TWO IN home.js. The sky is one
   portrait image running from a star field down to a clear horizon, and the
   descent is that image travelling upward past the scene. The height is the
   overhang: home.js starts it at +66.667% ((300-100)/300) so the box top sits
   exactly on the scene top, and lands leg one on 0 so the box bottom sits
   exactly on the scene floor. Change the height and BOTH of those change.

   ⚠ 300 AND NOT 220, AND THE REASON IS `cover`, NOT TASTE. This is a portrait
   image (aspect 0.671) in a box the CSS sizes by the SCENE, so `cover` decides
   which axis it scales by — and at 220% the box was WIDER than the picture, so
   cover scaled by width and silently clipped the bottom 22% of it clean off.
   That clipped strip is the horizon: the whole bottom zone of the art, the part
   that makes the descent land instead of stopping in mid-air. His note,
   2026-08-27: *"where is the rest of the photo, it is cut into 2 halves, use
   full height of the layer 1."*

   The test is `k >= 1.4906 * (viewport W/H)`. At 300% that holds everywhere
   normal — 1920x1017 needs 2.81, a maximised 1920x969 needs 2.95, 2560x1440
   needs 2.65. Cover then scales by HEIGHT, the full picture renders, and it
   crops ~6% off the SIDES instead, which on a sky costs nothing. An ultrawide
   2560x1080 would need 3.53 and still clips — it degrades to the old behaviour
   rather than breaking. */
.plx-sky img { position: absolute; left: 0; top: auto; bottom: 0; width: 100%;
  height: 300%; object-fit: cover; object-position: 50% 0%;
  /* ⚠ THE MASK IS ON THE IMAGE, NOT THE LAYER, AND THAT IS THE WHOLE TRICK.
     The globe scene ends on its own WebGL star field and this layer opens on a
     rendered one; they have different densities and different star sizes, so
     where the two sections meet they used to butt together at a hard horizontal
     line — two skies, visibly not the same sky.

     Fading the top of the IMAGE means the fade travels with it. At the start of
     the descent the image's top edge sits exactly on the top of the scene, so
     the band dissolves into `.plx`'s own #02040a — which is the same black the
     globe ends on — and the seam has nothing to show. By the time the reader is
     in the atmosphere the faded band has travelled up out of the scene and is
     never seen again.

     ⚠ A mask on `.plx-sky` instead would be fixed to the viewport, and would
     therefore still be blacking out the top third of the frame at the END of the
     descent, when that third is dawn sky and vine leaves. */
  /* ⚠ THE TOP FADES, THE BOTTOM MUST NOT. The globe closes on its own WebGL
     star field and this opens on a rendered one — different densities — so at
     the join they used to meet at a hard line; faded, the band dissolves into
     `.plx`'s #02040a, the same black the globe ends on. The mask is on the
     IMAGE so it travels with it: by the time the reader is in the atmosphere
     that band has moved up out of the scene.

     ⚠ A MATCHING FADE ON THE BOTTOM WAS TRIED AND IT IS WRONG. The sky's lower
     edge used to cross into the frame before the vineyard had climbed over it,
     and the fade turned a hard edge into a soft one — which is still a black
     band hanging across the middle of the picture, and his verdict was the
     right one: *"remove this black thing bro, no need for that, we have enough
     space yet in the bottom."* The answer is not to hide the edge but to stop
     it entering the frame at all: the tween now lands the sky's bottom exactly
     on the floor of the scene at 62% of the scroll and only lifts it once the
     vineyard's tree line is high enough to cover it. See home.js. */
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, rgba(0,0,0,.55) 5%, #000 13%);
  mask-image: linear-gradient(180deg, transparent 0%, rgba(0,0,0,.55) 5%, #000 13%);
  /* ⚠ THE ARRIVED STATE IS NOT bottom:0 — IT IS 14% ABOVE IT, AND THIS VALUE
     IS PAIRED WITH THE END OF THE TWEEN IN home.js. Bottom-aligned, the only
     part of the sky ever visible above the vineyard was CLOUD: the frame ended
     on the cloud deck and the open dawn beneath it stayed locked behind layer 2
     for the whole descent. His note, 2026-08-27: *"we are still in the clouds,
     sky is not visible, it is locked behind the layer 2"*.

     Lifting the image 14% of its own height brings the clear sky up into the
     band above the skyline. 14 and not more is the other half of his note —
     *"the bottom of layer one should lay a bit behind of layer 2's top, so the
     end is not visible"*: measured against the vineyard's tree line at 59–63%
     of the scene, 14% leaves the image's bottom edge roughly 65px BELOW it at
     1440, 1920 and 2560, so the end of the picture stays tucked behind the
     trees at every width. Go much past 15% and the edge climbs into the sky. */
  transform: translateY(-10.267%); }

/* Samarkand and the rows. Anchored to the floor of the scene: its own sky was
   keyed away, so what sits above the skyline is the layer behind it. */
/* ⚠ 60% OF THE SCENE, NOT ITS OWN ASPECT. At `height: auto` this layer is 46%
   of the viewport WIDTH, which on a 1440x900 screen stands its top edge at 26%
   of the frame — the monument climbed most of the way up the picture and left
   the sky a strip along the top. His note, 2026-08-27: *"lock layer 2 somewhere
   near the middle of the frame, because it is going too high now"*. At 60% the
   top lands at 40% and the tree line at 59–63% across 1440, 1920 and 2560,
   which is the framing he approved. `object-position: 50% 0` takes the crop off
   the nearest rows at the bottom, which the floor layer covers anyway. */
.plx-vine { top: auto; bottom: 0; height: 60%; }
.plx-vine img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 0; }

/* ⚠ THE PERCENTAGES ARE WHERE THESE SAT IN THE FRAME THEY WERE CUT FROM. Both
   corners came out of one generation and were split by connected components;
   26.63% and 42.35% are their widths in that original, so at these values they
   still read as two ends of the same branch. */
.plx-corner img { width: 100%; height: auto; }
.plx-corner-l { right: auto; width: 26.63%; }
.plx-corner-r { left: auto; width: 42.35%; }

/* The shaded floor of the vineyard. Last in, and the thing that closes the
   band — it rises until it fills the bottom and hands over to the dark section
   underneath.

   ⚠ A FIXED SHARE OF THE SCENE, NOT ITS OWN ASPECT. At `height: auto` this
   layer is 36% of the viewport WIDTH, which on a 1440x900 screen puts its top
   edge at y=386 — directly over the Registan, which the vineyard layer only
   reaches at y=417. The monument disappeared behind the leaves and the band
   read as a hedge. Pinned to a share of the scene's HEIGHT it clears the
   skyline at every size, and `object-position: 50% 0` crops the loss off its
   bottom, which is flat black and holds nothing. */
.plx-floor { top: auto; bottom: 0; height: 34%; }
.plx-floor img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 0; }

/* ===== the descent's three beats of copy ==================================
   Three statements about the company, faded through during the fall.

   ⚠ THE STATIC STACK IS THE DEFAULT AND THE OVERLAY IS THE OVERRIDE, because
   the overlay is the thing that can fail. `descent()` bails on `reduced ||
   narrow || !ST`, so in every one of those cases GSAP never touches these
   elements and CSS is on its own. Written the other way round — hidden by
   default, revealed by script — a reduced-motion reader, a phone, or a failed
   script would get three invisible paragraphs. */
.plx-copy { position: relative; z-index: 2;
  padding: clamp(40px, 6vw, 72px) 0 clamp(48px, 7vw, 84px);
  display: grid; gap: clamp(34px, 5vw, 56px); }
.pc { position: relative; }
.pc-inner { margin: 0 auto; width: min(560px, 86vw); }

/* ⚠ THE HEADING IS `--display` AND THE BODY IS `--body`, BOTH NAMED EXPLICITLY.
   The first version set no font-family at all, so both lines inherited `--body`
   at nearly the same size and the band was the only place on the site where a
   heading was not Bricolage — *"you didnt use design fonts, this font im seeing
   for the first time"*. Note the site's small caps-and-tracking eyebrow is
   `--mono`, not `--body`; that pattern is `.label` in site.css. */
.pc-h { font-family: var(--display); font-weight: 800; color: var(--chalk);
  font-size: clamp(30px, 3.6vw, 52px); line-height: 1.0; letter-spacing: -0.03em;
  margin: 0 0 clamp(10px, 1.1vw, 16px); }
/* The sign-off. Bigger than the other three because it closes the band, and
   `--chalk` like them — the violet was tried on 2026-08-27 and he cut it.

   ⚠ ITS SHADOW IS HEAVIER THAN THE OTHER BEATS' AND THAT IS LOAD-BEARING. It
   comes to rest at dead centre, and at p 0.76-0.98 the centre of the frame is
   luma 162-168 — pale sky over sunlit rows, the brightest ground in the whole
   band. The other three all land on something dark. */
.pc-h-brand { font-size: clamp(38px, 5vw, 72px); }
.pc-tagline { font-size: clamp(16px, 1.7vw, 22px); letter-spacing: 0.005em; }
.pc-h-brand, .pc-tagline {
  text-shadow: 0 1px 2px rgba(2,4,10,0.68),
               0 2px 12px rgba(2,4,10,0.62),
               0 0 44px rgba(2,4,10,0.55),
               0 0 90px rgba(2,4,10,0.4); }

.pc-line { font-family: var(--body); margin: 0;
  font-size: clamp(15px, 1.45vw, 19px); line-height: 1.55;
  color: var(--chalk); opacity: 0.92; text-wrap: pretty; }

/* ⚠ CONTRAST COMES FROM A GLYPH SHADOW, NOT FROM A SCRIM BEHIND THE BLOCK.
   Measured down the descent, the centre of the frame runs from luma 8 (space)
   through 143 (sunlit cloud tops) and back to 95 (blue sky) before climbing to
   162 near the horizon, so one text colour needs help somewhere in there.

   ⚠ A RADIAL SCRIM WAS TRIED FIRST AND IT IS WRONG. Behind beats one and two it
   is genuinely invisible, because space and cloud are textured and hide it. On
   the smooth pale sky at beat three it read as a dark oval floating in clear
   air — a blob with a soft edge is still a blob. Anything that paints an AREA
   will show the moment the backdrop is a clean gradient.

   A shadow on the glyphs paints no area. It gives each letter its own local
   contrast and disappears into the picture everywhere else. */
.pc-h, .pc-line {
  text-shadow: 0 1px 2px rgba(2,4,10,0.55),
               0 2px 10px rgba(2,4,10,0.45),
               0 0 34px rgba(2,4,10,0.38); }

/* The overlay. Only where the scrub actually runs — see the note at the top. */
/* ⚠ EVERY WIDTH SINCE 2026-09-22 (was min-width 901px): the fall runs on a
   phone too, and the phone block below re-sizes the pane and the overlay to
   100dvh and re-pads the beats. */
@media (prefers-reduced-motion: no-preference) {
  /* ⚠ STICKY WITH A NEGATIVE MARGIN, SO IT RIDES THE STICKY SCENE WITHOUT
     LIVING INSIDE IT. `.plx-scene` is `overflow: hidden`, which is fine for a
     child — but a child cannot fall back into normal flow on a phone, and that
     fallback is the whole reason this markup sits outside. Pulling it back by
     one scene-height puts its flow position at the top of `.plx`, and sticky
     then carries it down the 520vh track in step with the scene. */
  .js .plx-copy { position: sticky; top: 0; height: 100vh; margin-top: -100vh;
    padding: 0; display: block; pointer-events: none; }

  /* ⚠ `.pc` IS THE FRAME AND `.pc-inner` IS THE ONLY THING THAT MOVES, AND THE
     SPLIT EXISTS TO KEEP A CSS TRANSFORM OFF THE ANIMATED ELEMENT. Centring
     with `translate(-50%,-50%)` and then tweening `y` on the same element is
     the trap that doubled the sky's lift: GSAP reads the CSS transform back out
     of a computed MATRIX as px and owns it from then on, so its own `y` either
     fights or replaces the centring. Grid does the placement, `.pc-inner`
     carries nothing but what GSAP writes. */
  .js .plx-copy .pc { position: absolute; inset: 0;
    display: grid; align-content: center;
    padding: 0 clamp(48px, 8vw, 150px); }
  .js .plx-copy .pc-inner { margin: 0; width: min(520px, 100%); opacity: 0;
    will-change: transform, opacity, filter; }

  /* His placement, 2026-08-27: right, then left, then centre. Three statements
     in the same spot is a slideshow; moving them makes the fall feel wide. */
  .js .plx-copy [data-pc="1"] { justify-items: end; text-align: right; }
  .js .plx-copy [data-pc="2"] { justify-items: start; text-align: left; }
  .js .plx-copy [data-pc="3"] { justify-items: center; text-align: center; }
  .js .plx-copy [data-pc="3"] .pc-inner { width: min(600px, 100%); }

  /* ⚠ IT RESTS AT DEAD CENTRE AND STARTS FROM NEAR THE FLOOR — the travel is
     GSAP's, not the grid's. It used to rest low, because at p 0.76-0.98 the
     centre of the frame is luma 162-168, the brightest the band ever gets,
     while the lower third is 70-80. Landing it at centre is his call
     (*"let it move towards center and there it locks in"*) and it costs
     contrast, so the sign-off carries a heavier shadow than the other three —
     see `.pc-signoff` below. Do not thin that shadow. */
  /* ⚠ IT RESTS ABOVE CENTRE, NOT ON IT, AND THE OFFSET IS MEASURED. At the
     arrived state the vineyard's tree line sits at ~58.7% of the scene. Resting
     the card at true centre put the tagline's baseline at ~56% — under three
     percent of clearance, so the smaller line sat in the treetops and stopped
     being readable: *"move last text slightly upper, so text is fully visible
     and they dont blend with trees"*.

     `align-content: center` inside a box with bottom padding centres the
     content in what is left, so the card lifts by HALF the padding. ~140px of
     padding at 1009 tall lifts it ~70px, putting the tagline's baseline near
     49% and leaving open sky under it. ⚠ The GSAP tween is untouched by this —
     it still travels to `y: 0`, which is simply a different place now. Do not
     fix this in the tween.

     ⚠ THE CLEARANCE IS NOT THE SAME AT EVERY SIZE, AND 17vh IS SET BY THE WORST
     ONE. The tree line's height is `0.40H + 0.0894W` from the scene top —
     scene HEIGHT for the layer's box, scene WIDTH for the image inside it,
     because the vineyard is `object-fit: cover` fitted by width. So the flatter
     the viewport, the lower the trees sit and the easier this is; the squarer
     it is, the higher they climb. Measured clearance under the tagline at 14vh:
     82px at 1899x1009, 85px at 1920x969, 128px at 2560x1440 — but only **45px
     at 1440x900**, which is the 16:10 laptop case and the one that decides it.
     17vh brings that to ~55px and costs the wider screens nothing they miss. */
  .js .plx-copy [data-pc="4"] { justify-items: center; text-align: center;
    padding-bottom: clamp(110px, 17vh, 210px); }
  .js .plx-copy [data-pc="4"] .pc-inner { width: min(760px, 100%); }
}
/* The seam. The floor layer is near-black at its base but not flat, so this
   carries the last few pixels into the section below without a visible edge. */
.plx-out { position: absolute; left: 0; right: 0; bottom: 0; height: 22%;
  z-index: 2; pointer-events: none;
  background: linear-gradient(180deg, rgba(2,4,10,0) 0%, rgba(2,4,10,.75) 62%, #02040a 100%); }

/* ==========================================================================
   2c. INTEGRATION — the section the descent lands in
   Was `.section` on paper with a 4:5 photograph in a two-column split. His
   note, 2026-08-26: *"i dont really like exposing to white section
   immediately… section itself looks too simple with one sad photo of
   vineyard"*. The photograph's job moved up into the parallax band; this band
   keeps the words and the numbers and starts on the same black the band ends
   on, so paper does not arrive until the caliber belt.
   ========================================================================== */
.intg { background: linear-gradient(180deg, #02040a 0%, #150a2b 46%, #2a1049 100%);
  color: var(--chalk); }
.intg-body { max-width: 760px; }
.intg-lead { color: var(--chalk-dim); font-size: clamp(15px, 1.15vw, 17px);
  line-height: 1.62; max-width: 54ch; margin: 0 0 16px; }
/* Hairline-separated rather than boxed: on paper `.figures` needs its cell
   borders to hold together; on dark the numbers carry themselves and the boxes
   read as clutter. */
.intg-figs { display: grid; grid-template-columns: repeat(4, 1fr); margin-top: 44px;
  border-top: 1px solid rgba(242,237,248,.16); }
.intg-figs div { padding: 22px 20px 0 0; }
.intg-figs b { display: block; font-family: var(--display); font-weight: 800;
  font-size: clamp(26px, 2.6vw, 36px); letter-spacing: -.045em; line-height: 1; color: #fff; }
.intg-figs span { display: block; font-size: 12.5px; color: var(--chalk-dim);
  margin-top: 8px; line-height: 1.4; }
/* ==========================================================================
   ⚠⚠ DORMANT SINCE 2026-09-07 — NOTHING ON THE SITE HAS A `.jr` ANY MORE.
   His call: the home page asked for too much scrolling and a visitor gets
   exhausted before reaching anything he can act on, so the section came out of
   `index.html`. Everything below is kept, un-tidied, ON PURPOSE: putting the
   section back is then a markup move and nothing else. The markup with it still
   in is `.backup-journey/index.html` in the project folder; `journey()` in
   `home.js` is the matching module and bails on a missing `.jr`.
   ⚠ THE SEAM IT USED TO HIDE IS NOW HANDLED BY `#certs` AT THE FOOT OF THIS
   FILE. Restore this section and that ramp has to come out.
   If the decision holds past the next pass, this whole block and that module
   are the tidy-up. Until then, do not "clean up" one and leave the other.
   ========================================================================== */

/* ==========================================================================
   3. THE JOURNEY — eight stages, one at a time
   ⚠ THIS REPLACED `.v3-grape*` ON 2026-08-28 AND ATE TWO SECTIONS WITH IT.
   The scroll-scrubbed grape film is cancelled (*"we dont need to use video"*),
   and this section now carries what 07 STAGES and 10 PACKING used to say. The
   old `.v3-grape*` rules are gone rather than left dormant: nothing on the site
   uses them, `assets/video/grape-journey-*.mp4` was never generated, and a dead
   block that looks live is how the next person wastes an afternoon. Git has it.
   Shape and reasoning: `design/journey-options.html` direction B, adapted from
   era-residence.com; see `design/journey-reference-era.md`.
   ========================================================================== */
/* ⚠ A GRADED GROUND, NOT FLAT INK. First pass painted `--ink` flat and put
   an edge-anchored wedge over it — the wedge was carried over from the
   descent, where it earns its place because the backdrop there is a
   full-bleed photograph that runs from luma 8 to 162. Here the photograph is
   a CONTAINED panel, so the wedge was darkening something already dark and
   doing nothing at all. Removed. This gradient does the job instead: it
   gives the panel a ground to sit on, and it carries the violet the rest of
   the page is built from. */
/* ⚠ THE GRADIENT IS A BRIDGE BETWEEN THE TWO SECTIONS IT SITS BETWEEN, AND
   BOTH ENDS ARE COPIES OF SOMEONE ELSE'S COLOUR. Since the strip moved after
   CERTS on 2026-08-28 this section runs GALLERY → JOURNEY → CERTS with no paper
   anywhere in it, so a self-contained ramp put a visible step at each end.
     top    #02040a  = `.c3`'s background, the black GALLERY ends on
     bottom #37125e  = `--violet-dp`, the flat violet `.dark` (CERTS) starts on
   ⚠ CHANGE EITHER NEIGHBOUR AND CHANGE THIS. They are the same colours written
   twice and nothing enforces it — the same trap `.dark-dusk` carries, and that
   note says so too. The middle stops are the violet the page is built from;
   they are free to move, the two ends are not. */
.jr { position: relative; color: var(--chalk);
      background: linear-gradient(176deg, #02040a 0%, #100c1c 18%,
                                          #1d1330 42%, #2a1449 72%,
                                          #37125e 100%); }
/* ⚠ THE TRACK HEIGHT LIVES HERE, NOT IN JS, AND THAT IS A DIFFERENCE FROM THE
   PITCH BOARD. The board wrote `host.style.height` from the step count, which is
   fine for a board and wrong for a page: it makes the section's SIZE depend on a
   script, so a slow or failed `home.js` leaves a 100vh box with eight
   absolutely-positioned panels stacked inside it.
   100vh of pin + 8 steps x 85vh = 780vh. ⚠ If a stage is ever added or removed,
   this number and the markup move together — nothing computes it. */
.js .jr { height: 780vh; }

.jr-head { position: relative; z-index: 2; padding: clamp(64px,9vh,120px) 0 clamp(40px,6vh,72px); }
.jr-head .wrap { max-width: var(--max); margin: 0 auto; padding: 0 var(--gut); }
.jr-kick { font-family: var(--mono); font-size: 11px; letter-spacing: .16em;
           text-transform: uppercase; color: var(--violet-lt); margin: 0 0 16px; }
.jr-h2 { font-family: var(--display); font-weight: 800; color: var(--chalk);
         font-size: clamp(30px,4.2vw,60px); line-height: 1.0; letter-spacing: -.035em;
         margin: 0 0 18px; max-width: 16ch; }
.jr-lead { font-family: var(--body); font-size: clamp(15px,1.35vw,18px); line-height: 1.62;
           color: var(--chalk); opacity: .82; margin: 0; max-width: 54ch; }
/* ⚠ TWO ROWS, AND THE RAIL OWNS THE SECOND ONE. It used to be
   `position: absolute; bottom: …` over a single centred row, which made
   overlap a matter of arithmetic rather than structure: the panels are
   centred on the SCENE, so a tall one grows symmetrically and eventually
   reaches the rail. The eight-step variant's last panel is 660px — five spec
   rows, a note and a button — and it **collided below a 810px viewport**,
   which is an ordinary laptop once browser chrome is taken off. Measured:
   clear by 44px at 900, by 8px at 820, colliding by 11px at 780 and 47px at
   700. Now the stage is `minmax(0,1fr)` and the rail is `auto`, so the panels
   are centred in what is LEFT and the two can never occupy the same pixels.
   ⚠ `minmax(0, 1fr)`, not `1fr` — a bare `1fr` has `min-height: auto` and
   refuses to shrink below its content, which puts the overlap straight back. */
/* ⚠ EVERY PINNED RULE IS `.js`-GATED, AND THAT IS NOT STYLE — IT IS THE
   DIFFERENCE BETWEEN THIS SECTION WORKING WITHOUT JAVASCRIPT AND BEING
   INVISIBLE. The frames and panels are absolutely stacked at `opacity: 0` and
   the script is what reveals one at a time. With JavaScript off there is no
   script, so ungated those rules leave eight panels piled on one spot at zero
   opacity: a blank 780vh hole where the whole process description used to be.
   Ungated they also break a `.js` page whose GSAP never loads.
   Same convention, and the same reason, as `.js .plx` / `.js .plx-scene`
   above: the flow layout is the DEFAULT and the scene is the enhancement. */
.jr-scene { position: relative; }
.js .jr-scene { position: sticky; top: 0; height: 100vh; overflow: hidden;
            display: grid; grid-template-rows: minmax(0, 1fr) auto;
            /* ⚠ THE COLUMN NEEDS `minmax(0, 1fr)` FOR THE SAME REASON THE ROWS
               DO, and leaving it implicit was a bug. An implicit `auto` column
               is sized to the widest item's MAX-CONTENT — and `.jr-rail` is in
               that column, a flex row of eight step names. At 1024 those names
               want 1,218px, so the column became 1,218px, `.jr-stage` inherited
               it, and the photographs were dragged 154px off the right edge of
               the screen. Found 2026-08-29 by `adapt-probe.mjs`, the first time
               it ran against this page. The rail wraps now — see `.jr-names`. */
            grid-template-columns: minmax(0, 1fr);
            padding-bottom: clamp(18px, 3vh, 32px); }
.jr-stage { position: relative; min-height: 0; }

/* ---- the frames ---------------------------------------------------------
   ⚠ CONTAINED PANEL, NOT FULL BLEED, AND THE REASON IS RESOLUTION. His seven
   approved hero picks export at 900x675. Behind a 1440-wide viewport that is
   a 1.6x upscale — exactly what he rejected on the parallax layers. In a
   panel of ~640px the same file runs at 0.7x and stays crisp. It is also the
   era `apart-type-slide` composition, which was the better of their two. */
.js .jr-media { position: absolute; inset: 0; z-index: 0; }
.js .jr-panels { position: absolute; inset: 0; z-index: 2; }
/* ⚠ THE EDGE AND THE SHADOW ARE LOAD-BEARING. Without them the panel is a
   hard-cut rectangle pasted onto the background — the single thing that made
   the first pass look unfinished. A hairline plus a deep offset shadow is
   what puts it ON the ground rather than over it.

   ⚠ NO COLOURED GLOW, AND THAT WAS A CORRECTION. The first pass added
   `0 0 120px -40px rgba(75,30,120,.9)` — a violet halo — to "lift" the panel.
   It is the default cool-looking move of generated dark UI and it is foreign
   to this site: `site.css` and `home.css` contain **no** zero-offset
   coloured shadow anywhere, and their only two non-black shadows are
   ink-tinted and offset (`0 22px 48px -22px rgba(22,20,28,.38)`). Elevation
   here comes from offset and blur, never from a chromatic halo. The same
   mistake was on `.jr-fill` and was removed with it. */
/* ⚠ CAPPED, BECAUSE THE FLOW LAYOUT IS ALSO THE DESKTOP NO-JS LAYOUT. Left
   to fill the gutters it is 1360px wide at a 1440 viewport, from a 900px
   source — a 1.5x upscale, the exact thing he rejected on the parallax layers.
   760 keeps every frame at or below 1x. */
.jr-frame { position: relative; overflow: hidden;
  width: auto; max-width: 760px; margin: 0 auto 20px; }
.js .jr-frame { position: absolute; top: 50%; right: var(--spine);
            margin: 0; transform: translateY(-50%);
            /* ⚠ 620, NOT 660, AND THE COPY IS 480 NOT 520. At 1440 the spine is
               1200px wide and 520+660 left a 20px gutter between the two
               columns — they read as one crowded block rather than two.
               480+620 leaves 100px, which is the same air the rest of the
               site's two-column splits carry. */
            width: min(44vw, 620px); aspect-ratio: 4 / 3;
            opacity: 0; overflow: hidden; border-radius: var(--r);
            background: #0d0b12;
            box-shadow: 0 2px 0 rgba(242,237,248,.06) inset,
                        0 0 0 1px rgba(242,237,248,.12),
                        0 34px 72px -28px rgba(0,0,0,.85); }
.jr-frame img { width: 100%; height: 100%; object-fit: cover; display: block;
                will-change: transform; }
/* The frames with no photograph.
   ⚠ FIRST PASS RENDERED BLACK AND IT WAS THE WORST THING ON THE PAGE. The
   radial was anchored at 20% 0% with `--violet` only at stop 0, so almost the
   whole 660x495 box showed the #241046 tail and read as an unstyled div —
   on the frame that carries the enquiry button. `--violet` now owns the
   middle of the ramp, and the plate carries the stage numeral so it is a
   TITLE CARD rather than an empty box. A missing photograph has to look like
   a decision. */
.jr-frame.is-plate { background:
    linear-gradient(152deg, #6a2caa 0%, var(--violet) 34%, var(--violet-dp) 72%, #2a1250 100%); }
.jr-frame.is-plate::before { content: ''; position: absolute; inset: 0; z-index: 1;
    background-image:
      repeating-linear-gradient(90deg, rgba(242,237,248,.10) 0 1px, transparent 1px 52px),
      repeating-linear-gradient(0deg,  rgba(242,237,248,.10) 0 1px, transparent 1px 52px);
    mask-image: radial-gradient(90% 90% at 70% 30%, #000 0%, transparent 78%); }
.jr-plate-n { position: absolute; z-index: 2; right: 6%; bottom: -4%;
    font-family: var(--display); font-weight: 800; line-height: .8;
    font-size: clamp(140px, 20vw, 300px); letter-spacing: -.06em;
    color: rgba(242,237,248,.14); }
.jr-plate-l { position: absolute; z-index: 2; left: 8%; top: 10%;
    font-family: var(--mono); font-size: 11px; letter-spacing: .18em;
    text-transform: uppercase; color: rgba(242,237,248,.72); }


/* ---- the copy ---------------------------------------------------------- */
.jr-panel { position: relative; padding: 0 var(--gut) 54px; }
.js .jr-panel { position: absolute; top: 50%; left: var(--spine);
            transform: translateY(-50%); padding: 0;
            width: min(38vw, 480px); opacity: 0; }
.jr-eyebrow { font-family: var(--mono); font-size: 11px; letter-spacing: .16em;
              text-transform: uppercase; color: var(--violet-lt); margin: 0 0 18px; }
.jr-name { font-family: var(--display); font-weight: 800;
           font-size: clamp(38px, 4.6vw, 68px); line-height: .96;
           letter-spacing: -.035em; color: var(--chalk); margin: 0 0 20px; }
.jr-line { font-family: var(--body); font-size: clamp(15px, 1.35vw, 18px);
           line-height: 1.62; color: var(--chalk); opacity: .88;
           margin: 0 0 30px; text-wrap: pretty; }

/* ⚠ THE SPEC IS THE POINT OF THE WHOLE REBUILD. It used to be a five-tile
   bento at the bottom of the page — the most useful content on the site, in
   the place a buyer is least likely to reach. Mono label, display value,
   era's `apart-type-slide_data` treatment in our faces. */
.jr-spec { margin: 0; display: grid; gap: 0; border-top: 1px solid rgba(242,237,248,.16); }
.jr-row { display: flex; align-items: baseline; justify-content: space-between;
          gap: 20px; padding: 10px 0; border-bottom: 1px solid rgba(242,237,248,.10); }
.jr-row dt { font-family: var(--mono); font-size: 11px; letter-spacing: .1em;
             text-transform: uppercase; color: var(--chalk-dim); margin: 0; white-space: nowrap; }
.jr-row dd { font-family: var(--display); font-weight: 800; margin: 0;
             font-size: clamp(15px, 1.45vw, 19px); letter-spacing: -.015em;
             color: var(--chalk); text-align: right; }
.jr-cta { margin-top: 26px; }
.jr-cta p { font-family: var(--body); font-size: 14px; line-height: 1.6;
            color: var(--chalk); opacity: .78; margin: 0 0 14px; }
/* Two buttons, 2026-08-28. A WRAPPING ROW, not an inline pair: this panel is the
   narrowest column on the page (it shares the scene with the photograph), and
   "Request samples" + "Explore products" side by side stop fitting well before
   the phone stack takes over. Wrapped, they simply become two rows. */
.jr-cta-btns { display: flex; flex-wrap: wrap; gap: 12px; }

/* ---- the rail = the progress bar ---------------------------------------
   era shows the step names as a dimmed list and the progress as a separate
   hairline. Folded into one here: all eight names, always, with the active
   one lit — so the reader can see the whole process, where they are in it,
   and how much is left, from a single element. */
/* The rail is a progress indicator for a sequence that only exists with the
   script running, so without it there is nothing to indicate. */
.jr-rail { display: none; }
.js .jr-rail { display: block; position: relative; z-index: 3; margin: 0 var(--spine); }
.jr-track { position: relative; height: 2px; background: rgba(242,237,248,.14);
            margin-bottom: 16px; }
.jr-fill { position: absolute; inset: 0; transform-origin: 0 50%; transform: scaleX(0);
           background: var(--violet-lt); display: block; }
/* ⚠ `flex-wrap: wrap`, SO EIGHT NAMES CAN TAKE TWO LINES. Capping the grid
   column above stops the rail stretching the scene; this is what stops it being
   clipped instead. Between 900px — where the whole section becomes a stack —
   and roughly 1200px the eight names genuinely do not fit on one line. */
.jr-names { list-style: none; margin: 0; padding: 0;
            display: flex; flex-wrap: wrap; justify-content: space-between; gap: 10px; }
.jr-names li { font-family: var(--mono); font-size: 11px; letter-spacing: .04em;
               color: rgba(242,237,248,.34); white-space: nowrap;
               transition: color 320ms var(--ease-out); }
.jr-names li b { font-weight: 400; opacity: .6; margin-right: 5px; }
.jr-names li.past { color: rgba(242,237,248,.52); }
.jr-names li.on { color: var(--chalk); }
.jr-names li.on b { color: var(--violet-lt); opacity: 1; }

/* ⚠ A SHORT VIEWPORT IS THE REAL CONSTRAINT HERE, NOT A NARROW ONE. The
   eight-step variant's last panel carries five spec rows, a note and a
   button; the grid above guarantees it cannot reach the rail, but on a 700px
   viewport it would simply be clipped instead. This gives back ~66px — most
   of it from the spec rows, which is the one place tightening costs nothing,
   because they are single lines with generous leading. */
@media (max-height: 840px) and (min-width: 901px) {
  .jr-name { font-size: clamp(32px, 3.6vw, 50px); margin-bottom: 12px; }
  .jr-eyebrow { margin-bottom: 12px; }
  .jr-line { margin-bottom: 20px; }
  .jr-row { padding: 6px 0; }
  .jr-cta { margin-top: 18px; }
  .jr-cta p { margin-bottom: 10px; }
}

/* ---- what a phone and a reduced-motion reader get ----------------------
   ⚠ NOT A DEGRADED VERSION — A STACK. The scene cannot pin on a device whose
   address bar resizes the viewport mid-gesture, and a stepped sequence with
   no scroll to drive it is nothing. Every step becomes an ordinary block,
   photograph over copy over spec, in order. Same decision, same reason, as
   the descent's phone fallback. */
/* ⚠⚠ EVERY RULE IN THIS BLOCK CARRIES `.js `, AND THAT IS NOT DECORATION.
   The scene rules above are `.js .jr-panel`, `.js .jr-scene` and so on — (0,2,0).
   A bare `.jr-panel` here is (0,1,0), and **a media query adds no specificity**,
   so for as long as this block was written without the prefix the `.js` rules
   won and NONE of it applied. Found 2026-08-29 by `adapt-probe.mjs`, the first
   time it ran against this page: all eight panels painted on top of one another
   on a phone, with the rail overflowing to 1,138px inside a 390px viewport. It
   had been that way since the section was built.
   ⚠ THE PREFIX IS ALSO CORRECT ON ITS OWN TERMS. A reader with no JavaScript
   never gets the scene rules, so there is nothing for this block to undo — the
   base rules are already the flow layout. This block exists to undo `.js`, so
   it should only ever fire with `.js` present.
   ⚠ CHECK THIS WHENEVER A `.js`-GATED SCENE GETS A FALLBACK. PRODUCTS was
   written with the prefix; DESCENT had two rules missing it, fixed in the same
   pass. */
@media (max-width: 900px), (prefers-reduced-motion: reduce) {
  .js .jr { height: auto; }
  .js .jr-scene { position: relative; height: auto; overflow: visible; display: block;
                  padding: clamp(48px,8vw,80px) 0; }
  .js .jr-scene::before { display: none; }
  .js .jr-media { position: static; }
  .js .jr-frame { position: relative; top: auto; right: auto; transform: none;
                  width: auto; margin: 0 var(--gut) 20px; opacity: 1 !important; }
  .js .jr-panels { display: block; }
  .js .jr-panel { position: relative; top: auto; left: auto; transform: none;
                  width: auto; opacity: 1 !important; padding: 0 var(--gut) 54px; }
  .js .jr-rail { display: none; }
  /* ⚠ The frames and the panels are two separate lists in source order, so on
     a phone they would stack as four pictures then four blocks. Grid puts
     each frame next to its own panel instead. */
  .js .jr-scene { display: grid; }
  .js .jr-stage, .js .jr-media, .js .jr-panels { display: contents; }
  .jr-frame[data-frame="0"] { order: 1 } .jr-panel[data-panel="0"] { order: 2 }
  .jr-frame[data-frame="1"] { order: 3 } .jr-panel[data-panel="1"] { order: 4 }
  .jr-frame[data-frame="2"] { order: 5 } .jr-panel[data-panel="2"] { order: 6 }
  .jr-frame[data-frame="3"] { order: 7 } .jr-panel[data-panel="3"] { order: 8 }
  .jr-frame[data-frame="4"] { order: 9 } .jr-panel[data-panel="4"] { order: 10 }
  .jr-frame[data-frame="5"] { order: 11 } .jr-panel[data-panel="5"] { order: 12 }
  .jr-frame[data-frame="6"] { order: 13 } .jr-panel[data-panel="6"] { order: 14 }
  .jr-frame[data-frame="7"] { order: 15 } .jr-panel[data-panel="7"] { order: 16 }
}

/* ==========================================================================
   RESPONSIVE
   ⚠ Split by input method, not by "small". The scrub is a POINTER affordance:
   seeking a video from a touch scroll is unreliable across iOS versions and a
   stuttering film is worse than a still one. The CONTENT does not change — the
   same four beats are on the page, stacked and all readable.
   ========================================================================== */


@media (max-width: 900px) {
  /* ⚠ height RETURNS TO auto HERE. The desktop rule pins the hero to exactly
     one screen so the monuments stand on the bottom edge; a phone has no such
     luxury — a small screen in landscape, a large text setting or a translated
     line can push this block past the viewport, and with `height` fixed and
     `overflow: hidden` above, the CTAs would simply be cut off. Growing is the
     correct failure here. */
  .v3-hero { padding-top: calc(var(--mast-h) + 34px); height: auto; min-height: 100svh; }
  .v3-h1 { max-width: none; }
  .v3-lead { max-width: none; }
  .v3-scrollcue { display: none; }

  /* ⚠ THE WHOLE PANORAMA, SMALL — DO NOT ZOOM IN ON A PHONE. Scaling it to
     190% and bleeding it off both edges was tried first, reasoning that a
     130px-tall scene is too small to read. It is the wrong crop: Samarkand and
     Paris are at the two ENDS of this picture and everything between them is
     open country, so zooming in throws away both landmarks and leaves a phone
     hero showing a road through empty fields.

     ⚠ 100%, NOT the 106% used above, and not 104 either. The Eiffel's base runs
     to the very last column of the file, so ANY overhang slices the tower down
     its right side — at 512px wide even 4% cuts ten visible pixels off it. The
     overhang exists solely to absorb the pointer drift, and there is no pointer
     here: `arcDrift()` returns on `narrow` before it binds anything. Nothing
     moves, so nothing needs absorbing. */
  .v3-arc { width: 100%; }
  .v3-hero-wrap { padding-bottom: clamp(110px, 26vw, 200px); }

  /* ⚠ THE DESCENT DOES NOT SCRUB ON A PHONE. Its scene is 100vh of stacked
     full-bleed imagery and its track is 520vh — five screens of scrolling on
     a device where the address bar resizes the viewport mid-gesture. It
     collapses to the arrived composite at a readable height, which is what the
     no-JS path shows anyway. */
  /* ⚠ THE FALL RUNS ON A PHONE SINCE 2026-09-22 (his second note on the band;
     the story-card layout of the same day is gone). Same scene, same
     timeline, same overlay as the desktop: the pane and the overlay are
     100dvh so they follow the address bar, the track is 400vh (a 0.15 hold
     is ~45vh of scroll, about a thumb's swipe), and the beats sit on the
     gutter. A phone with reduced motion still falls through to the block
     below: the composite, still, and the three paragraphs. */
  .js .plx { height: 400vh; }
  .js .plx-scene { position: sticky; top: 0; height: 100svh; height: 100dvh; }
  .js .plx-copy { height: 100svh; height: 100dvh; margin-top: -100svh; margin-top: -100dvh; }
  .js .plx-copy .pc { padding: 0 var(--gut); }
  .js .plx-copy [data-pc="4"] { padding-bottom: 16dvh; }
  .pc-h { font-size: clamp(28px, 8vw, 36px); }
  .pc-h-brand { font-size: clamp(36px, 10vw, 44px); }
  .intg-figs { grid-template-columns: repeat(2, 1fr); }

  /* The JOURNEY's stack rules live with its own block in section 3 — same
     reason as the note in the width-only query above. */
}

@media (max-width: 620px) {
  .v3-cta .btn { width: 100%; justify-content: center; }
  .v3-counter { display: flex; }
}

/* ==========================================================================
   REDUCED MOTION
   Final states immediately, not shortened animations. JS kills every scrub and
   the parallax; these rules cover the CSS half and the case where the script
   never ran at all.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  /* The descent is a scrub and there is no shortened version of one. The track
     collapses and the arrived composite stands still. */
  .js .plx { height: auto; }
  .js .plx-scene { position: relative; }
  .plx-layer { transform: none !important; }

  .v3-word > span { transform: none !important; }
  .v3-arc-img { transform: none !important; }

  /* ⚠ THE JOURNEY'S OWN PHONE RULES LIVE WITH ITS BLOCK, not here, because they
     are a `(max-width: 900px), (prefers-reduced-motion: reduce)` pair and this
     query is width-only. A reduced-motion reader on a desktop needs the stack
     too, and putting half of it here would have given them a pinned scene with
     no scrub — one frame, seven invisible ones. See section 3. */
  .v3-scrollcue svg { display: none; }
}


/* ===== section three, Direction C — four beats, four photographs ===========
   Ported from `design/section3-options.html` on 2026-08-27. The static version
   it replaces (`.intg`, `.intg-figs`) is deliberately left in this file, so a
   revert is a markup swap rather than a rebuild.

   ⚠ THE SCRUBBED SECTIONS ARE NOW CONSECUTIVE AND THAT IS A PAGE-LEVEL
   JUDGEMENT, NOT A BUG. The globe is 640vh, the descent 520vh, and this is
   380vh — 1,540vh of pinned, scrubbed track back to back before the caliber
   belt gives the reader anything static to stand on. The options board raised
   this against Direction C when the count was three; the descent made it four
   scenes and 13 screens, and choosing direction B on 2026-08-27 — 220vh more
   descent, so the beats can actually be read — took it to ~15. He was shown
   that cost and took it. Recorded so it is a decision someone made rather than
   one that happened. */
/* ⚠ THE BACKGROUND MATCHES THE DESCENT'S TERMINAL COLOUR EXACTLY, not its own
   near-miss. `.plx-out` fades the band above to solid `#02040a`; this used to
   be `#05060f`, close enough to look like a mistake rather than a choice and
   just different enough to draw a line across the join. */
.c3 { position: relative; background: #02040a; }

/* ⚠ THE 45vh DARK LEAD-IN IS GONE, 2026-08-29, HIS CALL: *"remove dark gap
   between products and gallery sections"*. It is worth knowing why it existed
   and why removing it is now safe rather than a reversal of his own judgement.

   HIS ORIGINAL NOTE, 2026-08-27: *"for smoothe transition between section, put
   some dark gap"*. At the time GALLERY followed THE DESCENT — two full-bleed
   photographs meeting on the very next pixel, a hard horizontal line. 45vh of
   black was the fix, and he asked for more than the 30 it started at.

   WHAT CHANGED: PRODUCTS now sits between them. It ends on a full screen of
   #02040a — stage 05's plate fades to it deliberately, see `home.js` — so
   the gallery no longer arrives out of a photograph. It arrives out of a
   screen of black that is already there. The gap became 45vh of a SECOND
   nothing, which is what he saw.

   ⚠ THE TOP-EDGE MASK BELOW IS THE OTHER HALF OF THAT ENTRY AND IT STAYS. The
   gap and the mask were built together; the mask is what dissolves the
   photograph's own top edge and it does that work whatever precedes the
   section.

   ⚠ IF PRODUCTS EVER MOVES AGAIN, THE GAP COMES BACK. The moment DESCENT
   touches GALLERY directly, the hard line he complained about returns.

   `box-sizing: border-box` is global, so height and padding were one number:
   425 - 45 was 380vh of content box. It is 380vh flat now and the scene still
   pins for 280vh. Change one and change the other. */
.js .c3 { height: 380vh; }
.c3-scene { position: relative; height: 100vh; overflow: hidden;
  color: var(--chalk); display: grid; align-items: center; }
.js .c3-scene { position: sticky; top: 0; }

/* ⚠ THE TOP-EDGE MASK IS GONE, 2026-08-29, HIS CALL: *"remove black border
   shadow on top of gallery section"*. Read this before putting it back, because
   the reasoning that built it was sound and the reason it had to go is not
   obvious from the code.

   WHAT IT WAS: an eleven-stop smoothstep on `.c3-media` fading the media's top
   20% to transparent, so the photograph dissolved into `.c3`'s #02040a instead
   of meeting it on a straight horizontal line. Paired with the 45vh dark
   lead-in, both from his 2026-08-27 notes — *"smooth entering to the gallery"*,
   *"soften transition, make it smoooth"*.

   WHY IT HAD TO GO: `.c3-media` is `inset: 0` of a PINNED scene, so the mask is
   20% of the VIEWPORT and it does not travel. The comment that used to sit here
   admitted as much — "once it settles this reads as a vignette under the
   masthead. That is wanted." It was not: on a 900px screen it is 180px of
   permanent black across the top of every one of the four photographs, and with
   the lead-in also removed it is the first thing the section shows.

   WHAT STILL DOES THE JOB: the scene-relative opacity fade in `home.js`,
   which brings the whole media up from 0 over one viewport of approach. The
   media arrives out of PRODUCTS' closing screen of #02040a, so there is no
   bright edge for a hard line to form against.

   ⚠ THE MASTHEAD DOES NOT NEED IT. It carries its own `backdrop-filter` and
   background and is glass over this band either way. */
.c3-media { position: absolute; inset: 0; z-index: 0; }
.c3-shot { position: absolute; inset: 0; opacity: 0; }
.c3-shot:first-child { opacity: 1; }
.c3-shot img { width: 100%; height: 100%; object-fit: cover; }

/* ⚠ THE VEIL IS ASYMMETRIC AND THAT IS WHAT MAKES THE TYPE READABLE. The copy
   sits left; the photograph has to survive on the right. A flat scrim over the
   whole frame would either drown the picture or lose the text. */
/* ⚠ LIGHTENED 2026-08-27 — *"our photos being bright, they seem dark in the
   gallery"*. The first values came off the pitch board, where the frames were
   dim stock crops that could take a heavy scrim. His own photography is bright
   and the old ramp buried it: even at its clearest point it still carried 22%
   black, and the right EDGE went back up to 50%, so the picture never actually
   came through anywhere.

   ⚠ WHAT MUST NOT MOVE IS THE LEFT END. The numerals and the body copy sit in
   the left ~40%; that end stays near-opaque or the type goes. Everything from
   ~58% rightwards is what got opened up, and the right edge now settles at 16%
   instead of 50% so the frame breathes out rather than closing again. */
.c3-veil { position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(90deg, rgba(6,3,15,.92) 0%, rgba(6,3,15,.74) 30%,
    rgba(6,3,15,.30) 58%, rgba(6,3,15,.04) 78%, rgba(6,3,15,.16) 100%); }

.c3-copy { position: relative; z-index: 2; width: 100%; }
.c3-h { font-family: var(--display); font-weight: 800; color: var(--chalk);
  font-size: clamp(24px, 2.4vw, 32px); letter-spacing: -.02em; margin: 0 0 34px; }

/* ⚠ `min-height` HOLDS THE STACK OPEN. Every beat is absolutely positioned so
   they occupy one space; without a floor here the section collapses to the
   height of whichever beat happens to be first. 240px is the tallest of the
   four at the narrowest desktop width. */
.c3-beats { position: relative; min-height: 240px; }
.c3-beat { position: absolute; inset: 0; opacity: 0; }
.c3-beat:first-child { opacity: 1; }
.c3-beat b { display: block; font-family: var(--display); font-weight: 800;
  color: #fff; font-size: clamp(56px, 9vw, 132px); line-height: .9;
  letter-spacing: -.05em; }
.c3-beat em { display: block; font-style: normal; font-family: var(--mono);
  font-size: 11.5px; letter-spacing: .2em; text-transform: uppercase;
  color: var(--violet-lt); margin-top: 18px; }
.c3-beat p { color: var(--chalk-dim); max-width: 40ch; margin: 14px 0 0;
  line-height: 1.6; }

.c3-rail { position: absolute; left: var(--spine); bottom: 46px; z-index: 2;
  display: flex; gap: 8px; }
.c3-rail i { display: block; width: 34px; height: 2px;
  background: rgba(242,237,248,.22); transition: background .35s var(--ease-out); }
.c3-rail i.on { background: var(--violet-lt); }

/* ⚠ THE STATIC STACK IS THE DEFAULT FOR THE SAME REASON IT IS IN THE DESCENT:
   the scrub is the thing that can fail. `section3()` bails on reduced motion,
   narrow, or no ScrollTrigger, and CSS is then alone with four beats that are
   `position: absolute; opacity: 0`. These two blocks are what stop that being
   three invisible paragraphs and one floating number. */
@media (max-width: 900px) {
  /* 30vh of black is a breath on a desktop and a dead screen on a phone, where
     the section is a static stack anyway and there is no pinned scene to
     approach. */
  .js .c3 { height: auto; padding-top: 12vh; }
  .js .c3-scene { position: static; height: auto; padding: 70px 0; }
  .c3-media { position: relative; height: 44vh; margin-bottom: 30px; }
  .c3-veil { background: linear-gradient(180deg, rgba(6,3,15,.15) 0%, rgba(6,3,15,.75) 100%); }
  .c3-beats { min-height: 0; }
  .c3-beat { position: static; opacity: 1; margin-bottom: 40px; }
  .c3-beat:last-child { margin-bottom: 0; }
  .c3-shot { opacity: 0; }
  .c3-shot:first-child { opacity: 1; }
  .c3-rail { display: none; }
  /* ⚠ FOUR PHOTOGRAPHS, ONE PER FIGURE, since 2026-09-22 (his note: the phone
     version "could have been adapted better"). home.js clones each shot into
     its own beat on a phone (`.c3-beat-shot`) and the media block steps
     aside; it was one dimmed photograph over a black gap and a list. */
  .js .c3.c3-stacked { padding-top: 0; }
  .js .c3.c3-stacked .c3-scene { padding: 72px 0 40px; }
  .c3.c3-stacked .c3-media { display: none; }
  .c3.c3-stacked .c3-h { margin-bottom: 26px; }
  .c3.c3-stacked .c3-beat { margin-bottom: 34px; }
  .c3-beat-shot { position: relative; aspect-ratio: 4 / 3; border-radius: 12px; overflow: hidden; margin-bottom: 14px;
                  box-shadow: 0 30px 60px -30px rgba(0,0,0,.9), 0 0 0 1px rgba(242,237,248,.1); }
  .c3-beat-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .c3-beat-shot::after { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(6,3,15,0) 35%, rgba(6,3,15,.85) 100%); }
  .c3-beat-shot b { position: absolute; left: 16px; bottom: 12px; font-family: var(--display); font-weight: 800; font-size: clamp(40px, 12vw, 56px); letter-spacing: -.05em; line-height: 1; color: #fff; text-shadow: 0 6px 30px rgba(0,0,0,.6); }
  .c3.c3-stacked .c3-beat > b { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .js .c3 { height: auto; padding-top: 12vh; }
  .js .c3-scene { position: relative; height: auto; padding: clamp(64px, 8vw, 104px) 0; }
  .c3-media { position: relative; height: 46vh; margin-bottom: 34px; }
  .c3-veil { background: linear-gradient(180deg, rgba(6,3,15,.15) 0%, rgba(6,3,15,.75) 100%); }
  .c3-beats { min-height: 0; }
  .c3-beat { position: static; opacity: 1; margin-bottom: 40px; }
  .c3-beat:last-child { margin-bottom: 0; }
  .c3-rail { display: none; }
}

/* ============================================================================
   07 · PRODUCTS — five varieties, one stage each              2026-08-29
   ============================================================================
   His choreography, from `design/products-options.html` option A, ported here
   on *"move it to the v3"*. It REPLACES the five-cell hover belt on this page.

   ⚠ `site.css`'s `.calibers` / `.caliber` RULES ARE STILL THERE AND ARE NOW
   DEAD ON THE SHIPPING SITE. Until 2026-08-29 the reason not to touch them was
   that the belt was the product section on the live `index.html`. **That page
   is `archives/nnp-raisins-home-superseded/index-v1.html` now and nothing
   served references the belt.** They were left in place anyway: it is a
   finished component whose notes carry a hard-won input-method split, and
   deleting ~200 lines of working CSS is a separate decision from archiving the
   page that used it. **If you do delete them, check the archive is the only
   remaining reference first.** Everything below is `.pr-*` and lives here.

   ⚠ THE CASCADE IS: FLOW BY DEFAULT, SCENE UNDER `.js`, FLOW AGAIN UNDER
   `(max-width: 900px)` / `prefers-reduced-motion`. Same three-step shape as
   JOURNEY, and it is what makes the section readable with JavaScript off. The
   base rules ARE the no-JS layout — a plain stack of five product blocks — and
   every `position: absolute` that would collapse that stack into one pile is
   `.js`-prefixed. If you add a rule here, decide which of the three it belongs
   to before you write it.

   ⚠ THE FIVE GROUND COLOURS ARE ON THE MARKUP, as `--stage` per stage, because
   they are content-adjacent: each one belongs to a variety, not to a rule. The
   companion `--pc` is a light accent read off that pile and it drives the
   numeral, the card borders and the glow. */

/* ⚠ #02040a IS THE SAME BLACK `.plx` AND `.c3` PAINT, AND THAT IS THE WHOLE
   REASON THIS SECTION SITS WHERE IT DOES. His call, 2026-08-29: *"tail of
   descent is black, keep it black and as we enter the product section it becomes
   purple"*. DESCENT hands over to PRODUCTS with no seam and GALLERY takes it
   back the same way; the bloom to violet and the five variety colours happen on
   `.pr-ground`, above this, and only once the scene starts.
   ⚠ IT IS ALSO THE NO-JS AND REDUCED-MOTION GROUND — that stack has no
   `.pr-ground` and no tween, so this black is what the whole section is. Do not
   set it to a variety colour to "match" the scene. */
.pr { position: relative; background: #02040a; color: var(--chalk); }

/* ---- the head ----------------------------------------------------------
   In normal flow, so it scrolls away before the scene pins.
   ⚠ ONE CENTRED LINE. His call 2026-08-29: *"just headline in the center that
   fades up"*. It replaced an eyebrow, a two-line heading and a lead — and
   `.pr-kick` / `.pr-lead` went with them, so do not look for them.
   ⚠ THE FADE-UP IS `data-reveal` ON THE MARKUP, site.css's own. Nothing here
   animates it and nothing here should: a transform in this rule would fight the
   one the reveal is tweening.
   ⚠ THE PADDING WAS clamp(96px,17vh,190px) AND HE SENT IT BACK. At 17vh a side
   it made the head 453px — half a screen of nothing but one line, which is what
   he meant by *"you gave it its own section, which made products section
   long"*. It is a HEADING for the scene below it, not a beat of its own, so it
   is now sized like one. Measured: 453px → about 210px at a 900px viewport.
   ⚠ THE SECTION IS SHORTER TOO, 520vh → 460vh — see `.js .pr` below. Trimming
   the head alone would have left the section the same length and simply given
   the scene the slack, which is not what he asked for. */
.pr-head { position: relative; z-index: 2;
           padding: clamp(52px,7vh,92px) 0 clamp(46px,6vh,80px); }
/* ⚠ INSIDE THE SCENE SINCE 2026-09-22 (his call, see the markup): centred on
   the black pane as the scene's first beat, dissolved by the timeline. The
   flow rule above is the no-JS shape, where the head simply sits above the
   stack. z 6 puts it over the ground and every stage; pointer-events none so
   it never blocks stage 01's links once it has faded. */
.js .pr-head { position: absolute; inset: 0; z-index: 6; display: grid; place-items: center;
               padding: var(--mast-h) var(--gut); pointer-events: none; will-change: transform, opacity; }
.pr-h2 { font-family: var(--display); font-weight: 800; color: var(--chalk);
         font-size: clamp(32px,4.4vw,62px); line-height: 1.02; letter-spacing: -.04em;
         margin: 0 auto; max-width: 15ch; text-align: center; }

/* ---- the scene ---------------------------------------------------------- */
/* ⚠ 440vh. 520 → 460 on 2026-08-29 (*"the section read as long"*), 460 → 440
   on 2026-09-07 with the entry retime — see the long note in `home.js`. The 20
   is not a second trim for its own sake: the timeline lost 0.25s at the front,
   and the height has to lose the matching scroll or the whole section simply
   slows down. **The two move together, and the ratio to hold is about 72vh of
   scroll per timeline second** (height minus the 100vh of pin, over the
   timeline's duration). Do not raise it back without asking: the page already
   runs 20 screens of pinned scene before anything sits still. */
/* 455 since 2026-09-22: the head's 0.2s beat at the front (72vh/s, as below). */
.js .pr { height: 455vh; }
.js .pr-scene { position: sticky; top: 0; height: 100vh; overflow: hidden; }

/* ⚠ THE GROUND IS A SEPARATE ELEMENT FROM THE STAGES. Stages 01 and 02 share
   it and paint no plate of their own, so 02 can rise ONTO 01's stage while the
   colour cross-fades underneath both. Stages 03-05 bring their own opaque
   plate and are the overlay. Tweening `background-color` on the stage itself
   cannot do both jobs.
   ⚠ IT STARTS ON THE DESCENT'S BLACK, NOT ON VIOLET. It used to start on
   `--violet-dp` because this section followed CERTS. It follows DESCENT now, and
   the first thing the timeline does is bloom black → violet → the first
   variety's colour. **The literal is repeated in `home.js`** — GSAP cannot
   tween a `var()`, so the start of the tween is written there as `#02040a` too.
   Change both together or the ground jumps on the first frame. */
.pr-ground { display: none; }
.js .pr-ground { display: block; position: absolute; inset: 0; background: #02040a; }
/* A bloom so the flat ground has a centre. Achromatic white at 9%, NOT tinted:
   the ground beneath is already the product colour and tinting the bloom too
   doubles the saturation into mud. */
.js .pr-ground::after,
.js .pr-stage.is-panel .pr-plate::after {
  content: ''; position: absolute; inset: 0;
  background: radial-gradient(62% 58% at 50% 46%, rgba(255,255,255,.09), transparent 70%); }

.js .pr-stages { position: absolute; inset: 0; }

/* ⚠ `isolation: isolate` IS THE WHOLE OVERLAY, and it is the one bug that cost
   a build. A stage is `position: absolute` with NO z-index, so it creates no
   stacking context: its children's z-indexes (pile 2, copy 3, cards 4) escape
   into the parent and beat the NEXT stage's opaque plate, which is at z-index
   auto. The symptom is spectacular — stage 03 slides in, paints the ground
   amber, and all five stages of type print straight through it. One property
   makes each stage a sealed layer so plain DOM order decides what is on top. */
/* ⚠ `--panel` IS THE FLAT-STACK PAINT AND IT IS NAMED SO IT IS WRITTEN ONCE.
   It is what a no-JS reader and a reduced-motion reader see instead of the
   ground's tween: the variety's own colour with the same white bloom the scene
   puts over it. The scene itself must NOT use it — `.pr-ground` cross-fades
   between colours under stages 01 and 02, and a colour on the stage would sit
   on top of that fade and freeze it. Hence `background: none` below. */
.pr-stage { position: relative;
            --panel: radial-gradient(78% 44% at 50% 28%, rgba(255,255,255,.09), transparent 70%),
                       var(--stage);
            background: var(--panel);
            padding: clamp(48px,9vw,76px) var(--spine) clamp(56px,8vh,90px); }
.js .pr-stage { position: absolute; inset: 0; padding: 0; background: none;
                display: grid; place-items: center; isolation: isolate; }
.js .pr-stage.is-panel { will-change: transform; }

.pr-plate { display: none; }
.js .pr-stage.is-panel .pr-plate { display: block; position: absolute; inset: 0;
                                   background: var(--stage); }

/* ---- the pile ----------------------------------------------------------- */
.pr-pile { margin: 0 auto 26px; max-width: 420px; }
.js .pr-pile { position: relative; z-index: 2; margin: 0; max-width: none; }
.pr-pile img { display: block; width: 100%; height: auto; }
/* ⚠ TWO SHADOWS, AND THE COLOURED ONE IS DELIBERATE ON THIS ELEMENT ONLY. The
   JOURNEY notes ban chromatic halos on this site — "elevation comes from offset
   and blur, never from a coloured glow" — and that ban stands for panels, cards
   and buttons. This one is not elevation, it is a CAST: light thrown by an
   object that really is that colour, onto a ground that is a darker version of
   the same colour. The tight black shadow is what actually sits it down. */
/* ⚠ NO `filter` ON THE IMAGE ANY MORE, 2026-09-23 (Ravshan: "the site became
   very heavy, even on a Mac"). Two drop-shadows on a 1000px alpha image that
   scales and travels every frame is a full re-raster of the filtered image
   per frame, at Retina four times over. The shadow is a pseudo-element under
   the pile, which the compositor moves for free.
   ⚠ AND NO COLOURED CAST ANY MORE EITHER, the same evening. A drop-shadow
   follows the silhouette, so the old coloured glow read as light thrown by
   the pile; a radial gradient cannot follow anything, so the same colour
   became a lit disc 130% the width of the pile — a plate under every
   variety, on the live site (his five screenshots: "look how ugly this
   looks"). What survives is the sit-down shadow alone: dark, the pile's own
   size, offset down, so past the pile's edge only the soft falloff shows.
   If the cast is wanted back, bake it into the cutout WebPs
   (`tools/build-product-cutouts.py`) — that is the only free way to get a
   silhouette-shaped glow. */
.js .pr-pile img { width: min(46vw, 520px); position: relative; z-index: 1; }
.js .pr-pile::before { content: ''; position: absolute; left: 50%; top: 56%; width: 104%; height: 104%; transform: translate(-50%, -50%); border-radius: 50%; z-index: 0; pointer-events: none;
  background: radial-gradient(circle at 50% 50%, rgba(0,0,0,.55) 0%, rgba(0,0,0,.42) 40%, rgba(0,0,0,.18) 56%, transparent 70%); }

/* ---- the copy ----------------------------------------------------------- */
/* ⚠ `max-content`, NOT `22ch`. `.pr-name` is `white-space: nowrap` — it has to
   be, the `<br>` does the breaking — so a fixed measure does not wrap it, it
   lets it ESCAPE the box: measured 239px of name in a 215px column at 1024, and
   10px over at 1440. `ch` is the wrong unit here twice over, because it is
   computed from the BODY font on `.pr-copy` while the thing overflowing is a
   62px display face. Sizing to the content cannot overflow; the cap is there so
   a long variety name can never reach the pile. */
.js .pr-copy { position: absolute; z-index: 3; left: var(--spine); top: 50%;
               transform: translateY(-50%);
               width: max-content; max-width: min(38vw, 520px); }
.pr-idx { font-family: var(--mono); font-size: 12px; letter-spacing: .16em;
          color: var(--pc); margin: 0 0 18px; }
.pr-idx i { font-style: normal; opacity: .5; }
/* ⚠ `nowrap`, AND EVERY NAME CARRIES ITS OWN `<br>`. "Sultana Sun-Dried" is the
   only name in the range with a hyphen, CSS treats a hyphen as a break
   opportunity, and at 74px it came back as "Sultana / Sun- / Dried". The `<br>`
   puts the turn where it belongs; this stops the browser adding a second one.
   Same fix and same reason as `.caliber-name .nb` in site.css. */
.pr-name { font-family: var(--display); font-weight: 800; color: #fff;
           font-size: clamp(34px,5.4vw,74px); line-height: .95; letter-spacing: -.04em;
           margin: 0; white-space: nowrap; }
.pr-sub { font-family: var(--mono); font-size: 12px; letter-spacing: .1em;
          text-transform: uppercase; color: rgba(255,255,255,.5); margin: 18px 0 0; }
.pr-cta { display: inline-flex; align-items: center; gap: 9px; margin-top: 28px;
          font-family: var(--mono); font-size: 12px; letter-spacing: .08em;
          text-transform: uppercase; text-decoration: none; color: #fff;
          border-bottom: 1px solid color-mix(in srgb, var(--pc) 60%, transparent);
          padding-bottom: 6px; }
.pr-cta span { display: inline-block; transition: transform 260ms var(--ease-out); }
.pr-cta:hover span, .pr-cta:focus-visible span { transform: translateX(5px); }

/* ---- the floating cards -------------------------------------------------
   Glass, his brief. ⚠ `backdrop-filter` OVER A FLAT COLOUR DOES NOTHING — there
   is no detail behind it to blur — so what actually reads as a pane here is the
   white film plus the inset top highlight. The blur still earns its keep over
   the pile, which IS detailed, and that is where a card overlaps it.
   ⚠ THE COLOURED GLOW IS HIS EXPLICIT ASK and this is the one place a chromatic
   halo is allowed on this site — the JOURNEY notes ban them everywhere else and
   that ban stands. It is kept weak on purpose: 26px, negative spread, and the
   border does most of the work. */
/* ⚠ THE BASE STATE IS THE SPEC ROW, NOT `display: none`. The first build hid
   the cards outside `.js` and the no-JS render came back with the product names
   and NO NUMBERS — caliber, berry count and moisture all gone, which is the
   entire reason a buyer is on this section. Shot and caught. The flow version
   below is a plain definition row; `.js` promotes it to the floating pane. */
.pr-card { display: flex; justify-content: space-between; gap: 20px;
           padding: 11px 0; border-bottom: 1px solid rgba(242,237,248,.16); }
.js .pr-card { display: block; position: absolute; z-index: 4;
  padding: 15px 20px 17px; border-radius: 12px;
  background: rgba(255,255,255,.07);
  -webkit-backdrop-filter: blur(16px) saturate(120%);
  backdrop-filter: blur(16px) saturate(120%);
  border: 1px solid color-mix(in srgb, var(--pc) 46%, transparent);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.22),
              0 0 26px -6px color-mix(in srgb, var(--pc) 55%, transparent),
              0 20px 44px -26px rgba(0,0,0,.85); }
.pr-card .k { font-family: var(--mono); font-size: 10px;
              letter-spacing: .16em; text-transform: uppercase;
              color: rgba(255,255,255,.58); align-self: center; }
.js .pr-card .k { display: block; align-self: auto; margin-bottom: 7px; }
.pr-card b { font-family: var(--mono); font-weight: 500; font-size: 15px;
             letter-spacing: -.01em; color: #fff; }
.js .pr-card b { font-size: 20px; }
/* Placed off the SPINE so they orbit the pile rather than the viewport edge. */
.js .pr-card--a { right: calc(var(--spine) + 6vw); top: 20%; }
.js .pr-card--b { right: var(--spine); top: 47%; }
.js .pr-card--c { right: calc(var(--spine) + 11vw); top: 71%; }

/* ---- the rail ----------------------------------------------------------- */
/* A progress indicator for a sequence that only exists with the script running,
   so without it there is nothing to indicate. Same rule as JOURNEY's. */
.pr-rail { display: none; }
.js .pr-rail { display: flex; position: absolute; z-index: 5;
               left: 50%; transform: translateX(-50%);
               bottom: clamp(20px,3.4vh,34px); gap: 26px;
               list-style: none; margin: 0; padding: 0; }
.pr-rail li { font-family: var(--mono); font-size: 12px; color: rgba(255,255,255,.32);
              position: relative; padding-bottom: 10px; transition: color 260ms ease; }
.pr-rail li::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0;
                     height: 2px; background: currentColor; transform: scaleX(0);
                     transform-origin: 0 50%;
                     transition: transform 300ms var(--ease-out); }
.pr-rail li.on { color: #fff; }
.pr-rail li.on::after { transform: scaleX(1); }

/* ---- what a phone and a reduced-motion reader get -----------------------
   ⚠ NOT A DEGRADED VERSION — A STACK, and the same decision as JOURNEY's. The
   scene cannot pin on a device whose address bar resizes the viewport
   mid-gesture, and five overlaid panels with no scroll to drive them is
   nothing. Every variety becomes an ordinary block: pile, name, specs, link.
   ⚠ IT MUST UNDO EVERY `.js` RULE ABOVE THAT TAKES AN ELEMENT OUT OF FLOW.
   Anything left `position: absolute` here lands five products on one pile.
   ⚠ `!important` ON opacity/visibility/transform IS NOT LAZINESS — those three
   are written to the element's STYLE attribute by GSAP before this media query
   is ever consulted, and an inline style beats any selector. A reader who turns
   reduced motion on mid-session, or a phone rotated past 900px and back, would
   otherwise be left with four invisible products. */
/* ============================================================================
   04 · PRODUCTS — THE SCENE, ON A PHONE                        2026-09-07
   ============================================================================
   His question: *"you have just changed background and it is not same scrolling
   animations as in pc version, why can't we just use the same section for
   mobile version?"*. We can, and this is it — the same pinned, scrubbed
   section, the same five arrivals in the same order, re-composed for a portrait
   screen. It replaces the coloured stack shipped earlier the same day, which
   answered a narrower question than he was asking.

   ⚠ 2026-09-22: THE PANE IS `100dvh` NOW, see the rule below; the paragraph
   that follows is the 2026-09-07 reasoning for `svh`, kept as the record.
  ⚠ THE OLD NOTE SAID THE SCENE "CANNOT PIN ON A DEVICE WHOSE ADDRESS BAR
   RESIZES THE VIEWPORT MID-GESTURE". That was the real objection and it is
   answered rather than ignored: the sticky pane is **`100svh`**, the SMALL
   viewport height, which is the height with the browser chrome showing. It
   never changes as the bar collapses, so the pane cannot grow taller than the
   screen mid-scroll and jump. **Do not "simplify" it back to `100vh`** — that
   is the large viewport, and the pane would be an address bar taller than what
   the reader can see, which is exactly the fault the note described.
   The TRACK stays in `vh`; it is only scroll distance, and its length changing
   by a few percent is invisible.

   ⚠ WHAT CHANGES IS THE COMPOSITION, NOT THE CHOREOGRAPHY. The timeline in
   `home.js` is untouched: same colour bloom, same arrival directions, same
   slide-overs, same rail. Only the arrangement inside a stage is re-laid: the
   desktop puts copy left, pile centre, spec cards floating right — three
   columns that cannot exist at 390px. Here it is one column: pile, name, specs.

   ⚠ EVERY RULE HERE UNDOES A `.js` RULE THAT ASSUMES LANDSCAPE. Miss one and
   the copy sits under the pile at `left: var(--spine)` with the cards off the
   right edge. The list is short on purpose — the stage stays absolute and the
   stages stay stacked, because that IS the scene.

   ⚠ NO `!important` ANYWHERE IN THIS BLOCK, and that is the difference between
   it and the reduced-motion block below. There the three GSAP-written inline
   properties have to be beaten. Here GSAP is the thing doing the work and must
   win. */
@media (max-width: 900px) {
  /* ⚠ THE PANE IS `100dvh` SINCE 2026-09-22, HIS CALL: *"when url interface
     hides dont let content move upper and empty space appear below"*. It was
     `100svh` (the note above), which held its size as the address bar
     collapsed and so left a strip of the section under the pane on every
     phone. `dvh` is the visible height at any moment: the pane grows with
     the bar's collapse and the stage re-centres in it, and nothing is left
     empty. `svh` stays as the fallback for a browser without `dvh`. The
     TRACK stays in `vh`. */
  .js .pr-scene { height: 100svh; height: 100dvh; }

  /* One column, centred in the pane, with room under it for the rail. */
  /* ⚠ THE TOP PADDING IS THE MASTHEAD AND IT IS NOT DECORATION. The bar is
     `position: fixed`, so it does not take part in this pane's centring —
     without an allowance for it the content centres in the WHOLE pane and the
     top of the pile slides under the bar on a short phone. Measured at
     360×640: the pile's top was 26px from the pane's top against a 74px bar. */
  .js .pr-stage { display: flex; flex-direction: column; justify-content: center;
                  place-items: initial; align-items: stretch;
                  padding: var(--mast-h) var(--gut) clamp(56px,9vh,76px); }

  /* ⚠ IN FLOW, NOT CENTRED BY THE GRID. `place-items: center` above shrink-wraps
     every child; the pile came back zero-wide the last time that was left in
     place, which is written up in the reduced-motion block below. */
  .js .pr-pile { margin: 0 auto clamp(14px,2.4vh,22px); width: min(62vw, 300px); }
  .js .pr-pile img { width: 100%; }   /* the shadow is the pseudo-element (see the desktop rule) */

  /* ⚠ `position: static` AND NO `transform` OF OUR OWN. The desktop rule centres
     this with `translateY(-50%)`, and GSAP folds an existing percentage
     translate into its own `y` tween — the summing trap the JOURNEY notes
     describe. Leave it in and every arrival starts half a box high. */
  .js .pr-copy { position: static; transform: none;
                 width: auto; max-width: none; }
  .pr-name { font-size: clamp(30px,8.6vw,38px); }
  .pr-idx { margin-bottom: 10px; }
  .pr-sub { margin-top: 10px; }
  .pr-cta { margin-top: 16px; }

  /* The glass panes become spec rows. Every glass property has to be named —
     none of them is undone by changing `position`. */
  .js .pr-card { display: flex; position: static; justify-content: space-between;
                 gap: 20px; padding: 9px 0; border-radius: 0;
                 background: none; -webkit-backdrop-filter: none; backdrop-filter: none;
                 border: 0;
                 border-bottom: 1px solid color-mix(in srgb, var(--pc) 42%, transparent);
                 box-shadow: none; }
  .js .pr-card--a { margin-top: clamp(12px,2.2vh,20px);
                    border-top: 1px solid color-mix(in srgb, var(--pc) 42%, transparent); }
  .js .pr-card .k { display: inline; margin: 0; align-self: center; }
  .js .pr-card b { font-size: 15px; }

  /* The rail is the only thing telling a reader how many varieties are left
     while the page is not moving. It matters more here than on a desktop. */
  .js .pr-rail { gap: 16px; bottom: clamp(14px,2.4vh,22px); }
}

/* A short phone — an SE, or anything 700px and under — has about 100px of
   slack once the bar and the rail are taken off, and the pile is the only thing
   big enough to give it back. Measured at 360×640: 223px of pile against 510px
   of usable pane. Nothing else shrinks; the numbers and the name stay legible,
   which is the point of the section. */
@media (max-width: 900px) and (max-height: 700px) {
  .js .pr-pile { width: min(52vw, 210px); margin-bottom: 10px; }
  .pr-idx { margin-bottom: 8px; }
  .pr-sub { margin-top: 8px; }
  .pr-cta { margin-top: 12px; }
  .js .pr-card { padding: 7px 0; }
}

/* ⚠ THIS BLOCK IS `prefers-reduced-motion` ONLY AS OF 2026-09-07. It used to
   carry `(max-width: 900px)` as well, and the phone now gets the scene above
   instead. It stays because it is still the right answer for a reader who has
   asked the system for less motion, at any width, and because it is the no-JS
   shape too. It must come AFTER the phone block so that a phone WITH reduced
   motion still lands here. */
@media (prefers-reduced-motion: reduce) {
  .js .pr { height: auto; }
  .js .pr-scene { position: relative; height: auto; overflow: visible;
                  padding: 0 0 clamp(48px,8vw,80px); }
  .js .pr-ground { display: none; }
  .js .pr-head { position: static; padding: clamp(52px,7vh,92px) 0 clamp(46px,6vh,80px); }
  .js .pr-stages { position: static; }
  /* ⚠ `display: flex` + `align-items: stretch`, NOT `display: block`. The first
     build wrote `block` and the stack came back with every element shrink-wrapped
     and centred: the pile measured ZERO wide, the copy 310 of 1360, the three
     cards each their own max-content. `place-items: center` from the scene rule
     above survives a `display` change — it is still in the cascade — and the
     children were laid out against it. Resetting the alignment explicitly is
     what makes this deterministic; overriding `display` alone is not enough.
     ⚠ `place-items` MUST be reset here too. Leaving it and only changing
     `display` is what produced the zero-width pile. */
  .js .pr-stage { position: relative; inset: auto;
                  display: flex; flex-direction: column;
                  place-items: initial; align-items: stretch;
                  opacity: 1 !important; visibility: visible !important;
                  transform: none !important;
                  /* --spine, not --gut: it collapses TO the gutter under
                     900px, so a phone is unaffected, and a reduced-motion
                     desktop keeps the stack on the same spine as the head
                     above it.
                     ⚠ THE TOP VALUE WAS 0 UNTIL 2026-09-07 and it had to change
                     with the coloured ground below: on black-on-black it made no
                     difference, but a colour band that starts on the pile's top
                     pixel reads as a crop rather than as a panel. */
                  padding: clamp(48px,9vw,76px) var(--spine) clamp(56px,8vh,90px); }
  .js .pr-stage.is-panel .pr-plate { display: none; }
  /* The ground is gone with the scene, so the stage paints the variety itself.
     `--panel` is defined once on the base rule near the top of this section. */
  .js .pr-stage { background: var(--panel); }

  /* ⚠ HISTORY, KEPT BECAUSE IT EXPLAINS THE COLOURS, 2026-09-07, HIS CALL: *"home pages
     products section doesn't really look good for mobile version, we want it to
     be same as pc version"*. Asked which of the two things "same as pc" meant —
     the look, or the pinned scrolling scene as well — **he chose the look with
     normal scrolling.** So this is five coloured panels you scroll past, not the
     scene. Do not reach for `position: sticky` here without asking him again.

     WHAT WAS WRONG: the stack sat entirely on `.pr`'s #02040a. The five
     `--stage` colours are the section's identity on a desktop and a phone got
     none of them, so it read as one long dark list — and worse, the two dark
     piles, Red and Black, were near-invisible against it. The photographs were
     losing to the background.

     ⚠ `--stage` AND `--pc` ARE ON THE MARKUP, one pair per `<article>`, so this
     rule needs no list of colours and cannot drift from the desktop's. The same
     two custom properties drive `.pr-ground`'s tween and the plates up in the
     scene. One place to change a variety's colour: its style attribute.

     ⚠ THE BLOOM IS A BACKGROUND LAYER, NOT AN `::after`, and that is not a
     preference. The scene uses `::after` because everything there is positioned;
     here the spec rows are `position: static`, so a positioned `::after` at any
     z-index paints OVER them and washes the numbers out. A background layer
     cannot ever be in front of the element's own content.

     ⚠ UNPREFIXED, so the no-JS render gets it too. `.js .pr-stage` above still
     wins on `padding` because it is more specific — that is why the top value
     had to go into that rule rather than this one. */
  /* (the paint itself is the `.js .pr-stage` rule above) */

  .js .pr-pile { margin: 0 auto 26px; width: 100%; max-width: 420px; }
  /* ⚠ THE COLOURED GLOW IS BACK, 2026-09-07. It was dropped here because on
     the old flat black ground a `--pc` halo just made a grey smudge. Over the
     variety's own colour it does what it does on the desktop: lifts the pile off
     the panel and stops the two dark varieties sinking into it. Weaker than the
     scene's 90px — a phone shows the pile much larger relative to the screen, so
     the same radius reads as fog. */
  .js .pr-pile img { width: 100%;
                     filter: drop-shadow(0 14px 26px rgba(0,0,0,.5))
                             drop-shadow(0 0 56px color-mix(in srgb, var(--pc) 36%, transparent))
                             !important; }
  .js .pr-copy { position: static; transform: none !important;
                 width: auto; max-width: none; }
  /* The cards come back as a plain row of specs under the name — the facts were
     always the point, the glass never was. */
  /* Back to the base spec row. Every glass property has to be named — none of
     them is undone by `display` alone. */
  .js .pr-card { display: flex; position: static; justify-content: space-between;
                 gap: 20px; padding: 11px 0; border-radius: 0;
                 background: none; -webkit-backdrop-filter: none; backdrop-filter: none;
                 border: 0;
                 /* Tinted to the variety, 2026-09-07, for the same reason the
                    ground is: a chalk rule is the one part of this block that
                    would still look like the old flat stack. It is the same
                    `--pc` mix the CTA's underline uses, a shade weaker. */
                 border-bottom: 1px solid color-mix(in srgb, var(--pc) 42%, transparent);
                 box-shadow: none; }
  /* A top rule on the first row closes the set — three rows with only bottom
     borders read as a list that has lost its head. */
  .js .pr-card--a { border-top: 1px solid color-mix(in srgb, var(--pc) 42%, transparent); }
  .js .pr-card .k { display: inline; margin: 0; align-self: center; }
  .js .pr-card b { font-size: 15px; }
  .js .pr-cta { margin-top: 22px; }
  .js .pr-rail { display: none; }
}

/* ============================================================================
   06 · CERTS — the four cards become links                    2026-08-29
   ============================================================================
   His call: *"make certification cards clickable - they should open certs"*.
   Each `.cert` is an `<a>` on this page now, pointing at its own row on
   `/certification/`.

   ⚠ `site.css` OWNS `.cert`. It was shared with the previous home page, which
   is why the link styling was scoped here rather than written into site.css.
   That page is archived now and `#certs .cert` is the only user — **the scoping
   still stands**: `.cert` is a generic card and the next page that wants one
   should not inherit a link's behaviour by accident.

   ⚠ `.cert-mark, .cert h3, .cert p` IS ALREADY `position: relative; z-index: 1`
   in site.css, to sit above the card's hover glow. `.cert-go` is a `<span>` and
   would NOT be caught by that selector, so it carries its own. Miss this and
   the arrow paints under the glow on hover. */


/* ---- the ground -----------------------------------------------------------
   ⚠ A RAMP, NOT THE FLAT `--violet-dp` `.dark` GIVES IT, AND IT IS HERE BECAUSE
   JOURNEY LEFT — 2026-09-07, his call, the page asked for too much scrolling.
   That section's own background did this ramp over 780vh:
     top    #02040a  = `.c3`'s black, the colour GALLERY ends on
     bottom #37125e  = `--violet-dp`, the flat violet `.dark` starts on
   With it gone, GALLERY's near-black meets that violet on one hard horizontal
   line — the exact seam JOURNEY's gradient existed to prevent. This does the
   same journey inside this section's own top third instead.
   ⚠ THE TWO ENDS ARE COPIES OF SOMEONE ELSE'S COLOUR and nothing enforces it,
   same trap the `.jr` note above carries. Repaint `.c3` and repaint this.
   ⚠ IF JOURNEY EVER COMES BACK, THIS COMES OUT — the ramp would then run twice
   and CERTS would open on a black band for no reason. */
#certs { background: linear-gradient(180deg,
           #02040a 0%, #100c1c 12%, #1d1330 26%, var(--violet-dp) 52%); }
/* An `<a>` brings underline and link colour with it. The card was never a
   link's shape, so both go, and the card's own existing background/border
   transition does the hover work. */
/* ⚠ A COLUMN, SO THE FOUR ARROWS LAND ON ONE BASELINE. The issuer blocks are
   two and three lines long, so top-aligned content puts each `.cert-go` at a
   different height and the row stops reading as one object. `margin-top: auto`
   on the arrow pushes it to the floor of whichever card it is in. */
#certs .cert { display: flex; flex-direction: column;
               text-decoration: none; color: inherit; }
#certs .cert:hover,
#certs .cert:focus-visible { background: rgba(242,237,248,.11);
                             border-color: rgba(242,237,248,.30); }

/* The resting affordance. Without it a card looks exactly like the three
   non-clickable bands elsewhere on this page — the same fault the caliber belt
   had, which is why it went unnoticed for two weeks. */
#certs .cert-go { position: relative; z-index: 1;
  display: flex; align-items: center; gap: 7px; margin-top: auto; padding-top: 18px;
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--violet-lt); }
#certs .cert-go span { display: inline-block;
  transition: transform 260ms var(--ease-out); }
#certs .cert:hover .cert-go span,
#certs .cert:focus-visible .cert-go span { transform: translateX(4px); }

@media (prefers-reduced-motion: reduce) {
  #certs .cert-go span { transition: none; }
}
