/* hero.css — the hero section's box, its sky slot and its scroll cue.

   ⚠️ THE HERO IS TWO ELEMENTS AS OF 2026-08-30 (093 §1): the WebGL starfield
   (hero-sky.css, with the injected fallback plate styled here) and the scroll
   cue. The character layers and the pointer/scan reveal this file was built
   around are gone, and so is INSPIRE before them (090 §2.1).

   ⚠️ INTERIM, NOT FINAL. A new figure and a new effect are coming and have not
   been described. Nothing that survives here should be reshaped to suit a hero
   with no character in it — least of all the layer stack below, whose empty
   slots are where the replacement lands. */

/* Section-level custom properties. A declaration on a child shadows the
   inherited value, so a property written here and re-declared on the consumer
   is a silent no-op — the shape 035 §5 hit twice. Nothing in this block is
   inherited by anything any more; see below. */
#hero {
  background: #05060d;

  /* ⚠️ NOTHING ELSE IS DECLARED HERE ANY MORE (093 §2). This block held
     --reveal-x / --reveal-y for the wireframe mask, and --fig-scale-land /
     --fig-scale-port / --fig-x / --fig-y for the character's transform, plus an
     @media (orientation: portrait) block that chose between the two scales.
     The character and the reveal are both gone; all six went with them.

     The background stays and matters more than it did: with no character layer
     above it, this is what shows wherever the starfield does not reach. */
}

.hero { position: static; }

/* EXPLICIT SIZING IS MANDATORY, NOT COSMETIC (023 §7.1).
   Without it an injected image falls into flow and resizes the section as it
   decodes, moving every pin position below it — the decode-after-pins failure
   by a different route. It is also what makes the boot race safe: out of flow,
   an image landing AFTER ScrollTrigger.refresh() cannot change layout, so no
   second refresh is needed. Measured margin: scrollHeight 1600 -> 4295.

   ⚠️ .hero__galaxy IS NOT IN index.html AND THIS RULE IS NOT DEAD (064 §2.7).
   The fallback is INJECTED at runtime by hero/sky/index.js when WebGL is
   unavailable or ?sky=plate is set. Delete it and a visitor with no GPU gets an
   unstyled block filling the section. ⚠️ Since 2026-09-01 the injected element
   is a <div> carrying a gradient, not an <img> — so the rule is now the ONLY
   thing that draws it, and deleting it means a visitor with no GPU gets
   nothing at all rather than something unstyled.

   ⚠️ DO NOT ADD .hero__sky HERE. object-fit applies to <canvas> and `cover`
   silently CROPS an aspect mismatch instead of showing it (063 §3.1). The
   sky's geometry is in hero-sky.css, with object-fit at `fill`. */
/* ⚠️ A GROUPED SELECTOR THAT LOST TWO OF ITS THREE MEMBERS, NOT A DEAD RULE
   (093 §2). .hero__rendered and .hero__wire were removed on 2026-08-30 with the
   character and the reveal. .hero__galaxy remains and is the reason the rule
   survives — it is injected at runtime when WebGL fails or ?sky=plate is set,
   so it is never in the markup and always looks deletable. It is not. */
.hero__galaxy {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* object-fit is gone with the <img> — this is a <div> now, see below. */
}

