/* /our-vision/ — The Dragon Gate.
 *
 * ✗✗ TWO VERSIONS DIED HERE BEFORE THIS ONE, and they died of the same
 * thing, not of styling:
 *   1. A technical drawing of a wing in plan view -- "wtf is that diagram
 *      you drew" + "it feels very empty".
 *   2. Her manifesto set on a drafting grid -- "i dont like it... its
 *      horrible man its so ai-y".
 *
 * The diagnosis after the second one was that THE PAGE HAD NO MECHANISM.
 * Three short lines of text is not a screen, and a drawing standing in for
 * having something to say is decoration however carefully it is drafted.
 * Every page that works on this site is an instrument you operate: hovering
 * a daemon on /inside/ performs a matrix lookup, /apply/ is a real shell,
 * /founder/ scans. This page had nothing to do.
 *
 * ★ Her fix, and it is the right one: A TELESCOPE. The page is called "our
 * vision" and now you literally look through something. The focus ring is
 * the only navigation, so the single control on the page is also the single
 * idea on the page.
 *
 * ── THE OPTICS, which is where this is won or lost ────────────────────
 * ⚠️⚠️ A FIELD STOP IS NOT A VIGNETTE. The edge of an eyepiece's circle is
 * a machined metal ring, so it is SHARP, and the black outside it is
 * absolute. The soft darkening lives just INSIDE that hard edge
 * (edge-of-field falloff). Nearly every fake scope on the web inverts this
 * -- a soft radial gradient fading outward -- which is exactly why they
 * read as a CSS mask instead of as glass.
 *
 *        black        hard edge      soft falloff         full
 *      ───────────────┐
 *                     │▓▓▒▒░░
 *                     │      ░░░░░░░░░░░░───────────────────
 *                     ↑
 *                   SHARP
 *
 * ⚠️ And "tunnel vision" is a RATIO, not an effect. The feeling comes from
 * how much black surrounds the disc: a big circle in a thin frame is a
 * porthole, a smaller circle in a lot of black is a tunnel. So the disc is
 * deliberately smaller than instinct wants. Restraint and the effect are
 * the same decision here, which is the only kind of restraint she keeps.
 *
 * ⚠️ NO CHROMATIC ABERRATION. Real fringing is blue and cyan, and there is
 * no blue on this page -- her style bible bans it from the plates and it
 * would be the one thing on screen that is not red.
 *
 * ── THE RACK FOCUS, which is where the performance is won ─────────────
 * ★★ THE BLUR IS PRE-BAKED AND THE RUNTIME ONLY CROSSFADES OPACITY.
 * `filter: blur()` animated on a full-screen image is the most expensive
 * thing you can hand a compositor -- same family as the backdrop-filter
 * that ate 44% of the frame budget on krystallisjr. So each stop ships four
 * plates (three disc-bokeh tiers + sharp, see tools/vision_plates.py) and
 * focus is a stack of opacities. Opacity is compositor-only: free.
 */

@font-face {
  font-family: 'Newsreader';
  src: url('../assets/newsreader.woff2') format('woff2');
  font-weight: 200 800; font-display: swap;
}
@font-face {
  font-family: 'DotGothic16';
  src: url('../assets/dotgothic16.woff2') format('woff2');
  font-weight: 400; font-display: swap;
}
@font-face {
  font-family: 'Rajdhani';
  src: url('../assets/rajdhani-700.woff2') format('woff2');
  font-weight: 700; font-display: swap;
}

:root {
  /* Her red, carried from /founder/: near-zero blue, because #ff2d55
     reads pink against black and she clocked that instantly. */
  --red:      #ff1a08;
  --red-hot:  #ffd9d2;
  --red-deep: #8c1206;
  --dim:      #5c2a24;
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --disp: 'Rajdhani', 'Arial Narrow', ui-sans-serif, system-ui, sans-serif;
  --cut:  'DotGothic16', 'Courier New', ui-monospace, monospace;
  --serif: 'Newsreader', Georgia, serif;

  /* ★ THE TUNNEL RATIO. Smaller than feels natural, on purpose -- but not
     as small as the first pass: "the tunnel vision is a bit too small. i
     love it. but i want more vision".
     ⚠️ 62svh -> 74svh, and the room had to be FOUND rather than taken. The
     old number was not a taste decision, it was whatever was left after
     the chrome: at 900px tall the line, ring, foot, gaps and padding came
     to 342px and the disc got the remainder. Growing it meant moving the
     foot out of the flow (Apply now sits beside the ring, Back is pinned
     top-right) and tightening the gaps, which bought 136px of diameter.
     ⚠️ ...and then the closest-side fix further down revealed that the
     PAINTED disc had only ever been 0.699 of this number -- so what she
     called "a bit too small" was really 390px, not 558. This value is now
     the true diameter. 576 at 1440x900 is about 1.5x what she was actually
     looking at, and still only ~20% of the viewport: the black around it
     is what makes it a tunnel, not the size of the hole. */

  /* ⚠️⚠️ ...AND THEN A CEILING, BECAUSE 67svh IS A TASTE NUMBER WITH
     NOTHING UNDERNEATH IT. Measured on a landscape phone (844x390),
     which is the viewport this whole site had never been looked at on:

        .eye rows   tube 261 + stage 158 + line 60 + ring 54 + apply 54
        + gaps and padding                                       ~ 60
        total                                                      647px
        viewport                                                   390px

     `.eye` is position:fixed with align-content:center, so those 257
     extra pixels were split evenly top and bottom -- the disc's top sat
     at y=-114 and THE FOCUS RING SAT AT y=389. The ring is the only
     navigation this page has, `body` is overflow:hidden, and a fixed
     element cannot be scrolled: in landscape the page simply had no way
     forward. Apply, at y=451, was gone the same way.

     So stop asking for a diameter and ask for the one that is LEFT --
     the principle /founder/ already states in its own sheet ("sized by
     WHAT IS LEFT, not by a magic viewport number"). The chrome is
     written out term by term, and the gap/padding tokens are the ones
     `.eye` itself now uses, so the two cannot drift apart: changing the
     padding changes the ceiling with it.

     ⚠️ --eye-line is the line's FLOOR (its min-height of 3.4em), not its
     true height -- the legend wraps past two lines on a narrow screen.
     That is deliberate rather than sloppy: the ceiling only has to bind
     where height is scarce, which is landscape, where the line really is
     two lines and the floor really is its height. Portrait has height to
     spare and the 56vw / 84vw terms bind there instead.
     ⚠️ Verified by measuring all eight viewports after the change, not
     trusted from this arithmetic. The hero's dead tail on this same site
     is the standing reminder that the two disagree more often than not. */
  --eye-gap: clamp(8px, 1.8svh, 18px);
  --eye-pad: clamp(14px, 3.4svh, 34px);
  --eye-line: calc(3.4 * clamp(1.06rem, 2.1vw, 1.66rem));
  /* 54px is .ring's own height. 10px is slack for the index pip, which
     overshoots the ring's box by 5px above it. */
  --eye-chrome: calc(2 * var(--eye-gap) + 2 * var(--eye-pad)
                     + var(--eye-line) + 54px + 10px);
  --disc: min(67svh, 56vw, 690px, calc(100svh - var(--eye-chrome)));
}

* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; background: #000; }

body {
  opacity: 0;
  transition: opacity 460ms ease;
  color: var(--dim);
  font-family: var(--mono);
  font-size: clamp(0.78rem, 1.02vw, 0.92rem);
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;          /* neither act scrolls */
}
body.is-in { opacity: 1; }
@media (prefers-reduced-motion: reduce) { body { opacity: 1; transition: none; } }

.vi { position: relative; min-height: 100svh; }

/* ══ ACT ONE: THE ROOFTOP ═══════════════════════════════════════════════*/
.roof {
  position: fixed; inset: 0;
  display: grid;
  opacity: 1;
  transition: opacity 900ms ease, transform 1400ms cubic-bezier(.4,0,.2,1);
}
.roof__pic { grid-area: 1 / 1; display: block; min-width: 0; }
.roof__pic img {
  display: block; width: 100%; height: 100svh; object-fit: cover;
  object-position: 50% 50%;
}
/* ★ The move INTO the telescope: the rooftop pushes in slightly and dims as
   the eyepiece opens over it. Not a cut -- you walked up to the thing. */
.vi.is-looking .roof { opacity: 0; transform: scale(1.14); pointer-events: none; }

/* ✗✗✗ EVERY RUNTIME EFFECT ON THIS SCREEN IS GONE -- windows switching,
   traffic on her traced roads, blinking boats, the bay shimmer, the haze
   drift. All of it deleted, not disabled.

   "get rid of any effects you put and i think we just make the towers glow
   red no??" -- her second note on this screen, and she is right in a way
   worth writing down: every one of those was an OBJECT BEHAVING. I had an
   argument for each (a city is alive because its lights disagree; traffic
   is the biggest inhabited cue there is) and the argument was answering a
   question she had not asked. She did not want behaviour on this screen.
   She wanted it to LOOK better. Those are different requests and I spent
   three rounds answering the second with the first.

   The glow is a LOOK, so it lives in the plate: baked once by
   tools/vision_plates.py, zero runtime cost, nothing competing with the
   judgements or the callout -- which are the only two things on this
   screen that have to land. Same method as the hero's `glow-*-bloom`
   plates on the other site, for the same reason.
   tools/roof_lights.py and the roads/boats data stay in the repo: her
   tracing is not wasted, it is just not being spent here. */

/* ── The judgements, scratched on the parapet ────────────────────────────
   Her words, and the ones this whole company is a reply to. Hovering
   strikes one through -- the gesture IS the argument, so it is the one
   piece of motion on this screen that is not the telescope. */
.judge {
  grid-area: 1 / 1;
  align-self: end;
  justify-self: start;
  margin: 0 0 clamp(96px, 22svh, 240px) clamp(24px, 7vw, 120px);
  display: flex; flex-wrap: wrap; gap: 0.1em 1.9ch;
  max-width: min(92vw, 44ch);
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(1.05rem, 2.5vw, 2.1rem);
  line-height: 1.3;
  color: #d8c6c2;
  text-shadow: 0 2px 18px rgba(0,0,0,0.9);
}
.judge__w {
  position: relative;
  cursor: default;
  transition: color 240ms ease, opacity 240ms ease;
}
/* ── The strike ─────────────────────────────────────────────────────────
 * ✗ FIRST VERSION: a 2px CSS bar scaled in from the left. Her note was
 * "make sure the crossing out is more natural looking. it looks a bit
 * ai-y", and she was right for a reason the research names exactly:
 *
 *   ★ JITTER ALONG THE LINE IS THE PRIMARY HAND-DRAWN CUE -- more than
 *     width variation, more than anything else. A perfectly straight line
 *     reads as fake not because it lacks detail but because the ABSENCE of
 *     imperfection is itself the thing the eye notices.
 *
 * So it is a generated SVG path now (js/vision.js), with four properties a
 * ruled bar cannot have: a wandering centreline, overshoot past both ends
 * of the word, a slight tilt off horizontal, and tapered ends where the
 * pen lifts. Drawn with stroke-dasharray, eased so the stroke accelerates
 * and settles rather than travelling at a constant rate.
 *
 * ⚠️ NOT rough-notation or rough.js. Both do this well and both are a
 * dependency plus a second one, for an effect that is twenty lines when
 * you generate the path yourself -- and generating it is what lets it be
 * her red, carry the glow, and be seeded so it does not re-randomise on
 * every resize. */