/* LAYER STACK (033 §3.1, reduced by 090 §2.1 and 093 §2, slot 3 taken by 106/108,
   slot 2 taken by 132):

     sky/galaxy 1 · trail 2 · title/body 3 · [4 reserved] · [5 reserved] · cue 6

   ⚠️ THIS BLOCK WAS STALE FOR A WHOLE SESSION AND NOTHING STRUCTURAL CAUGHT IT
   (133 §4, 000 §5 entry 21). It listed slot 3 as empty while session 9 had put
   .hero__title and .hero__body there — hero.css:175 and :283 — and hero-sky.css
   carried a second copy that was wrong three ways. A comment that describes a
   layout is a record, and a record goes stale exactly the way a measurement
   does, with no diff to find.

   ⚠️ TWO RESERVED SLOTS NOW, AND THEY STAY RESERVED AND STAY NUMBERED. 4 held
   the rendered character and 5 the wireframe (093 §2); they are the slots
   shaped for the new figure and its effect, which are still coming and still
   undescribed. The numbering is kept rather than compacted for the reason it
   has been kept three times before — an off-by-one in a stack that took two
   documents to get right buys nothing. Renumbering now means renumbering back.

   ⚠️ SLOT 2 IS LOAD-BEARING BEYOND ITS NUMBER. .hero__trail's z-index is what
   confines a trail item below .bar at 71 and .badge at 70 — Xin Lu's
   requirement that the effect never covers the chrome (132 §1 item 6). Two
   other stacking contexts happen to do the same job today and neither is
   guaranteed; 133 §5.2 measures all twelve combinations. See hero-trail.css.

   The cue and the chrome sit above. 036 removed the page-level canvas; .bar and
   .badge are siblings of <main> and are outside this stack entirely. */

/* THE INJECTED FALLBACK PLATE (064 §2.7). Rendered only when WebGL is
   unavailable or ?sky=plate is set. Fallback or not, this rule is load-bearing:
   delete it and a visitor with no GPU gets an unstyled full-size image over the
   character.

   ⚠️ THE DRIFT IS GONE (090 §2.4) AND THE 110% IS NOT. The scroll-coupled
   translate has been removed here and from the canvas in hero-sky.css, so the
   sky has no scroll coupling at all and the no-WebGL rescue is a still image.

   ⚠️ THE WIDTH IS INHERITED FROM THE DRIFT ERA AND IS RETAINED PENDING A
   SEPARATE DECISION — it is not justified by anything on this page any more.
   -5% / 110% existed for exactly one reason: at inset:0 the element is exactly
   viewport-sized, so a translate would expose a bare edge, and widening by 10%
   gave 5% of headroom each side to travel into. With no travel there is nothing
   to give headroom to. Narrowing it to 100% is a LOOK change rather than a
   cleanup — for the canvas it moves uResolution and reframes the starfield, and
   for this plate it changes the crop — so it is Xinlu's under 003 §1.1 and is
   held back as its own item (090 §6). Do not "tidy" it in passing, and do not
   leave the next reader believing a drift still justifies it. */
.hero__galaxy {
  z-index: 1;
  left: -5%;
  width: 110%;

  /* ⚠️ THIS IS A GRADIENT NOW, NOT A 2.3 MB PNG (116 §4). The two galaxy
     plates are deleted; sky/index.js's injectPlate builds a <div> with this
     class instead of a <picture>. It shows a still of the aurora, because a
     starfield fallback under an aurora would simply be wrong.

     ⚠️ AND IT IS DERIVED FROM THE SHADER, NOT EYEBALLED. Over the hero's
     #05060d the aurora composites to intensity * ramp * alpha, so fading to
     transparent and fading to the background are the same operation — which
     is why two stacked gradients express the field rather than approximate
     it. The vertical layer's stops are 1 - (intensity * alpha) sampled every
     4% from the shader's own maths at the mean noise phase (exp(0) = 1);
     coverage reaches zero at 60% down, which is where the stop list ends.

     Measured against a plate re-rendered from the same maths at the deleted
     plates' own dimensions: mean per-channel difference 3.74/255 — 1.5% —
     max 51, concentrated at the two ends of the ramp and smallest in the
     middle. The comparison is committed at PS_Operational_Docs/assets/.

     ⚠️ Change Xin Lu's three colours in shaders.js and this goes stale
     silently — it is the one place they are duplicated. */
  background:
    linear-gradient(to bottom, rgba(0,0,0,0.280) 0%, rgba(0,0,0,0.328) 4%, rgba(0,0,0,0.376) 8%, rgba(0,0,0,0.424) 12%, rgba(0,0,0,0.472) 16%, rgba(0,0,0,0.520) 20%, rgba(0,0,0,0.570) 24%, rgba(0,0,0,0.634) 28%, rgba(0,0,0,0.708) 32%, rgba(0,0,0,0.783) 36%, rgba(0,0,0,0.851) 40%, rgba(0,0,0,0.909) 44%, rgba(0,0,0,0.952) 48%, rgba(0,0,0,0.980) 52%, rgba(0,0,0,0.995) 56%, rgba(0,0,0,1.000) 60%, rgba(0,0,0,1.000) 64%, #05060d 100%),
    linear-gradient(to right, #06b6d4, #b497cf, #5227ff);
}

/* ⚠️ THE CHARACTER LAYERS AND THE REVEAL LIVED HERE (093 §2).
   Removed 2026-08-30, together and deliberately: .hero__rendered and
   .hero__wire at z-index 4 and 5, their shared scale transform, --reveal-r and
   the radial mask that made the cursor spotlight, the .hero--scan band and its
   linear mask, the reduced-motion states for both, and the ?perf=nomask
   control. All of it existed to reveal a wireframe under a rendered figure,
   and there is no figure.

   ⚠️ WHAT A REPLACEMENT MUST NOT ASSUME IT INHERITS. The mask technique here
   was chosen on measurement, not taste — 021/022 tested a moving CSS mask
   gradient against the alternatives ON THIS PAGE, 023 shipped it, and 000 §3.6
   records Xinlu accepting it on Mac Chrome, Android Chrome and iPhone Safari.
   That acceptance now has no subject. It is not a licence for the next effect;
   it is evidence about a technique, and the next effect gets its own gate.

   Two constraints the deleted rules encoded, both worth re-reading before
   anything replaces them:
     - the mask was declared on .hero__wire while --reveal-x / --reveal-y were
       declared on #hero, because a declaration on the consumer shadows the
       inherited value (035 §5, twice);
     - the scan gradient carried FOUR stops and no 0%/100% anchors on purpose:
       a gradient extends its first colour above the first stop and its last
       below the last, and adding `transparent 100%` would place a stop AFTER
       stops that resolve past 100%, which CSS clamps — silently breaking the
       sweep at exactly its end, where nobody is looking. */

/* ── THE HEADING (106 §3.2) ──────────────────────────────────────────────
   z-index 3, the slot 090 §2.1 deliberately left empty for exactly this.
   ⚠️ 4 AND 5 ARE STILL RESERVED for a new figure and its effect (000 §3.6 and
   the layer-stack note above). Nothing here takes them.

   ⚠️ ABSOLUTELY POSITIONED, AND NOT FOR LAYOUT CONVENIENCE. .section is
   `display:flex; align-items:center` (layout.css:10-18), so an in-flow heading
   would be flex-centred AND would give .section__inner real height — and the
   cue's position is resolved against the section box. Measured: .section__inner
   is position:static with height 0 today, because both hero children are out of
   flow. Out of flow, the heading cannot move the cue underneath it. Put
   anything in flow here and the cue moves with it.

   HORIZONTAL CENTRING IS text-align, NOT a transform, and the vertical
   transform is on .hero__title while the jolt is on .hero__type. Two
   transforms on one element is how 035 §5's silent no-op happened twice;
   keeping them on separate elements is also what lets C6 measure centring with
   the jolt running. */
.hero__title {
  position: absolute;
  z-index: 3;
  left: 0;
  right: 0;
  top: var(--type-y, 42.1%);   /* 45.8 -> 42.1, 108 §1.3. A starting value
                                  from ONE capture, not a derived constraint */
  transform: translateY(-50%);
  text-align: center;

  color: #fff;
  font-family: 'Blinker Subset', system-ui, sans-serif;
  font-weight: 400;
  line-height: 1;

  /* ⚠️ --type-scale MULTIPLIES THE WHOLE min(), AND THAT IS NOT STYLISTIC.
     On a portrait phone the WIDTH term binds; on a laptop the HEIGHT term
     does. A scale applied to only one term would move a laptop and be
     completely dead on the phone Xin Lu tunes on — 000 §5 entry 26, a flag
     consumed somewhere other than where its name points. C5's one-phone gate
     and its crossover check both exist to catch exactly that (105 §4.3).

     ⚠️ THE DIVISOR IS 9.108, NOT 8.901, AND THE DIFFERENCE IS THE CARET.
     8.901 em is `Creative Technologist` kerned — the longest of the six
     phrases, re-derived twice (fontTools over the shipped woff2, and Chrome's
     own shaping at 890.11 px per 100 px; 105 §2.1, §2.2). But the thing that
     has to fit on one line is not the text, it is THE RENDERED LINE, and
     .hero__type contains the text span AND the caret span. Blinker's `|` is
     207/1000 units = 0.207 em, so the line is 8.901 + 0.207 = 9.108 em.

     Built with 8.901 this overflowed at every width-bound viewport — measured
     366.33 px against 358 available at 390x844, 407.23 against 398 at 430x932,
     294.66 against 288 at 320x568 — and .section is overflow:hidden, so all
     three were SILENTLY CLIPPED. C4 caught it because it measures width
     against the available box; a line-count check could not have, which is
     exactly why 104 §4.3 forbade one.

     Consequence, stated because it is a size Xin Lu will see: the three
     width-bound viewports render ~2.3% smaller than the first table predicted
     (390 -> 39.31 px, 430 -> 43.70 px, 320 -> 31.62 px). The height-bound
     three are unchanged, and the binding term at each viewport is unchanged.

     Any letter-spacing added here invalidates the constant, and so does
     changing the caret glyph — C4 is what would catch either.

     13.07svh = the reference's 8.197% cap height / 0.627 cap-per-em. */
  font-size: var(--type-size);
}

/* ⚠️ --type-size IS DECLARED HERE, ONCE, AND NOWHERE ELSE (108 §1.2).
   It was computed inline on .hero__title until 2026-09-01. The body copy has
   to derive its size from the heading's, so the expression moved up to the
   one element both consumers inherit from.

   ⚠️ THIS IS THE EDIT IN THIS TASK THAT CAN FAIL INVISIBLY. A second
   declaration of --type-size, or a redeclared --type-margin, on EITHER
   consumer shadows the inherited value (035 §5, twice on this project) — and
   the failure does not look like one: every viewport still reports a
   plausible font-size, with the wrong BINDING PATTERN underneath. C17 checks
   which term binds where and C18 asserts the property resolves to the same
   value at both consumers. Neither is decoration.

   Custom-property substitution happens HERE, at #hero: --type-scale,
   --type-margin and --type-svh are resolved against this element and the
   resulting token stream is what descendants inherit. Two consequences worth
   stating so neither is rediscovered:
     - ?typescale= MUST be written on #hero, not on .hero__title. Written on
       the title it would substitute into nothing and the flag would be dead
       at every viewport — the 000 §5 entry 14 shape. type.js writes it here.
     - the expression uses only viewport units, never em, so it resolves
       identically wherever it is consumed and there is no font-size
       circularity to worry about. Do not introduce one. */
#hero {
  --type-margin: 16px;
  --type-svh: 13.07svh;
  --type-size: calc(var(--type-scale, 1) * min(
    (100vw - 2 * var(--type-margin)) / 9.108,
    var(--type-svh)
  ));
}

@media (min-width: 769px) {
  #hero { --type-margin: 48px; }
}

/* ── THE BODY COPY (108 §1) ──────────────────────────────────────────────
   The first body copy on the page and the only visible sentence describing
   Xin Lu. Real, permanent, visible text — NOT aria-hidden, no typing, nothing
   decorative about it.

   Absolutely positioned for the same reason the heading is (104 §4.3): in
   flow it would give .section__inner real height and the cue's anchor would
   move with it. Measured: .section__inner is still height 0.

   ⚠️ SIZE IS A RATIO WITH A FLOOR, AND THE FLOOR IS NOT A FUDGE. 108 §1.2
   derives 0.1404 from the two reference captures agreeing at 6.40x heading
   cap to body cap, converted through the two faces' cap ratios (Blinker
   0.627, Poppins 0.698). Honouring that ratio alone would give 5.52 px at
   390x844 and 4.44 px at 320x568, because OUR heading is width-bound on a
   phone where the reference's is not (§2). So the ratio governs where the
   heading is generous and 15px floors it where it is squeezed. C17 asserts
   which of the two binds at each viewport, and that structural prediction is
   what would catch --type-size having been shadowed.

   max-width is in em of THIS element, so the measure scales with the copy
   rather than with the heading. line-height 1.73 and opacity 0.82 are both
   measured off the reference (62 px against a 35.8 px body font; body p99.5
   209 against the heading's 255). */
.hero__body {
  position: absolute;
  z-index: 3;
  left: 0;
  right: 0;
  top: var(--body-y, 64.4%);
  transform: translateY(-50%);
  margin: 0 auto;
  max-width: min(32em, 88vw);

  text-align: center;
  color: #f4f4f4;
  opacity: var(--body-dim, 0.82);
  font-family: 'Poppins Subset', system-ui, sans-serif;

  /* ⚠️ 300, AND css/fonts.css MUST DECLARE A 300 FACE FOR THIS TO MEAN
     ANYTHING (110 §1.3). Browsers synthesise bold and never light: with no
     300 face this silently resolves to 400 and renders plausibly, so a
     computed-style read of font-weight would report 300 either way. C16
     asserts the rendered WIDTH against the 300 face's advance and against the
     400's, which is the only reading that can tell them apart.

     Xin Lu rejected 400 as too bold on the s9-body-copy-a1 channel. ?bodyweight=
     switches between the three declared faces on hardware — measured stem-to-cap
     is 0.1000 at 300 against 0.1304 at 400, a 23.3% thinner stroke. */
  font-weight: var(--body-weight, 300);
  line-height: 1.73;
  font-size: clamp(15px, calc(var(--type-size) * var(--body-ratio, 0.1404)), 20px);
}

/* nowrap guarantees ONE LINE — Xin Lu's rule, mobile included — but it does not
   guarantee it FITS: it overflows instead, and .section has overflow:hidden, so
   an overflow is silently clipped. ⚠️ C4 therefore measures WIDTH against the
   available box and never getClientRects().length, which under nowrap is always
   1 and could not fail. */
.hero__type {
  display: inline-block;
  white-space: nowrap;
  will-change: transform;
}

/* The caret is a real | glyph, so it needs no dimensions of its own. Blink by
   CSS keyframes rather than the GSAP tween upstream uses: a permanently-running
   tween buys nothing over a compositor animation that the browser can idle. */
.hero__type-caret {
  animation: hero-caret-blink 0.5s steps(1, end) infinite;
}

@keyframes hero-caret-blink {
  0%, 49.9%   { opacity: 1; }
  50%, 100%   { opacity: 0; }
}

/* 009 §8 treats this path as first-class, and 036 §7.5 requires the static
   values be STATED rather than left as whatever the last frame happened to be.
   No typing, no jolt, no blink; the first phrase shown; caret hidden. The
   <h1>'s hidden sentence is unaffected and still carries the full text. */
@media (prefers-reduced-motion: reduce) {
  .hero__type { transform: none !important; }
  .hero__type-caret { animation: none; opacity: 0; }
}

/* ── THE SCROLL CUE (059 §3, as amended by 059 §14 and rebuilt by 090 §2.2) ──
   Hero only. Check 10 asserts exactly one exists and that its nearest section
   ancestor is #hero.

   ⚠️ THE OVAL RING IS GONE. The label sits on top and the ARROW BELOW IT, the
   same treatment in both orientations. What survived the ring is the arrow's
   APERTURE, and the distinction is the whole of 090 §2.2: the ring was
   decoration, the aperture is the mechanism. 059 §3.2 measured the arrow
   TRANSLATING through a hard clip window and being masked by it — visible top
   edge pinned at the window's top on entry, visible bottom at the window's
   bottom on exit. Remove the aperture and the arrow travels its full height in
   free space and overlaps the label. See cue.js's header, note 1.

   ⚠️ z-index 6 (059 §14 R4). First value clear of the whole stack, and it must
   stay clear: in portrait the cue sits above the figure's top edge, so anything
   below 6 is occluded by the wireframe layer.

   Everything here reads custom properties cue.js writes. Magnitude, colour and
   direction live in this file; JS feeds values. Same division as everything
   else in the hero (036 §7). */
.cue {
  position: absolute;
  z-index: 6;
  left: var(--cue-x, 51.2%);

  /* ⚠️ BOTTOM-ANCHORED SINCE 2026-08-31 (106 §3.4). It was
     `top: var(--cue-y, 79.3%)` with `translate(-50%, -50%)`, and portrait's
     43% put it almost exactly on the vertical centre — 105 §4.1 measured the
     block's top edge at 39.6% of the hero. It had been unconstrained since
     093 deleted the figure it was solved against; the heading is the thing
     that now constrains it.

     THE MECHANISM CHANGE IS THE POINT, NOT NEW CONSTANTS. A top percentage
     cannot know about the iOS home indicator. env() can only be added to an
     offset measured from the edge it protects, so the anchor has to move to
     the bottom for the safe area to mean anything.

     ⚠️ AND THE ALTERNATIVE WAS REJECTED ON A HARD CONSTRAINT, not on taste:
     keeping the percentage and adding a safe-area term needs the cue's own
     RENDERED HEIGHT inside the expression to stop the block overhanging the
     inset — and reading a rendered value is exactly what 090 §2.5 removed
     from cue.js and what its header forbids reintroducing. Bottom-anchoring
     avoids the read by construction (105 §4.1).

     translateX only. The Y half of the old translate centred the block on
     --cue-y; with the block's own bottom edge as the anchor there is nothing
     to centre, and leaving translateY(-50%) in would lift it by half its
     height for no stated reason — two positioning modes on one element, which
     is how 035 §5's silent no-ops bred. */
  bottom: calc(env(safe-area-inset-bottom, 0px) + var(--cue-foot));
  transform: translateX(-50%);

  /* max(), so the floor holds where 3svh is small — 24px at 844x390 landscape,
     where 3svh is 11.7. ?cuefoot= overrides it in px, and it REPLACED ?cuey=
     rather than joining it. */
  --cue-foot: max(24px, 3svh);

  /* ⚠️ --cue-size WAS "THE OVAL'S HEIGHT" AND THERE IS NO OVAL (090 §2.2).
     It survives as the block's one scalar: every dimension below is a ratio of
     it, and the ratios are unchanged so the arrow and the label keep the sizes
     they were tuned to. Read it as "the cue's unit", not as any drawn edge.

     svh rather than svmin, because svmin resolves to WIDTH in portrait and
     gives a 4.2 px label cap on a phone — measured, 060 §3.2.

     ⚠️ AND A PIXEL FLOOR UNDER IT, because svh has the same failure at a
     different viewport (060 §4.9). On a LANDSCAPE PHONE svh bottoms out: at
     844x390 an 8svh unit is 31.2 px and the label cap 3.74 px. The floor's
     justification was always the LABEL CAP, not the ring, so it survives the
     ring intact: floored to 64 px the same viewport gets a 7.7 px cap. It only
     ever RAISES the size and does not engage at either viewport the default was
     tuned against — 72 px at 1440x900, 67.5 px at 390x844. ?cuefloor=0 defeats
     it; cue.js warns when it engages rather than clamping silently.

     ⚠️ --cue-size-cap IS GONE with the portrait gap derivation (090 §2.5). It
     existed to shrink the ring into the space between INSPIRE's ink and the
     figure's crown, by reading the ring's RENDERED height — which is 000 §5
     entry 11, an instrument reading a value its own previous run wrote. There
     is no word to measure against and no ring to measure, and the clearance is
     now a closed-form check (C18) rather than a runtime clamp. Do not
     reintroduce a cap that reads a rendered value. */
  --cue-size-svh: 8vh;                 /* vh works everywhere; svh below is the upgrade */
  --cue-size-floor: 64px;
  --cue-size: max(var(--cue-size-floor), var(--cue-size-svh));
  --cue-label-rest: 0.26;

  /* One foreground colour, and it no longer inverts — see HOVER below. */
  --cue-fg: #f4f4f4;

  /* How far the arrow drops on hover and focus. 10% of the unit: visible as a
     response, small enough that it reads as the cue leaning toward the section
     it points at rather than as a second animation competing with the loop. */
  --cue-nudge: calc(var(--cue-size) * 0.10);

  display: flex;
  flex-direction: column;
  align-items: center;
  /* Gap, label bottom to aperture top: 24% of the unit (059 §3.1's ring-to-label
     gap, kept at its measured value with the two swapped in order). */
  gap: calc(var(--cue-size) * 0.24);

  justify-content: center;

  color: var(--cue-fg);

  /* ⚠️ NOT A CONTROL AS OF 2026-09-03 (145 §5.2). Four declarations went with
     the anchor and each was here for a reason that no longer applies:

       min-width / min-height: 44px  — the tap-target backstop (059 §14 R4).
         There is no tap. Measured before removing, at six viewports rather
         than the two the old comment cited: the block is 0.847 x --cue-size,
         which is 60.94 px at 1440x900 and 54.19 px at 320x568, and forcing
         min-*: 0 on a clone gave byte-identical rects everywhere. The floor
         bound nothing (147 §3.4).
       text-decoration: none               — an anchor property.
       -webkit-tap-highlight-color         — a tap property.
       opacity: var(--cue-opacity, 1)      — the fade's only reader. cue.js was
         its only writer and both are gone (145 §5.1), so a var() with a
         fallback nobody sets would be dead code that reads as live.

     cursor: default is stated rather than inherited: a <div> gets it anyway,
     and saying so is what stops someone restoring `cursor: pointer` on the
     grounds that it looks clickable. C-1c reads this. */
  cursor: default;
}

/* svh is the upgrade over the vh base above: it is fixed to the SMALL viewport
   so the cue does not resize when iOS Safari's address bar collapses mid-scroll
   — the same reasoning that put section heights in svh (000 §3.1), and that
   governed --reveal-r until the reveal was removed (093 §2).
   cue.js rewrites --cue-size-svh when ?cuesize= is set, choosing the unit with
   CSS.supports so it never writes a value this browser cannot parse. */
@supports (height: 100svh) {
  .cue { --cue-size-svh: 8svh; }
}

/* THE APERTURE — a hard clip window, 44% of the unit, and the arrow's only
   parent. Formerly .cue__clip, centred inside the ring; now a block in its own
   right, below the label. Height and the arrow's travel are UNCHANGED, so
   059 §3.2's measurement still describes what is on screen: at yPercent 100 the
   arrow's real bottom is past the window's, which is what pins the visible
   bottom edge for the last 58% of the travel.

   ⚠️ THE WIDTH IS NOT THE RING'S. The ring was --cue-size / 1.70 wide because
   1.70 was Xinlu's mouse proportion; with the ring gone that ratio would be a
   number sizing an invisible box, which is exactly the "hunting for an oval"
   090 §2.2 removes. 20% of the unit is set by the ARROW HEAD: an 11% square
   rotated 45° spans 15.6%, so 20% clears it by 2.2 points and the head is never
   clipped horizontally. --cue-ratio and ?cueratio= are gone with the oval. */
.cue__aperture {
  position: relative;
  width: calc(var(--cue-size) * 0.20);
  height: calc(var(--cue-size) * 0.44);
  overflow: hidden;
  transition: transform 260ms ease;
}

/* The arrow. Height 31% of the unit, shaft 3.8%. Drawn from two elements' worth
   of geometry in one box: the shaft is the background, the head is a rotated
   square. Both are the same colour. */
.cue__arrow {
  position: absolute;
  left: 50%;
  top: 0;
  width: max(1px, calc(var(--cue-size) * 0.038));
  height: calc(var(--cue-size) * 0.31);
  margin-left: calc(var(--cue-size) * -0.019);
  background: var(--cue-fg);
}

.cue__arrow::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 0;
  width: calc(var(--cue-size) * 0.11);
  height: calc(var(--cue-size) * 0.11);
  transform: translate(-50%, -30%) rotate(45deg);
  border-right: max(1px, calc(var(--cue-size) * 0.038)) solid var(--cue-fg);
  border-bottom: max(1px, calc(var(--cue-size) * 0.038)) solid var(--cue-fg);
}