.judge__w { padding: 0 0.06em; }
.judge__ink {
  position: absolute;
  left: -9%; right: -9%; top: -10%; bottom: -10%;
  width: 118%; height: 120%;
  overflow: visible;
  pointer-events: none;
}
.judge__ink path {
  fill: none;
  stroke: var(--red);
  stroke-linecap: round;
  filter: drop-shadow(0 0 3px rgba(255,26,8,0.95))
          drop-shadow(0 0 9px rgba(255,26,8,0.55));
  stroke-dasharray: var(--len, 200);
  stroke-dashoffset: var(--len, 200);
  transition: stroke-dashoffset 520ms cubic-bezier(.22,.9,.3,1);
}
.judge__w.is-struck { color: #6d5a57; }
.judge__w.is-struck .judge__ink path { stroke-dashoffset: 0; }

/* "Look." arrives once they have all been struck through. */
.roof__back {
  position: absolute;
  top: clamp(16px, 3.4svh, 34px);
  right: clamp(18px, 5vw, 64px);
  z-index: 3;
  font-family: var(--mono);
  font-size: clamp(0.72rem, 0.95vw, 0.86rem);
  letter-spacing: 0.16em; text-transform: uppercase;
  color: #c08275;
  text-decoration: none;
  border-bottom: 1px solid rgba(192,130,117,0.5);
  padding-bottom: 2px;
  text-shadow: 0 1px 6px rgba(0,0,0,0.9);
  transition: color 160ms ease, border-color 160ms ease;
}
.roof__back:hover, .roof__back:focus-visible {
  color: #fff; border-color: var(--red); outline: none;
  text-shadow: 0 0 5px #fff, 0 0 14px var(--red);
}
.vi.is-looking .roof__back { opacity: 0; pointer-events: none; }

/* ── Hit areas ──────────────────────────────────────────────────────────
 * ★ Three controls on this page are small engraved words -- "use", "back
 * inside", "back" -- and that is the right look: a label cut into an
 * instrument, not a button stuck on it. Measured by hit-testing, they are
 * 23, 30 and 37 CSS pixels tall. The floor for a finger is 44 (Apple's
 * HIG, and WCAG 2.5.5 lands in the same place), so all three are hard to
 * hit on a phone -- and the one on the rooftop is the ONLY way off that
 * screen for anyone who does not find the telescope.
 *
 * ⚠️ The fix cannot be padding. All three carry a border-bottom underline,
 * so padding-bottom moves the rule away from the word; and padding-top on
 * an element pinned by `top` pushes the word down the screen. Both change
 * what she tuned.
 *
 * ✓ An absolutely-positioned ::after instead: a transparent rectangle
 * centred on the element, which grows the area a finger can land on and
 * paints nothing, so not one pixel moves.
 *
 * ⚠️ 46px, not 44. The hit area is measured from the centre outward in
 * 2px steps, so a control sitting exactly on the floor is one rounding
 * error from being under it.
 * ⚠️ NOT gated behind `@media (pointer: coarse)`. This project's headless
 * browser reports `hover: none, pointer: none` at every viewport, so such
 * a gate cannot be verified here at all -- and a touchscreen laptop
 * reports `pointer: fine` while still being driven by a finger. A
 * comfortable hit area costs a mouse nothing, so there is nothing to gate.
 * ⚠️ No `position: relative` needed: all three are already absolutely
 * positioned, so each is its own containing block. If one ever goes back
 * into the flow, it needs one adding or its extender will escape to the
 * nearest positioned ancestor and cover half the page.
 * ⚠️ A fourth element, `.eye__sw`, was briefly in this list and is gone
 * with the link itself. It is worth knowing why it was the awkward one:
 * it went `position: static` under 860px, i.e. back into the flow on
 * exactly the viewports a finger is used on, so it needed its own
 * `relative` AND an `inset: auto` to stop the base rule's absolute
 * `left`/`bottom` becoming a 64px offset. Anything added here later that
 * is not absolutely positioned has the same two problems. */
.roof__back::after, .eye__back::after, .use::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 50%;
  height: 46px;
  translate: 0 -50%;
}

.roof__look {
  grid-area: 1 / 1;
  align-self: end; justify-self: start;
  margin: 0 0 clamp(52px, 13svh, 150px) clamp(24px, 7vw, 120px);
  font-family: var(--serif);
  /* ⚠️⚠️ THE FLOOR IS SET TO PRESERVE A RELATIONSHIP, NOT PICKED ALONE.
     Found by tools/audit_proportions.py, which measures how much every
     element shrinks from laptop to phone and flags the ones that do not
     shrink with the rest. Measured 38.4 -> 19.2px (x0.50) against a page median of x0.73. This is
     the line that tells you to use the telescope, and on a phone it had
     shrunk to half -- the one instruction on the rooftop, reading as a
     caption. 1.75rem is 28px, which is x0.73.
     ★ The rule: on these pages the chrome is already near its clamp floor
     at BOTH sizes, so it only shrinks ~0.8. Display type set with a low
     floor shrinks 0.5-0.6 and the hierarchy collapses on a phone -- the
     big thing stops being big relative to everything round it. Floors are
     a proportion decision; they are not a safety net. */
  font-size: clamp(1.75rem, 2.8vw, 2.4rem);
  color: #fff;
  text-shadow: 0 0 10px rgba(255,26,8,0.6), 0 2px 18px rgba(0,0,0,0.9);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 900ms ease, transform 900ms ease;
}
.vi.is-told .roof__back {
  position: absolute;
  top: clamp(16px, 3.4svh, 34px);
  right: clamp(18px, 5vw, 64px);
  z-index: 3;
  font-family: var(--mono);
  font-size: clamp(0.72rem, 0.95vw, 0.86rem);
  letter-spacing: 0.16em; text-transform: uppercase;
  color: #c08275;
  text-decoration: none;
  border-bottom: 1px solid rgba(192,130,117,0.5);
  padding-bottom: 2px;
  text-shadow: 0 1px 6px rgba(0,0,0,0.9);
  transition: color 160ms ease, border-color 160ms ease;
}
.roof__back:hover, .roof__back:focus-visible {
  color: #fff; border-color: var(--red); outline: none;
  text-shadow: 0 0 5px #fff, 0 0 14px var(--red);
}
.vi.is-looking .roof__back { opacity: 0; pointer-events: none; }

.roof__look { opacity: 1; transform: none; }

/* ── "use", on the telescope ────────────────────────────────────────────
 * ✗ FIRST VERSION: a bordered pill with a tracked all-caps word and a
 * pulsing dot beside it. "the USE needs to be nicer. it feels ai-y also"
 * -- and it did, because that is the stock treatment: box + glow + pulse
 * is what gets drawn when nobody decided anything. A box around a word is
 * also, by her own rule, the thing that has been thrown off every page of
 * this site. A framed rectangle is a card whatever the paint.
 *
 * ✓ It is an ANNOTATION now, not a button: a hairline leader running from
 * the telescope out to a single engraved word. That is how a part on an
 * instrument is labelled, it reads as belonging to the object rather than
 * floating over the photograph, and it is the same engraved treatment as
 * the marks on the focus ring in act two -- so the one control on the
 * first screen and the one control on the second are visibly the same
 * family. No box, no dot, no pulse.
 */
.use {
  position: absolute;
  /* Measured against the plate: the tube runs (9%,43%) to (29%,28%) and
     the mount sits under it near (18%,45%). The leader starts ON the
     instrument and the word sits out in the clear air to its right. */
  left: clamp(9%, 13vw, 17%);
  top: 43%;
  display: flex; align-items: center; gap: 0;
  padding: 0.4em 0;
  font: inherit;
  background: none;
  border: 0;
  cursor: pointer;
  color: var(--red-hot);
  -webkit-appearance: none; appearance: none;
}
/* the leader line: starts at the instrument, stops at the word.
   ⚠️ 1px -> 2px and longer. "make more prominent espeically the USE" --
   the annotation was right and it was whispering. The leader is the part
   that makes it belong to the telescope, so it grows WITH the word rather
   than being left behind as a hairline next to large type. */
.use__dot {
  display: block;
  width: clamp(44px, 5.4vw, 92px);
  height: 2px;
  border-radius: 0;
  background: linear-gradient(90deg, rgba(255,26,8,0) 0%, var(--red) 38%, var(--red) 100%);
  box-shadow: 0 0 5px rgba(255,26,8,0.75);
  animation: none;
  transition: width 260ms cubic-bezier(.2,.8,.2,1);
}
/* ...and a small tick where it meets the telescope, the way a callout is
   actually drawn on a technical plate */
.use__dot::after { content: none; }
/* ★ BIG. This is the only thing to do on the first screen and it was set
   at 1.1rem -- smaller than the judgements it sits above. Prominence here
   is SIZE and VALUE, not a box and not a pulse: a callout on a technical
   plate is allowed to be large, and large plus a leader line is still an
   annotation. A box would be a framed rectangle again. */
.use__w {
  font-family: var(--disp); font-weight: 700;
  /* ⚠️⚠️ THE FLOOR IS SET TO PRESERVE A RELATIONSHIP, NOT PICKED ALONE.
     Found by tools/audit_proportions.py, which measures how much every
     element shrinks from laptop to phone and flags the ones that do not
     shrink with the rest. Measured 44.6 -> 27.2px (x0.61) against x0.73. `USE` is the single
     control on that screen and her note was that it had to be more
     prominent, which was answered with size and value rather than a box
     -- so letting it lose 39% on a phone undoes that fix where it
     matters most. 2.05rem is 32.8px, which is x0.73.
     ★ The rule: on these pages the chrome is already near its clamp floor
     at BOTH sizes, so it only shrinks ~0.8. Display type set with a low
     floor shrinks 0.5-0.6 and the hierarchy collapses on a phone -- the
     big thing stops being big relative to everything round it. Floors are
     a proportion decision; they are not a safety net. */
  font-size: clamp(2.05rem, 3.1vw, 2.85rem);
  letter-spacing: 0.2em; text-transform: uppercase;
  line-height: 1;
  padding-left: 0.6em;
  color: #fff;
  text-shadow: 0 2px 2px rgba(0,0,0,0.9),
               0 0 12px rgba(255,26,8,0.85),
               0 0 38px rgba(255,26,8,0.5);
  transition: color 180ms ease, text-shadow 180ms ease, letter-spacing 300ms ease;
}
.use:hover .use__w, .use:focus-visible .use__w {
  text-shadow: 0 2px 2px rgba(0,0,0,0.9),
               0 0 16px rgba(255,120,96,1),
               0 0 54px rgba(255,26,8,0.8);
  letter-spacing: 0.235em;
}
.use:hover .use__dot, .use:focus-visible .use__dot { width: clamp(60px, 7vw, 124px); }
.use:focus-visible { outline: none; }
@media (prefers-reduced-motion: reduce) {
  .use__dot { transition: none; }
}

/* ══ ACT TWO: THE EYEPIECE ══════════════════════════════════════════════*/
.eye {
  position: fixed; inset: 0;
  display: grid;
  /* ⚠️ THREE ROWS, NOT FOUR. The foot used to be a row of its own and it
     was costing the disc ~74px of diameter for two links. Back is pinned
     top-right now and Apply sits beside the ring, so the only things
     taking vertical space are the view, her line, and the control. */
  grid-template-rows: 1fr auto auto;
  justify-items: center;
  align-content: center;
  /* ⚠️ THE SAME TOKENS --disc's CEILING IS COMPUTED FROM. They used to be
     written out twice -- the literal clamps here and the same clamps
     repeated in the arithmetic -- which is exactly how a ceiling quietly
     stops matching the box it is supposed to fit inside. */
  gap: var(--eye-gap);
  padding: var(--eye-pad) clamp(18px, 5vw, 64px);
  opacity: 0;
  /* ⚠️ INERT BY pointer-events, NOT BY display. js/vision.js strips the
     [hidden] attribute the moment it mounts, because a display:none <img>
     is never fetched -- left hidden until the button is pressed, entering
     the telescope would show an empty black disc while 300KB downloaded.
     So the section is present and loading from the start, and merely
     invisible and unclickable until she looks. */
  pointer-events: none;
  transition: opacity 1100ms ease;
}
.eye[hidden] { display: none; }
.vi.is-looking .eye { opacity: 1; pointer-events: auto; }