/* The label. Cap height 12% of the unit, block width 89%.
   ⚠️ REST BRIGHTNESS IS 26% WHITE AND THIS IS A KNOWN PROBLEM SHIPPED
   DELIBERATELY. Measured in the real composited hero at Xinlu's marked
   position, the band lands on the galaxy's horizon glow and reads 1.17-1.71:1
   in landscape, 2.06-2.08:1 in portrait (060 §3, R6). At this value the label is
   legible only as each letter passes through the highlight. He asked for the
   recording's colours exactly and was told the measured number. ?cuelabel=
   raises it, and hover now takes it to 1. Do not silently "improve" the rest. */
.cue__label {
  display: flex;
  justify-content: center;
  width: calc(var(--cue-size) * 0.89);
  font-size: calc(var(--cue-size) * 0.12 / 0.72);   /* 0.72 ≈ cap height ÷ em */
  line-height: 1;
  letter-spacing: 0.14em;
  text-indent: 0.14em;                              /* balances the last letter's tracking */
  font-weight: 600;
  color: var(--cue-fg);
}

/* Each letter's opacity is animated individually by cue.js — the wave sweeps
   left to right, one letter at a time, on the same period as the arrow
   (059 §3.3). The rest value here is the pre-JS state, so a page where the
   script never runs still shows a readable-at-rest label rather than nothing. */
.cue__ch { opacity: var(--cue-label-rest, 0.26); }

/* ── HOVER (Xinlu's ruling, 090 §2.2) ──────────────────────────────────
   ⚠️ DECORATION, NOT AN AFFORDANCE, AS OF 2026-09-03. The nudge is kept — 090
   §2.2 is Xinlu's ruling and it stands — but the cue is no longer a control,
   so this signals nothing and promises nothing. It is a hover flourish on a
   decorative element. :focus-visible went from all three rules below: a <div>
   with no tabindex cannot receive focus, so those selectors could never fire.
   ⚠️ THE FOREGROUND/BACKGROUND INVERSION IS GONE AND MUST NOT COME BACK.
   059 §3.4 measured the affordance as the whole RING filling at once, with the
   arrow inverting to dark against that white fill. With no ring the fill has
   nowhere to land, and the inversion alone would set --cue-fg to #05060d
   against #hero's #05060d — an arrow that disappears on hover and, worse, on
   :focus-visible, where the outline would frame an empty box.

   What replaces it: the arrow NUDGES DOWNWARD and the label goes to full
   opacity. The nudge is on the APERTURE, not the arrow — cue.js animates the
   arrow's own yPercent continuously and a transform here would be overwritten
   on the next frame. Moving the window moves the arrow with it. */
.cue:hover .cue__aperture {
  transform: translateY(var(--cue-nudge));
}

/* ⚠️ !important, and the reason is the one intro.css:418 already records:
   cue.js writes the wave's opacity INLINE on each letter, and a plain rule
   cannot outrank an inline style. Without it this declaration is a silent
   no-op — which looks exactly like a hover that was never wired. */
.cue:hover .cue__ch {
  opacity: 1 !important;
}

/* 059 §9. No loop, no wave, and the transitions go too: the hover nudge may
   remain per §9, but it arrives instantly rather than easing. cue.js sets the
   arrow centred at full opacity and the label to full opacity on this path;
   see its header for why the label does NOT rest at 26% there. */
@media (prefers-reduced-motion: reduce) {
  .cue__aperture { transition: none; }
}