/* The tube carries the sway, so the sway moves the VIEW and not the page. */
.eye__tube {
  position: relative;
  align-self: center;
  width: var(--disc); height: var(--disc);
  translate: var(--sx, 0px) var(--sy, 0px);
}

/* ── What each year actually IS ─────────────────────────────────────────
   ★ Her note: "more detail on the years and so on". The legend told the
   koi story beautifully and never once said what the three stages ARE --
   you could watch the whole thing and not learn that this is a community
   now, an accelerator next and a fund after that. Her own words from the
   build this page replaces, unchanged.
   ★ It lives in the BLACK BESIDE THE DISC, which is the move that let the
   disc grow at the same time: that margin was empty, it costs nothing
   vertically, and a label set beside an instrument's view is how an
   instrument is labelled. Pinned to the tube rather than to the viewport
   so it tracks the disc at every size -- and deliberately NOT inside
   .eye__tube's sway, or the label would drift with the optics. */
.stage {
  position: absolute;
  right: calc(50% + var(--disc) / 2 + clamp(26px, 3.6vw, 64px));
  top: 50%;
  translate: 0 -50%;
  /* ⚠️ THE THIRD TERM IS THE SPACE THAT ACTUALLY EXISTS TO ITS LEFT, and
     without it the flat 350px ran off the screen on a short laptop:
     measured at 1280x760 the box sat at x=-11, so the longest line of a
     RIGHT-aligned paragraph was being shaved on its first character.
     The disc is sized off height, the pin is half the viewport plus the
     disc's radius, and the label is a flat width -- three numbers that
     each move for a different reason, which is the shape of a bug that
     only shows up at one window size.
     ⚠️ The -16px is a gutter, and it is chosen so this cannot touch the
     1440x900 case she tuned the copy at: there the available space is
     366.7px, so min() still returns the 350 it always did. At 1280x760 it
     returns 323.5 and the box lands at x=16. Verified both. */
  width: min(30vw, 350px,
             calc(50% - var(--disc) / 2 - clamp(26px, 3.6vw, 64px) - 16px));
  text-align: right;
  pointer-events: none;
  opacity: 0;
  transition: opacity 600ms ease;
}
.stage.is-in { opacity: 1; }

/* ✗✗ WHAT THIS LOOKED LIKE BEFORE, and why she said "the writing and so
 * on still feel a bit ai-y". It was three generic tells stacked in one
 * 90px-tall block:
 *
 *     COMMUNITY            <- tracked all-caps name
 *     NOW                  <- a tracked all-caps EYEBROW under it
 *     A network of foun...  <- body copy in MONOSPACE
 *
 * The tracked-caps label above a heading is the single commonest tell in
 * generated layout, and doing it TWICE in a row -- name, then a second
 * all-caps fragment -- is the pattern, not a variation on it.
 * ⚠️ And the body copy was monospace, which is a flat violation of her own
 * standing rule: NO MONOSPACE UI. It inherited it from `body`, so nothing
 * in this block ever chose it. The worst kind of wrong: not a decision.
 *
 * ✓ It is prose now, in Newsreader -- the SAME voice as her legend under
 * the eyepiece. One voice on the page instead of a caption written by an
 * interface and a story written by a person. The "now / next / after
 * that" line is gone entirely: the focus ring already says NOW, 1Y and
 * 3Y about four inches away, and saying it twice is what made it read as
 * a template filling its own fields. */
.stage__n {
  display: block;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(1.4rem, 2.45vw, 2.15rem);
  line-height: 1.16;
  letter-spacing: 0.005em;
  color: #fff;
  text-shadow: 0 0 10px rgba(255,26,8,0.5), 0 0 30px rgba(255,26,8,0.25);
}
/* kept in the DOM and emptied by js -- removing the element would mean a
   markup change for a copy decision she may well reverse. */
.stage__when { display: none; }
/* ⚠️ BIGGER AND HEAVIER, AND THE WEIGHT IS THE HALF THAT FIXES "GRAINY".
   Light type on a dark ground renders optically THINNER than the same
   weight dark-on-light -- the bright strokes bleed into the black and the
   antialiasing eats them, which at 18px in a 300-weight serif is exactly
   the grain she is seeing. Size alone would have made bigger grainy text.
   300 -> 400 and 18px -> 21px, with the colour lifted as well, because
   contrast is the third term in the same problem. */
.stage__c {
  display: block;
  margin-top: 0.78em;
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(1.06rem, 1.48vw, 1.3rem);
  line-height: 1.6;
  color: #c7958b;
}

/* ⚠️⚠️ `closest-side`, AND THIS WAS A REAL BUG, not a tidiness point.
   A bare `radial-gradient(circle ...)` sizes itself FARTHEST-CORNER by
   default, so on a square box its 100% sits at the corner -- 0.707 x the
   side, not 0.5. Which meant `49.4%` resolved to a radius of
   0.494 x 0.707 = 0.349 of the side, and the painted disc came out at
   **0.699 of the element**: measured 470px inside a 666px box.
   The element was reserving the full square in layout the whole time, so
   roughly 200px of dead space sat under the view, and her "the tunnel
   vision is a bit too small" was reporting a gradient-sizing default
   rather than a taste call. Her eye was right and the number was lying. */
.eye__field {
  position: absolute; inset: 0;
  overflow: hidden;
  -webkit-mask-image: radial-gradient(circle closest-side at 50% 50%, #000 98.6%, transparent 99.4%);
          mask-image: radial-gradient(circle closest-side at 50% 50%, #000 98.6%, transparent 99.4%);
}
/* ⚠️⚠️ OPACITY, NOT `display`. This was `display: none` / `display: block`
   and it is the flicker she saw scrolling between the stops on a phone:
   "the accelerator and venture capital and so on does a weird flicker".
   A display change DESTROYS AND REBUILDS the element's compositor layers,
   so at every swap all four of that stop's plates had to be re-rastered
   and re-uploaded to the GPU in a single frame -- and `will-change:
   opacity` on .pl cannot help, because there is no layer to promote while
   the parent is display:none. The swap is already timed to land at f = 0,
   where every plate is a field of defocused discs, so the arithmetic was
   right and the compositing was throwing it away.
   ✓ Kept in the layer tree and hidden with opacity, so switching stops is
   a flag flip on layers that already exist. The whole page is ~215 KB of
   plates, so holding all of them composited costs almost nothing.
   ⚠️⚠️ AND pointer-events STAYS off even on the active stop. `display: none`
   was doing two jobs and only one of them was visibility: it also kept the
   plates out of hit-testing. Restoring them to the layer tree put a
   full-bleed image over the ROOFTOP, and it swallowed the USE button --
   caught by the harness, which could not click `.use` at all because
   "s1-1400.webp intercepts pointer events". Nothing needs to click a
   plate: the ring takes the drag and the buttons take the taps. */
.stop {
  position: absolute; inset: 0;
  opacity: 0;
  pointer-events: none;
  will-change: opacity;
}
/* The shimmer sits above the plates and below the falloff, so the
   edge-of-field darkening applies to it too -- light that ignored the
   optics would be the thing that gave the whole eyepiece away. */
.eye__koi {
  position: absolute; left: 0; top: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  z-index: 2;
}
.stop.is-on { opacity: 1; }

/* The four plates of one stop, stacked. The runtime raises each one's
   opacity over the one below as focus comes in, so there is never a seam:
   they are the same photograph at four defocus radii.
   ⚠️ `scale` and `translate` as their own properties rather than inside a
   transform, so focus-settle and pan can be driven independently without
   either having to know the other's value. */
.pl {
  position: absolute; left: 50%; top: 50%;
  width: 100%; height: 100%;
  object-fit: cover;
  translate: calc(-50% + var(--px, 0px)) -50%;
  scale: var(--pz, 1);
  will-change: opacity;
}
.pl--b0 { opacity: 1; }
.pl--b1 { opacity: var(--f1, 0); }
.pl--b2 { opacity: var(--f2, 0); }
.stop picture { display: contents; }
.pl--hi  { opacity: var(--f3, 0); }

/* ── Inside the stop: the soft falloff ──────────────────────────────────
   This is the part that is allowed to be a gradient, because this one
   really is a gradient in the real world. Transparent well into the field,
   so the centre is untouched. */
.eye__falloff {
  position: absolute; inset: 0;
  pointer-events: none;
  border-radius: 50%;
  /* closest-side for the same reason as the field stop -- these stops are
     fractions of the RADIUS, not of the corner.
     ⚠️⚠️ AND THE VALUES HAD TO COME DOWN WITH IT. Under the old
     farthest-corner sizing, 100% sat out at the box corner, so most of
     this ramp fell OUTSIDE the visible disc and only its gentle beginning
     ever showed. Corrected to closest-side, the same numbers put 0.9 black
     exactly on the rim -- which quietly destroyed the hard field stop,
     because the image was already black by the time the mask cut it. The
     stop was still there and there was nothing left for it to cut.
     An edge-of-field falloff is subtle; the rim keeps enough light to show
     the edge is MECHANICAL. */
  background: radial-gradient(circle closest-side at 50% 50%,
    rgba(0,0,0,0) 0%, rgba(0,0,0,0) 62%,
    rgba(0,0,0,0.12) 82%, rgba(0,0,0,0.30) 95%, rgba(0,0,0,0.42) 100%);
}
/* ★ Scatter. Real glass is never perfectly black -- a little of the city
   bounces around inside the barrel. Pure #000 outside the disc reads as a
   mask; a breath of red reads as an optic. Sits OUTSIDE the field stop, so
   it never touches the image. */
.eye__scatter {
  position: absolute; inset: -14%;
  pointer-events: none;
  /* ⚠️ The background has to be LIT where the mask lets it through, and
     the first closest-side pass got that backwards: it faded to transparent
     by 72% while the mask only opened at 78%, so the two never overlapped
     and the scatter was mathematically invisible. It is brightest just
     OUTSIDE the field stop, which is where light actually bounces -- off
     the inside of the barrel, not out in the room. */
  background: radial-gradient(circle closest-side at 50% 50%,
    rgba(0,0,0,0) 0%, rgba(0,0,0,0) 74%,
    rgba(255,26,8,0.10) 79%, rgba(255,26,8,0.035) 89%, rgba(0,0,0,0) 100%);
  /* Knocked out exactly at the field stop so it never touches the image.
     This box is 1.28x the tube, so with closest-side the disc's own radius
     is 0.5/0.64 = 78% of this gradient's 100%. Those two numbers are tied:
     change `inset` and this has to move with it. */
  -webkit-mask-image: radial-gradient(circle closest-side at 50% 50%, transparent 77%, #000 78.5%);
          mask-image: radial-gradient(circle closest-side at 50% 50%, transparent 77%, #000 78.5%);
}

/* ── Her line, in the black under the eyepiece ──────────────────────────*/
.line {
  margin: 0;
  min-height: 3.4em;
  max-width: min(90vw, 46ch);
  text-align: center;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(1.06rem, 2.1vw, 1.66rem);
  line-height: 1.44;
  color: #cbb6b2;
  opacity: 0;
  transition: opacity 700ms ease;
}
.line.is-in { opacity: 1; }
/* The payoff lines land lit. They are the only answer on the page; every
   other line is the legend. */
.line.is-last { color: #fff; text-shadow: 0 0 8px rgba(255,26,8,0.6), 0 0 28px rgba(255,26,8,0.35); }
.line .jp { font-family: var(--cut); letter-spacing: 0.08em; }

/* ── The focus scale ───────────────────────────────────────────────────
 * ✗ FIRST VERSION: a horizontal knurled band with three words on it and a
 * caret above. "the slider for the vision thing. it feels ai-y."
 * Two things made it so, and both are the same thing:
 *   - A KNURL DRAWN AS A repeating-linear-gradient is texture-as-decoration.
 *     It is the stock way to say "machined" without any part of it being
 *     machined, which is the fake-telemetry move in a different medium.
 *   - A straight horizontal strip under the content IS a slider widget.
 *     Shape alone announced "UI control", and this page has spent its whole
 *     life refusing to put UI on top of an instrument.
 *
 * ✓ It is an engraved distance scale now, on the curve of the barrel. No
 * texture, no fill, no box -- only marks cut into a dark edge, which is all
 * a real focuser is. And the ticks are UNEVEN: on a real lens they crowd
 * toward infinity because focus distance is nonlinear, and that single
 * detail does more to make it read as an instrument than any amount of
 * gradient ever did.
 */
.ring {
  position: relative;
  width: min(86vw, 460px);
  height: 54px;
  cursor: grab;
  touch-action: none;
  user-select: none; -webkit-user-select: none;
}
.ring:active { cursor: grabbing; }
.ring__svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
/* the barrel's edge: one hairline, curved, and nothing else */
.ring__edge { fill: none; stroke: rgba(255,72,40,0.42); stroke-width: 1; }
.ring__t  { stroke: rgba(255,80,48,0.62); stroke-width: 1; }
/* ★ Brighter red, her call: "make the dial of the telescope a bit
   brighter red". The ticks were a desaturated salmon (255,150,120) which
   on this plate reads closer to bone than to red -- the one instrument on
   the page was the least red thing on it. Pushed toward the site's own
   --red without going to full #ff1a08, which would compete with Apply. */
.ring__t--maj { stroke: rgba(255,96,56,1); stroke-width: 1.4; }
.ring__lab {
  font-family: var(--disp); font-weight: 700;
  font-size: 11px; letter-spacing: 2.2px; text-transform: uppercase;
  fill: #c2503c;
  transition: fill 260ms ease;
}
.ring__lab.is-on { fill: var(--red-hot); }
.ring:focus-visible .ring__edge { stroke: var(--red); }
.ring:focus-visible { outline: none; }

/* The index: a scribed line on the body the ring turns against. */
.ring__index {
  position: absolute; left: 50%; top: 2px;
  width: 1px; height: 30px;
  translate: -50% 0;
  background: linear-gradient(180deg, var(--red) 0 58%, rgba(255,26,8,0.15) 100%);
  box-shadow: 0 0 5px rgba(255,26,8,0.75);
  z-index: 2; pointer-events: none;
}
.ring__index::before {
  content: ''; position: absolute; left: 50%; top: -5px;
  width: 0; height: 0; translate: -50% 0;
  border-left: 3.5px solid transparent; border-right: 3.5px solid transparent;
  border-top: 5px solid var(--red);
}

/* ── Foot ───────────────────────────────────────────────────────────────
   ⚠️ OUT OF THE GRID. Two links were costing the disc ~74px of diameter.
   Apply sits beside the ring on the same line and Back is pinned to the
   top corner, so neither takes a row. */
.eye__foot { display: contents; }
.apply {
  position: absolute;
  left: calc(50% + min(42vw, 230px) + clamp(18px, 2.6vw, 44px));
  bottom: clamp(16px, 4svh, 44px);
  font-family: var(--disp); font-weight: 700;
  /* ★ The payoff of the whole page, and it was set at the same size as
     the back link. It is the one thing on the last stop that asks for
     something, so it gets to be the loudest. */
  font-size: clamp(1.1rem, 1.5vw, 1.42em); letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--red-hot);
  text-decoration: none;
  padding: 0.62em 2.1em;
  border: 1px solid rgba(255,26,8,0.8);
  box-shadow: inset 0 0 26px rgba(255,26,8,0.12), 0 0 20px rgba(255,26,8,0.22);
  text-shadow: 0 0 12px rgba(255,26,8,0.7);
  opacity: 0; pointer-events: none;
  transition: opacity 700ms ease, color 160ms ease,
              border-color 160ms ease, box-shadow 160ms ease;
}
/* ★ Apply only exists once she has reached the top of the falls. Offering
   it at NOW would be asking before the argument has been made. */
.vi.is-arrived .apply { opacity: 1; pointer-events: auto; }
.apply:hover, .apply:focus-visible {
  color: #fff; border-color: var(--red); outline: none;
  box-shadow: inset 0 0 20px rgba(255,26,8,0.16), 0 0 16px rgba(255,26,8,0.38);
}
.eye__back {
  position: absolute;
  top: clamp(14px, 3.4svh, 34px);
  right: clamp(18px, 5vw, 64px);
  /* ⚠️ WAS A FLAT 0.95rem, SO IT WAS THE SAME 15.2px ON A 390 PHONE AS ON A
     1440 LAPTOP -- a shrink factor of exactly 1.00 against a page median of
     0.73. Every other thing on the eyepiece got smaller and this did not,
     so proportionally it grew: 0.34 of the page's largest type on a laptop,
     0.56 on a phone. A back link that is over half the size of the stage
     name is competing with it.
     ⚠️ Its 46px tap extender is unaffected -- that is drawn in px by the
     hit-area block above and does not follow the type. */
  font-size: clamp(0.78rem, 1.06vw, 0.95rem);
  /* ⚠️ WAS rgb(92,42,36) -- measured, and effectively invisible on black.
     "have a back button somewhere so you can escape easily": it was
     already there, in the right corner, and simply could not be seen. An
     exit nobody can find is not an exit. */
  color: #c08275; text-decoration: none;
  letter-spacing: 0.16em; text-transform: uppercase;
  border-bottom: 1px solid rgba(192,130,117,0.5); padding-bottom: 3px;
  text-shadow: 0 0 10px rgba(255,26,8,0.35);
}
.eye__back:hover, .eye__back:focus-visible {
  color: #fff; border-color: var(--red); outline: none;
  text-shadow: 0 0 5px #fff, 0 0 14px var(--red);
}

/* ── The loader ─────────────────────────────────────────────────────────
   ★ Her ask: "make sure the words on /vision come at the exact same time
   as the image renders. put a daedalus load logo if you want."
   Both halves of that are one fix. The line used to fade in on a focus
   threshold, which is a proxy for "the image is probably there" -- and a
   proxy is exactly what breaks on a cold cache, where the words land over
   an empty black disc. Now js/vision.js awaits img.decode() and the mark
   holds the disc until the pixels genuinely exist. Nothing is a guess. */
.eye__load {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity 420ms ease;
}
.eye__load.is-on { opacity: 1; }
.eye__load img {
  width: 34%; height: auto;
  opacity: 0.5;
  /* white mark -> her red, and cyan is reachable by a duotone but red is
     NOT (searched: the chain tops out at 255,154,56). So it is a tint
     multiplied through a mask instead -- same lesson as the silhouette. */
  filter: brightness(0.42) sepia(1) saturate(14) hue-rotate(-28deg);
  animation: breathe 2.4s ease-in-out infinite;
}
@keyframes breathe {
  0%, 100% { opacity: 0.26; transform: scale(0.985); }
  50%      { opacity: 0.62; transform: scale(1.015); }
}
@media (prefers-reduced-motion: reduce) {
  .eye__load img { animation: none; opacity: 0.45; }
}

/* ── Narrow ─────────────────────────────────────────────────────────────*/
/* ⚠️⚠️ `and (min-height: 620px)` IS NOT TIDINESS, IT IS THE LANDSCAPE FIX.
   This block takes the stage label and the Apply button out of their
   absolute slots and makes each of them a GRID ROW, on the reasoning that
   a narrow viewport has no room beside the disc. True in portrait. On a
   844x390 landscape phone it is exactly backwards: there is 844px of
   width doing nothing and 390px of height to fit five rows into, and the
   two extra rows this block adds (158px of stage + 54px of Apply) were
   over half of the 257px that pushed the focus ring off the screen.
   Width was standing in for "is there room beside the disc", and in
   landscape that proxy inverts. So the stack is now gated on there being
   real height to stack INTO; below that, the desktop arrangement -- label
   beside the disc, Apply beside the ring -- is the one that fits. */
@media (max-width: 860px) and (min-height: 620px) {
  /* No room beside the disc, so the label goes under the view. */
  .stage {
    position: static; translate: none;
    width: min(92vw, 44ch); text-align: center;
    margin: 0 auto;
  }
  /* ⚠️ It used to be hidden here, from when it was one mono line that the
     legend below already said better. It is the DETAIL now -- her note
     was "we need to include more detail on each thing" -- and hiding the
     detail on phones is hiding it from most people. */
  .stage__c { font-size: 0.95rem; line-height: 1.58; }
  .apply { position: static; }
  .eye { grid-template-rows: 1fr auto auto auto auto; }
}
@media (max-width: 720px) and (min-height: 620px) {
  /* ⚠️ The ceiling term stays in the min(). Dropping it here would undo
     the whole landscape fix for any phone narrower than 720 held
     sideways -- which is most of them. */
  :root { --disc: min(46svh, 84vw, calc(100svh - var(--eye-chrome))); }
  .ring, .eye__foot { width: 92vw; }
}
/* The rooftop half is a scene, not a grid, so it only ever needed the
   width half of the old query. */
@media (max-width: 720px) {
  .judge { font-size: clamp(1rem, 5.4vw, 1.5rem); margin-bottom: clamp(80px, 20svh, 160px); }
  .use { left: 8%; top: 42%; }
}

/* ── Short ──────────────────────────────────────────────────────────────
   A landscape phone, a very short laptop window, or a phone with the
   keyboard open. Height is the scarce thing and the disc's ceiling above
   already handles the big term; what is left is the chrome that was
   measured for a tall window.
   ⚠️ The ring keeps its full 54px. It is a drag control and it is the only
   navigation on the page -- it is the LAST thing allowed to get smaller
   on the viewport where it is hardest to hit. */
@media (max-height: 560px) {
  /* The legend is three or four lines of a story and the stage label
     repeats part of it beside the disc. On a 390px-tall screen the disc
     plus the control is the page; the prose has to come down to a size
     that leaves them room rather than pushing them off. */
  .line { font-size: clamp(0.95rem, 1.7vw, 1.2rem); min-height: 2.9em; }
  .stage__n { font-size: clamp(1.1rem, 2.1vw, 1.6rem); }

  /* ⚠️⚠️ THE PARAGRAPH GOES, THE NAME STAYS, AND THAT SPLIT IS THE WHOLE
     DECISION. On a 390px-tall screen the margin beside the disc is 269px
     wide and about 250px tall, and four sentences of Newsreader in it is
     either six raggedly-wrapped lines or type too small to read. Measured
     both ways: at the desktop pin the block came out 172px tall and its
     last line ("are building.") landed on top of the legend's first line
     at y=270; narrowed to clear the disc it grew to ~290px, which forced
     the `1fr` row taller than the screen and put the focus ring back off
     the bottom at y=388 -- i.e. it reintroduced the exact bug this
     section exists to fix.

     So the two halves are treated as the different things they are.
     `.stage__n` is the name of the stop you are currently focused on --
     it OPERATES, it is the instrument reporting its own setting, and it
     is one word. `.stage__c` is the detail, and the legend directly under
     the disc is already telling that story in the same voice. Keeping the
     name costs 24px and keeping the paragraph costs the navigation.
     ⚠️ Only at this height. Her note on the 860px breakpoint still stands
     everywhere else -- "we need to include more detail on each thing",
     and hiding the detail on a PORTRAIT phone would be hiding it from
     most people. A portrait phone has room underneath; this does not. */
  .stage__c { display: none; }
  .stage { width: min(30vw, 300px); }
}

@media (prefers-reduced-motion: reduce) {
  .roof, .eye, .line, .roof__look, .apply { transition: none; }
  .judge__w::after { transition: none; }
}
