/* MOSI Three.js stage — cinematic shell.
   Palette from the boss's references: near-black navy, one deep blue, one bright electric blue, and
   off-white sections that dissolve into the blue at their lower edge. Typography is a neo-grotesque
   sans throughout (the references contain no serif anywhere). */
:root {
  --font-sans:"Inter","Segoe UI Variable Text","Segoe UI",system-ui,-apple-system,"Helvetica Neue",Arial,sans-serif;
  --night:#060A18; --night-2:#0A1024; --deep:#0E2352; --blue:#1A5FC8;
  --electric:#5566FF; --electric-bright:#7280FF; --sky:#8CC0FF;
  --line-blue:#9EC1E8; --line-blue-soft:#B7C9DD;
  --accent:var(--electric); --accent-bright:var(--electric-bright); --accent-pale:var(--sky);
  --paper:#F7F7F8; --paper-2:#FAFAFA;
  --ink-light:#0B0C12; --ink-light-2:#4A5060;
  --ink:#F4F8FF; --ink-2:#C2CEDF; --ink-3:#93A7C0; --warm:#FF9F0A;
  --line:rgba(158,193,232,.26); --line-strong:rgba(158,193,232,.46);
  --glass:linear-gradient(150deg, rgba(10,14,23,.74), rgba(5,7,13,.56));
}
* { box-sizing:border-box; }
html { background:var(--night); color:var(--ink); scroll-behavior:auto; overflow-x:clip; }
body { margin:0; min-width:320px; font-family:var(--font-sans); background:var(--night); letter-spacing:-.006em; overflow-x:clip; -webkit-font-smoothing:antialiased; }
a { color:inherit; text-decoration:none; }
a:focus-visible, button:focus-visible { outline:3px solid var(--accent-bright); outline-offset:4px; }
.skip-link { position:fixed; left:12px; top:-60px; z-index:9; background:var(--paper); color:#0B1A33; padding:10px; }
.skip-link:focus { top:12px; }
.space-stage { position:fixed; inset:0; z-index:0; overflow:hidden; pointer-events:none; }
#scene-canvas { position:absolute; inset:0; width:100%; height:100%; display:block; }
.space-vignette { position:absolute; inset:0; background:radial-gradient(125% 95% at 64% 40%, rgba(5,7,13,0) 32%, rgba(5,9,18,.46) 72%, rgba(4,6,11,.9) 100%), linear-gradient(90deg, rgba(4,6,11,.95) 0%, rgba(5,9,18,.78) 26%, rgba(7,13,26,.28) 50%, rgba(10,20,40,.06) 72%, rgba(5,9,18,.34) 100%), linear-gradient(0deg, rgba(4,6,11,.9) 0%, rgba(5,9,18,.46) 26%, transparent 58%); }
.site-header, .section-shell, .site-footer, .frame-selector { position:relative; z-index:1; }
.site-header { width:min(1440px,100%); margin:auto; padding:26px clamp(20px,4vw,64px); display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:24px; border-bottom:1px solid var(--line); backdrop-filter:blur(3px); }
.wordmark { font-size:23px; font-weight:800; letter-spacing:.24em; color:var(--ink); }
.wordmark span { color:var(--warm); }
nav { display:flex; gap:30px; font-size:12px; text-transform:uppercase; letter-spacing:.14em; color:var(--ink-2); text-shadow:0 1px 14px rgba(4,11,26,.85); }
nav a:hover, .header-cta:hover { color:var(--accent-pale); }
.header-cta { justify-self:end; font-size:12px; padding:10px 0; text-transform:uppercase; letter-spacing:.14em; border-bottom:1px solid var(--accent-bright); }
.section-shell { width:min(1440px,100%); margin:auto; padding-left:clamp(20px,7vw,120px); padding-right:clamp(20px,7vw,120px); }
.hero { min-height:calc(100vh - 88px); padding-top:clamp(96px,15vh,170px); padding-bottom:90px; max-width:820px; }
.eyebrow { margin:0 0 20px; color:#BEDAFF; font-size:12px; line-height:1.5; font-weight:700; letter-spacing:.2em; text-transform:uppercase; text-shadow:0 2px 16px rgba(3,9,20,.9), 0 0 3px rgba(3,9,20,.88); }
.eyebrow span { color:var(--warm); }
/* r29: 11px was the only micro-label size in the sheet - nav, .eyebrow and .stage-caption are all 12px -
   so the tier is 12px now and the tracking comes back a little (0.2em -> 0.16em) because very open
   tracking on very small caps costs legibility faster than it buys elegance. */
/* r39 · ONE MOTION LANGUAGE. Three custom properties and one easing, used by every moving thing on the
   page (the entrance, the recession, the section raises, the world-marks count). Serialising these into
   the stylesheet means the JS and the CSS cannot drift into two different feels - which is what "整体动效
   也不行" was describing: each effect was tuned on its own. Durations follow the measured budget in the
   project notes (single element <=400ms; entrance <=2s total; >500ms reads slow at NN/g's 100-500ms). */
:root {
  --mo-fast: 180ms;
  --mo-base: 340ms;
  --mo-slow: 520ms;
  --mo-enter: 900ms;
  --mo-ease: cubic-bezier(.22,.61,.36,1);   /* decelerating: arrives, never overshoots */
  --mo-ease-in: cubic-bezier(.55,.06,.68,.19);
  --mo-lift: 18px;                          /* one displacement step, used by every block */
  --mo-recede: 96px;                        /* the hero's departure distance */
}
.section-index { margin:0 0 20px; color:var(--accent-pale); font-size:12px; line-height:1.5; font-weight:700; letter-spacing:.16em; text-transform:uppercase; }
/* r29: THE SECTION LABEL IS ONE PATTERN USED SIX TIMES, so it is defined once here instead of being
   restyled per section. Two tones from a single inherited colour (the sections override .section-index to
   --electric on the light ones), so the pattern holds in both themes: a heavier figure, a lighter
   descriptor.
   The figures are tabular on purpose. These labels are read as a sequence - 01, 02, 03 - and Inter's
   default proportional digits have different widths, so the left edge of the page breathed in and out by
   a pixel or two on every scroll. It is a small thing that is very visible once you scroll the page
   rather than look at one screen. */
.section-index .num { font-weight:800; letter-spacing:.10em; font-variant-numeric:tabular-nums; }
/* A warm-to-neutral split, not a fade: at 12px the difference between weight 700 and 800 is invisible, so
   the hierarchy is carried by colour temperature instead - the figure keeps the section's accent, the
   descriptor drops to the neutral. Opacity stays at 1 so neither half loses legibility over the moving
   scene, which is the one background here that cannot be quietened (the user has ruled on that twice). */
.section-index .lbl { font-weight:600; letter-spacing:.13em; color:var(--ink-3); }
/* r39: THE SAME NUMBER, RENDERED TWICE - the page's half of it.
   Each numbered section owns a structure in the 3D scene whose form is built from THAT SECTION'S OWN
   numbers, and this row carries one mark per piece the structure is made of (js/entity-links.js). The
   counts are not decoration and not a second source: both sides read the same FACTS, and
   qa/r39-entity-dom.cjs asserts mark-per-piece equality against MosiStage.entityAnchors(). The user's
   note was that the structures "are not really part of the page interaction" - being made of the same
   numbers is what makes them part of it, rather than being lit by it.
   It rides on the existing .section-index line (no new block, no overlay, nothing covering the scene),
   sits on the text baseline, and carries no colour of its own: an on-mark is the section's accent, an
   off-mark is a hairline. .world-marks stays fully legible at rest - the strip is not an animation that
   has to be waited for. */
.world-marks { display:inline-flex; align-items:center; gap:3px; margin-left:10px; vertical-align:1px; }
.world-marks i {
  display:block; width:2px; height:9px; background:currentColor; opacity:.22;
  transform-origin:50% 100%; transition:opacity .34s cubic-bezier(.22,.61,.36,1), transform .34s cubic-bezier(.22,.61,.36,1);
}
.world-marks i.is-on { opacity:.92; transform:scaleY(1.0); }
.world-marks i:not(.is-on) { transform:scaleY(.45); }
@media (prefers-reduced-motion: reduce) { .world-marks i { transition:none; } }
.section-light .section-index .lbl { color:var(--ink-light-2); }
.hero h1 { max-width:760px; margin:0; font-family:var(--font-sans); font-size:clamp(50px,7vw,104px); line-height:1.02; font-weight:500; letter-spacing:-.028em; color:var(--ink);
  /* r41 · EDGE DEFINITION, NOT A MASK. Measured with qa/r41-contrast.cjs: the eyebrow band sits on a
     background of mean luminance 132.7 (max 211) while the type itself is rgb(190,218,255) — a contrast
     ratio of about 1.8:1, which is the "small print is unreadable over the snow" note the reviews kept
     making. The fix is a tight dark halo around the glyphs (zero offset, small radius) plus the existing
     long ambient falloff: it darkens only the pixels immediately outside each letter, so the background
     is not covered and no sheet is laid over the scene — which is the thing the user has rejected twice.
     The offset layer still comes from the SCENE's key light (--sun-x/--sun-y, set by js/scene-fusion.js). */
  text-shadow:
    calc(var(--sun-x, -0.5) * 7px) calc(var(--sun-y, 0.5) * 7px) 30px rgba(3,5,12,.95),
    0 0 3px rgba(3,5,12,.85),
    0 0 9px rgba(3,5,12,.55),
    0 1px 3px rgba(3,5,12,.8); }
.hero-copy { max-width:470px; margin:30px 0 12px; font-size:21px; line-height:1.34; color:#FFFFFF;
  text-shadow:
    calc(var(--sun-x, -0.5) * 6px) calc(var(--sun-y, 0.5) * 6px) 24px rgba(3,9,20,.95),
    0 0 3px rgba(3,9,20,.88),
    0 0 10px rgba(3,9,20,.6),
    0 1px 3px rgba(3,9,20,.8); }
.hero-detail { max-width:540px; margin:0; color:#E6EEF9; line-height:1.7; font-size:16px; letter-spacing:.005em;
  text-shadow:
    calc(var(--sun-x, -0.5) * 5px) calc(var(--sun-y, 0.5) * 5px) 22px rgba(3,9,20,.96),
    0 0 3px rgba(3,9,20,.9),
    0 0 9px rgba(3,9,20,.62); }
.hero-actions { display:flex; flex-wrap:wrap; gap:13px; margin-top:36px; }
.button { display:inline-flex; align-items:center; gap:16px; padding:15px 20px; border:1px solid var(--line-strong); font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.14em; transition:transform .18s ease, box-shadow .18s ease, background .18s ease; }
.button:hover { transform:translateY(-2px); }
/* A blue button on a blue hero disappeared into the scene: the primary action is the one place the
   palette goes light, which is also what the references do with their white-on-navy panels. */
.button-primary { background:#F7F8FF; color:#0B1030; border-color:transparent; box-shadow:0 16px 44px rgba(5,8,20,.55); }
.button-primary:hover { background:#FFFFFF; box-shadow:0 18px 50px rgba(85,102,255,.45); }
.button-quiet { background:rgba(9,21,43,.6); color:var(--ink); border-color:var(--line-strong); backdrop-filter:blur(6px); }
.stage-caption { margin-top:76px; font-size:12px; color:#CFDDEE; text-transform:uppercase; letter-spacing:.14em;
  /* r41: this line sits on the darkest part of the scene (measured mean luminance 19.8) and carried NO
     shadow at all, so it was the one text block with no defence when the camera drifted. */
  text-shadow: calc(var(--sun-x, -0.5) * 4px) calc(var(--sun-y, 0.5) * 4px) 18px rgba(3,9,20,.9), 0 0 3px rgba(3,9,20,.85); }
.caption-dot { display:inline-block; width:6px; height:6px; margin-right:8px; border-radius:50%; background:var(--warm); box-shadow:0 0 12px var(--warm); }
/* Two sections sit on paper, not on glass: the references pair near-black panels with off-white
   ones, and the light one dissolves into electric blue at its lower edge. Everything inside a light
   section flips to dark ink and drops the scene text-shadow. */
.section-light {
  /* r19: THE PAGE WAS PAINTING OVER THE SCENE. These two sections used to hold a 0.86-0.90 alpha wash
     from 10% to 74% of their height, which erased the 3D behind them - the client's words were "these two
     screens cover the background, it is odd". The wash is now a light tonal veil (peak 0.42) and the ink
     gets its contrast from a LOCAL halo around the text block instead: the scene stays visible on every
     side of the copy, and the type reads as something lying IN the image rather than on a page above it. */
  /* 0.42 still read as a flat pale field over the dark sky (the veil is over BLACK there, so even a
     third alpha turns the whole upper area white-grey). Down to 0.20-0.24; the halo behind the copy does
     the legibility work, the veil only has to take the edge off the scene. */
  /* r44 · THE WHITE PAGES WERE TOO HARD-EDGED.
     The two light sections are the only place in the page where the scene is covered by a flat pale field,
     and at 0.24/0.20 alpha over a BLACK sky the veil reads as an abrupt white slab - the client's note is
     "那两页白色的页面也很生硬". Three changes, all of them about dissolving the edges:
       - the veil peaks lower (0.24 -> 0.16) and starts higher up the section, so it has room to ramp
         instead of arriving as a band;
       - the colour is warmed very slightly (247,247,248 -> 246,245,242) so it does not read as pure
         printer paper against a blue-violet scene;
       - the top and bottom edges get longer ramps, which is what turns a slab into a light gradient. */
  background:linear-gradient(180deg,
    rgba(246,245,242,0) 0%,
    rgba(246,245,242,.07) 4%,
    rgba(246,245,242,.16) 14%,
    rgba(246,245,242,.15) 66%,
    rgba(180,190,255,.22) 86%,
    rgba(120,134,255,.32) 96%,
    rgba(85,102,255,.36) 100%);
  color:var(--ink-light);
  margin-top:0;
}
/* The halo. A soft radial light behind each text block, no hard card edge, so the type stays legible
   where it sits while the frame around it keeps the landscape. */
.section-light .meaning-panel,
.section-light .partner,
.section-light .cycle-intro,
.section-light .cycle-list {
  position:relative;
}
.section-light .meaning-panel::before,
.section-light .partner::before,
.section-light .cycle-intro::before,
.section-light .cycle-list::before {
  /* r44: the halo was 0.95 alpha at its centre and covered 115% of the block - a solid card, which is
     what made the type read as "on top of" the scene rather than in it. It is now a smaller, softer light
     (peak 0.62, 92% spread): enough to hold the ink, not enough to be a surface. The 3D keeps showing on
     every side, and through the falloff. */
  content:""; position:absolute; z-index:-1; inset:-7% -9%;
  background:radial-gradient(92% 88% at 20% 44%, rgba(250,250,252,.62) 0%, rgba(250,250,252,.50) 40%, rgba(250,250,252,.22) 68%, rgba(250,250,252,0) 100%);
  border-radius:34px; pointer-events:none;
}
.section-light.section-shell h2, .section-light.section-shell h3 { color:var(--ink-light); text-shadow:none; }
/* r44: with the veil down to 0.16 the dark ink sits directly on the scene in places, so it gets the
   inverse treatment of the dark sections' type - a light halo instead of a dark one. This is the same
   mechanism (edge definition around the glyphs), not a mask: the scene stays visible between the letters
   and everywhere around the block. */
.section-light .meaning-panel p, .section-light .partner>p, .section-light .cycle-intro>p,
.section-light .cycle-list p, .section-light .cycle-note, .section-light .section-index {
  color:var(--ink-light-2);
  text-shadow:0 0 3px rgba(250,250,252,.85), 0 0 10px rgba(250,250,252,.55), 0 1px 0 rgba(250,250,252,.35);
}
.section-light.section-shell h2, .section-light.section-shell h3 {
  text-shadow:0 0 4px rgba(250,250,252,.75), 0 0 14px rgba(250,250,252,.45);
}
.section-light .section-index { color:var(--electric); }
.section-light .scrim::before { display:none; }
.section-light h2::before, .section-light h3::before { background:linear-gradient(90deg, var(--electric), rgba(67,83,255,0)); }
.section-light .stage-number { color:var(--electric); }
.section-light .button-quiet { background:rgba(11,12,18,.05); color:var(--ink-light); border-color:rgba(11,12,18,.30); backdrop-filter:none; }
.section-light .button-primary { background:#3730A3; box-shadow:0 14px 40px rgba(55,48,163,.3); }
/* the italic second line is the strongest sentence on the page: it has to be dark on paper */
.section-light em { color:#2E3478; font-style:italic; text-shadow:none; }
.section-light .cycle-note { border-left-color:var(--electric); }
/* On paper the two-column glass layout leaves a dead third: stack the index above the panel and
   let the copy column carry the section. */
.section-light .meaning { grid-template-columns:minmax(0,1fr); align-items:start; padding-top:110px; padding-bottom:130px; min-height:auto; }
.section-light .meaning-panel { max-width:860px; padding:0; }
.section-light .partner { padding-top:110px; padding-bottom:140px; min-height:auto; }
.section-light .partner>p { color:var(--ink-light-2); }
/* Typography motion (paired with splitWords in js/ui.js). Words start displaced and fade up; the
   whole headline is settled within ~600ms and every element's own transition is 320ms. */
.hero h1 .w { display:inline-block; opacity:0; transform:translateY(14px) rotate(1.2deg); transition:opacity .32s ease-out, transform .32s cubic-bezier(.2,.7,.2,1); }
.hero h1.typed .w { opacity:1; transform:none; }
.line-in { opacity:0; transform:translateY(12px); transition:opacity .34s ease-out, transform .34s cubic-bezier(.2,.7,.2,1); }
.line-in.in-view { opacity:1; transform:none; }
@media (prefers-reduced-motion:reduce) { .hero h1 .w, .line-in { opacity:1 !important; transform:none !important; } }
/* Glass sheets: content floats over the scene, it never turns the page into a document. */
.scrim { position:relative; }
.scrim::before { content:""; position:absolute; z-index:-1; inset:-36% -4%; background:radial-gradient(closest-side, rgba(4,11,26,.70) 0%, rgba(5,13,30,.46) 56%, rgba(5,13,30,0) 100%); }
.reveal { opacity:0; transform:translateY(22px); transition:opacity .42s ease-out, transform .42s ease-out; }
.reveal.in { opacity:1; transform:none; }
.meaning { min-height:94vh; display:grid; grid-template-columns:1fr minmax(320px,640px); gap:clamp(30px,8vw,140px); align-items:center; padding-top:90px; padding-bottom:90px; }
.meaning-panel { padding:4px 0 4px 0; }
.meaning h2, .cycle h2, .partner h2 { margin:0; font-family:var(--font-sans); font-weight:500; letter-spacing:-.024em; font-size:clamp(38px,4.8vw,78px); line-height:1.06; color:var(--ink); text-shadow:0 4px 28px rgba(3,5,12,.95), 0 1px 3px rgba(3,5,12,.8); }
.meaning em { color:#DCE9FF; font-style:italic; text-shadow:0 3px 22px rgba(3,9,20,.95); }
/* r29: :not(.section-index) IS LOAD-BEARING. These three selectors are (0,1,1) - one class plus one type
   - and .section-index is (0,1,0), so the body-copy rule was silently outranking the label rule in every
   section whose label happens to be a direct child of these containers. Measured on the live page:
   .meaning .section-index came out 11px in the accent colour as designed, while .cycle .section-index
   came out 15px in rgb(215,227,242) - i.e. two of the six section labels had lost their size, tracking,
   weight and colour and were rendering as ordinary body text. That is why the label hierarchy read as
   inconsistent rather than systematic. */
.meaning-panel p:not(.section-index), .cycle-intro>p:not(.section-index), .partner>p:not(.section-index) { color:#D7E3F2; line-height:1.7; max-width:470px; font-size:15px; text-shadow:0 2px 18px rgba(3,9,20,.9); }
.cycle { min-height:104vh; padding-top:40vh; padding-bottom:104px; display:grid; grid-template-columns:minmax(250px,.85fr) minmax(420px,1.15fr); gap:clamp(40px,9vw,150px); align-items:end; }
.cycle-intro { padding:22px 0 0; }
.cycle-intro h2::before, .meaning h2::before, .partner h2::before { content:""; display:block; width:76px; height:2px; margin-bottom:22px; background:linear-gradient(90deg, var(--accent-bright), rgba(79,70,229,0)); }
.cycle-list { list-style:none; margin:0; padding:0; display:grid; grid-template-columns:1fr; gap:0; }
.cycle-list li { min-height:0; padding:15px 0; background:transparent; display:grid; grid-template-columns:56px 1fr; gap:16px; align-items:baseline; transition:transform .22s ease, border-color .22s ease; }
.cycle-list li:hover { transform:translateX(8px); }
.stage-number { color:var(--accent-pale); font-size:11px; font-weight:700; letter-spacing:.14em; font-variant-numeric:tabular-nums; }
.cycle-list h3 { display:inline; margin:0; font-family:var(--font-sans); font-size:clamp(21px,2vw,30px); font-weight:500; letter-spacing:-.018em; color:var(--ink); }
/* r29: 13.5px was the one value in the whole stylesheet that belonged to no scale - every other size
   here is a clean step (11/12/15/16/18/21/...). 14px costs nothing visually and puts the body copy back
   on the system. The 1.6 line-height is set against the inline run beside a 21-30px heading, where 1.55
   crowded the two together. */
.cycle-list p { display:inline; margin:0 0 0 12px; color:#D7E3F2; font-size:14px; line-height:1.6; }
.cycle-note { grid-column:2; margin:0; max-width:580px; padding:22px 0 0 26px; color:var(--ink-2); border-left:2px solid var(--warm); line-height:1.66; }
.partner { min-height:82vh; padding-top:130px; padding-bottom:140px; max-width:1000px; }
.partner-inner { padding:26px 0 0; }
.partner h2 { max-width:800px; }
.partner>p { font-size:18px; margin:26px 0 34px; }
.site-footer { display:grid; grid-template-columns:1fr minmax(300px,560px); gap:20px; padding:38px clamp(20px,7vw,120px); border-top:1px solid var(--line); background:rgba(5,13,30,.92); backdrop-filter:blur(8px); color:var(--ink-3); font-size:12px; line-height:1.6; }
.site-footer p { margin:0; }
.site-footer p:first-child { color:var(--warm); font-weight:700; letter-spacing:.2em; }
.frame-selector { position:fixed; right:30px; bottom:30px; left:auto; transform:none; width:auto; display:flex; align-items:center; gap:0; padding:0; background:transparent; border:0; box-shadow:none; opacity:1; transition:opacity .35s ease; }
body.is-scrolling .frame-selector { opacity:0; }
.frame-selector>p:first-child { position:absolute; width:1px; height:1px; margin:-1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }
.frame-tabs { display:flex; flex-direction:row; gap:18px; }
.frame-tabs button { position:relative; width:9px; height:9px; min-height:0; border-radius:50%; border:1px solid rgba(158,193,232,.5); background:rgba(9,21,43,.5); color:transparent; cursor:pointer; font:inherit; font-size:0; padding:0; transition:transform .18s ease, background .18s ease, border-color .18s ease; }
.frame-tabs button:hover { transform:scale(1.35); border-color:var(--line-blue); }
.frame-tabs button span, .frame-tabs button .tab-label { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }
.tab-label { font-style:normal; }
.frame-tabs button[aria-selected="true"] { background:var(--warm); border-color:var(--warm); box-shadow:0 0 16px rgba(255,159,10,.8); transform:scale(1.15); }
.frame-tabs button[aria-selected="true"] span { color:transparent; }
.frame-status { position:absolute; width:1px; height:1px; margin:-1px; padding:0; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }
@media (max-width:760px) {
  .site-header { grid-template-columns:1fr auto; padding:18px 20px; }
  .site-header nav { display:none; }
  .header-cta { font-size:9px; }
  .hero { min-height:auto; padding-top:54vh; padding-bottom:82px; max-width:none; }
  .hero h1 { font-size:clamp(44px,13vw,66px); }
  .hero-copy { font-size:17px; }
  .hero-detail { font-size:15px; }
  .stage-caption { font-size:12px; color:#D7E3F2; }
  .space-vignette { background:radial-gradient(120% 70% at 60% 30%, rgba(7,18,39,0) 30%, rgba(7,18,39,.5) 78%, rgba(5,13,30,.9) 100%), linear-gradient(0deg, rgba(5,13,30,.94) 0%, rgba(11,26,51,.30) 46%, rgba(11,26,51,.06) 100%); }
  .meaning { min-height:auto; display:block; padding-top:82px; padding-bottom:82px; }
  .meaning-panel { padding:28px 22px; }
  .cycle { min-height:auto; display:block; padding-top:48vh; padding-bottom:88px; }
  .cycle-intro { padding:24px 20px; }
  .cycle-list { grid-template-columns:1fr; margin-top:36px; }
  .cycle-note { grid-column:1; padding:20px; }
  .partner { min-height:auto; padding-top:86px; padding-bottom:130px; }
  .partner-inner { padding:28px 22px; }
  .site-footer { grid-template-columns:1fr; padding:26px 20px 60px; }
  body { padding-bottom:46px; }
  /* phones: the narrative is pure scroll; the chapter control is a desktop review affordance.
     Keeping it here measured 1632px² of overlap with the hero paragraph at 390/430 wide. */
  .frame-selector { display:none; }
  .frame-selector:hover, .frame-selector:focus-within { opacity:1; }
  .frame-selector>p:first-child, .tab-label { display:none; }
  .frame-tabs button { padding:7px 10px; font-size:10px; }
  .stage-caption { margin-top:50px; }
  .hero-actions { margin-top:28px; }
  .button { width:100%; justify-content:space-between; }
}
@media (prefers-reduced-motion:reduce) { *, *::before, *::after { scroll-behavior:auto !important; transition-duration:.001ms !important; } .reveal { opacity:1; transform:none; } }

/* Paper sections: these overrides sit at the end of the file on purpose. The base rules for
   .meaning em / .partner h2 / .section-index come later in source order than the block above, so
   equal-specificity declarations there would win and leave white text on a white panel. */
.section-light em, .section-light .meaning em { color:#2E3478; font-style:italic; text-shadow:none; }
.section-light .partner-inner .section-index, .section-light .section-index { color:var(--electric); }
.section-light .partner h2, .section-light .meaning h2, .section-light h3 { color:var(--ink-light); text-shadow:none; }
.section-light .meaning-panel p, .section-light .partner>p, .section-light .cycle-list p { color:var(--ink-light-2); text-shadow:none; }
.section-light .partner .button-primary, .section-light .meaning .button-primary { background:#3730A3; }

/* S4: the hero copy integrates into the scene instead of covering it.
   No plate, no dark wash: legibility comes from colour, a soft shadow and letter treatment, so the
   terrain, clouds and ridge line stay fully visible behind the type. */
.hero { padding-top: clamp(74px, 12vh, 138px); position: relative; }
/* r44 · THE TYPE HAS TO BELONG TO THE PICTURE, NOT SIT ON IT.
   The review's verdict was "text looks pasted on top: flat white type with soft shadow, no scene lighting
   or depth interaction". Parallax, recession and a light-derived shadow direction are all real, but none
   of them changes how the letters are MADE, so at rest the type is a flat layer pasted over a rendered
   image. These two are the cheap, honest fixes:
     - a very slight per-line tilt, driven by the same scroll progress the scene is on, so the block is not
       a perfectly level rectangle inside a scene whose horizon is not level either;
     - a translucent white ink (alpha .93) so the darkest part of the scene reads faintly THROUGH the
       strokes, which is what makes type look like it is in the air of the image rather than on a sheet
       above it. Both are transform/colour only, and neither covers the scene. */
.hero h1 { --tilt: 0deg; transform: rotate(var(--tilt)); }
.hero h1 .w { color: rgba(255,255,255,.93); }
.hero h1 {
  font-size: clamp(40px, 5.2vw, 80px);
  max-width: 640px;
  letter-spacing: -0.02em;
  background: linear-gradient(96deg, #FFFFFF 0%, #EAF1FF 42%, #A9C2FF 78%, #7E9BFF 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  text-shadow: 0 2px 26px rgba(4,8,20,0.45);
  filter: drop-shadow(0 1px 10px rgba(6,12,28,0.55));
}
.hero-copy, .hero-detail { max-width: 430px; color: #F2F6FF; text-shadow: 0 1px 14px rgba(4,8,20,0.55); }
.hero-detail { color: #D8E3F6; }

/* Entrance: each word rises out of its own line mask (transform/opacity only), 380ms per word with a
   46ms stagger - inside the 100-500ms band, no blur, no filter animation. */
.hero h1 .w {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.62em) rotate(0.6deg);
  transition: opacity 300ms ease-out, transform 380ms cubic-bezier(0.22, 0.68, 0.24, 1);
}
.hero h1.typed .w { opacity: 1; transform: none; }
/* The last scroll stop has to land on the partner block: with a short block the viewport centre fell
   inside the pilot section and the state/section mapping check failed. */
.partner { min-height: 96vh; display: flex; align-items: center; }
.partner-inner { width: 100%; }

/* r12: world-anchored labels. DOM, not sprites: at 0.3-0.8 rad from the camera a 3D text quad is a
   few pixels of mush, while a DOM pill stays crisp and keeps the brand type. They are positioned every
   frame by projecting the pillar tops; opacity is driven by the same reveal as the pillars, so the
   opening screen carries none of them. */
.world-labels {
  position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 2;
}
.world-label {
  position: absolute; left: 0; top: 0; display: flex; align-items: baseline; gap: .55em;
  padding: .34em .7em .36em; border: 1px solid rgba(150, 190, 255, .28);
  border-radius: 2px; background: rgba(6, 12, 26, .52); backdrop-filter: blur(3px);
  color: #e8eefc; font-size: 12px; letter-spacing: .10em; text-transform: uppercase;
  white-space: nowrap; will-change: transform, opacity; transition: opacity .18s linear;
  box-shadow: 0 0 22px rgba(90, 140, 255, .12);
}
.world-label__n { color: #9fc4ff; font-variant-numeric: tabular-nums; }
.world-label__t { color: rgba(232, 238, 252, .82); }
.world-label--key { border-color: rgba(180, 214, 255, .45); background: rgba(8, 18, 38, .62); }
.world-label--key .world-label__n { color: #cfe2ff; font-size: 13px; }
@media (max-width: 760px) { .world-labels { display: none; } }


/* r12b: the leader line. The pill hangs 20px above its anchor and this hairline is the visible
   attachment to the pillar top - the thing the first pass was missing. */
.world-label::after {
  content: ""; position: absolute; left: 50%; top: 100%; width: 1px; height: 20px;
  background: linear-gradient(rgba(150, 190, 255, .55), rgba(150, 190, 255, .08));
}
.world-label__n { font-weight: 600; }


/* r20: the brand particle piece. Its own full-width beat between the pilot and the partnership call, with
   its own canvas: a second, much lighter particle system (34k points, no post-processing) that assembles
   the wordmark out of a scatter field and then folds it into the six-step ring. It is the one place on the
   page where the motion is the subject rather than the background. */
/* r22: THE PIECE IS GROUNDWORK NOW, NOT AN EVENT.
   The client's note: the logo took too long to appear, and the piece should be spread across the whole page
   as a bedding layer rather than something you wait for. The canvas is no longer inside the section - it is
   a fixed full-viewport layer between the space scene (z 0) and the copy (z 1), at reduced strength, and
   the wordmark assembles within about a second of arrival and then simply stays, breathing, for the whole
   page. */
.brand-piece {
  position:relative; z-index:1; min-height:46vh; display:grid; align-items:end;
  padding:clamp(40px,8vw,120px) clamp(20px,7vw,120px);
}
.brand-piece canvas {
  position:fixed; inset:0; width:100vw; height:100vh; display:block;
  z-index:0; pointer-events:none; opacity:.5; mix-blend-mode:screen;
}
.brand-piece__copy { position:relative; max-width:640px; }
.brand-piece h2 { margin:0 0 14px; font-size:clamp(30px,4.4vw,58px); line-height:1.02; letter-spacing:-.02em; }
.brand-piece p { margin:0; max-width:46ch; color:var(--ink-2, rgba(232,238,252,.74)); }
@media (max-width: 760px) { .brand-piece { min-height:64vh; } }


/* r21: DECK MODE. The page becomes a sequence of screens instead of a scroll: the client asked for
   entry-driven transitions so no animation can be scrolled past unwatched. Only added when deck.js runs
   (the html.deck class), so a failed script leaves the normal scrolling page intact. */
html.deck, html.deck body { height: 100vh; overflow: hidden; }
html.deck .site-header { position:relative; }
html.deck .deck-screen {
  position:fixed; inset:0; overflow:hidden; display:flex; align-items:center;
  opacity:0; transform:translate3d(0, 3.5vh, 0); pointer-events:none;
  transition:opacity .62s cubic-bezier(.22,.8,.2,1), transform .85s cubic-bezier(.22,.8,.2,1);
}
html.deck .deck-screen.is-active { opacity:1; transform:none; pointer-events:auto; }
/* the copy rises in sequence on arrival - this is the "entry" the client asked for, and it replays every
   time you come back to a screen because the class is removed and re-added */
html.deck .deck-stagger > * { opacity:0; }
html.deck .deck-screen.is-entering .deck-stagger > * { animation:deckRise .78s cubic-bezier(.22,.8,.2,1) both; }
html.deck .deck-screen.is-entering .deck-stagger > *:nth-child(1) { animation-delay:.10s; }
html.deck .deck-screen.is-entering .deck-stagger > *:nth-child(2) { animation-delay:.20s; }
html.deck .deck-screen.is-entering .deck-stagger > *:nth-child(3) { animation-delay:.30s; }
html.deck .deck-screen.is-entering .deck-stagger > *:nth-child(4) { animation-delay:.40s; }
html.deck .deck-screen.is-entering .deck-stagger > *:nth-child(5) { animation-delay:.50s; }
html.deck .deck-screen.is-entering .deck-stagger > *:nth-child(6) { animation-delay:.60s; }
html.deck .deck-stagger > * { animation-fill-mode:both; }
@keyframes deckRise { from { opacity:0; transform:translate3d(0, 26px, 0); } to { opacity:1; transform:none; } }
html.deck .frame-selector { display:none; }
html.deck .brand-piece { min-height:100vh; }
.deck-rail { position:fixed; right:clamp(16px,3vw,40px); bottom:clamp(16px,3vw,34px); z-index:6;
  display:grid; gap:10px; justify-items:end; text-align:right; color:rgba(232,238,252,.72); }
.deck-rail__label { margin:0; font-size:11px; letter-spacing:.16em; text-transform:uppercase; }
.deck-rail__dots { display:flex; gap:8px; }
.deck-dot { width:9px; height:9px; min-height:0; padding:0; border-radius:50%; cursor:pointer;
  border:1px solid rgba(201,214,247,.45); background:rgba(9,21,43,.5); transition:transform .25s ease, background .25s ease, border-color .25s ease; }
.deck-dot:hover { transform:scale(1.35); }
.deck-dot.is-active { background:#ffab5c; border-color:#ffab5c; box-shadow:0 0 14px rgba(255,171,92,.65); }
.deck-rail__track { width:150px; height:1px; background:rgba(201,214,247,.25); overflow:hidden; }
.deck-rail__track span { display:block; height:100%; width:100%; background:#ffab5c; transform-origin:left center; transform:scaleX(0.14); transition:transform .7s cubic-bezier(.22,.8,.2,1); }
.deck-hint { margin:0; font-size:10px; letter-spacing:.14em; text-transform:uppercase; opacity:.6; }
@media (max-width: 760px) { .deck-rail__track, .deck-hint { display:none; } }


/* r24: the label for the pillar the page is talking about. One at a time, attached to a lit object - the
   earlier experiment failed with seven of them floating over scenery. */
/* ============================================================================
   r30: TYPE IN THE SCENE'S SPACE
   The type system was made consistent in r29; this is the spatial layer the client asked for. Four
   rules, all transform/opacity/custom-property only, nothing that covers the picture:
     1. every hero layer carries a share of the camera pan set by its data-fx depth (js/scene-fusion.js);
     2. the layer recedes as the camera dollies forward, so the type leaves WITH the camera;
     3. .scene-anchor is a DOM point pinned to a WORLD position (the lake shore) - it follows the
        scene, not the viewport, which is what makes the page and the landscape one coordinate system;
     4. text shadows take their direction from the scene's own key light (--sun-x/--sun-y).
   prefers-reduced-motion: nothing moves, the stylesheet's own layout stands. */
/* No permanent will-change / backface-visibility here: promoting the type to its own composited
   layer at REST switches the browser to grayscale antialiasing and the headline reads measurably
   softer (a page A/B showed every text pixel changing). The layer is promoted only while it is
   actually moving - js/scene-fusion.js sets will-change on the move and clears the transform when
   the motion settles, so the type at rest is rendered by the normal text pipeline. */
html.no-fusion .hero > [data-fx] { transform: none !important; opacity: 1 !important; }

/* Shadows fall away from the sun that lights the terrain. The static soft shadow stays as the
   legibility layer; the directional one is added, never substituted. */
.hero h1 {
  text-shadow:
    calc(var(--sun-x, 0) * 14px) calc(var(--sun-y, 0) * 14px) 30px rgba(4,8,20,0.50),
    0 2px 26px rgba(4,8,20,0.45);
}
.hero-copy, .hero-detail {
  text-shadow:
    calc(var(--sun-x, 0) * 9px) calc(var(--sun-y, 0) * 9px) 16px rgba(4,8,20,0.60),
    0 1px 12px rgba(4,8,20,0.50);
}
.eyebrow { text-shadow: calc(var(--sun-x, 0) * 7px) calc(var(--sun-y, 0) * 7px) 14px rgba(3,9,20,0.75), 0 0 3px rgba(3,9,20,.88); }

/* The world-anchored marker: same language as .world-label (hairline + dot + small caps), one more
   consumer of the projection that already places the pillar labels. */
.scene-anchor {
  position: absolute; left: 0; top: 0; width: 0; height: 0;
  pointer-events: none; opacity: 0; transition: opacity 220ms linear;
  will-change: transform, opacity;
}
.scene-anchor__dot {
  position: absolute; left: -3px; top: -3px; width: 6px; height: 6px; border-radius: 50%;
  background: #9FC8FF; box-shadow: 0 0 10px rgba(159, 200, 255, 0.85);
}
.scene-anchor__line {
  position: absolute; left: 0; top: -1px; width: 1px; transform: translateX(-0.5px);
  background: linear-gradient(rgba(159, 200, 255, 0), rgba(159, 200, 255, 0.55));
}
.scene-anchor__label {
  position: absolute; left: 12px; top: -6px; white-space: nowrap;
  font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
  color: #CFE2FF; text-shadow: 0 1px 10px rgba(3, 9, 20, 0.85);
}
@media (max-width: 760px) { .scene-anchor__label { display: none; } }
.world-label--linked {
  position: fixed; left: 0; top: 0; z-index: 3; pointer-events: none;
  padding: .4em .8em .45em; border: 1px solid rgba(159, 200, 255, .5); border-radius: 3px;
  background: rgba(6, 14, 30, .62); backdrop-filter: blur(4px); color: #e8eefc;
  font-size: 12px; letter-spacing: .14em; text-transform: uppercase; white-space: nowrap;
  box-shadow: 0 0 26px rgba(110, 160, 255, .25); transition: opacity .22s linear;
}
.world-label--linked::after {
  content: ""; position: absolute; left: 50%; top: 100%; width: 1px; height: 20px;
  background: linear-gradient(rgba(159, 200, 255, .75), rgba(159, 200, 255, .05));
}
.world-label--linked .world-label__n { color: #cfe2ff; font-weight: 600; }

/* r52c · THE WAYPOINT. One pill, over the structure that is largest on screen, naming the section that
   structure belongs to. It reuses the world-label's language (hairline border, small caps, leader line)
   because it is the same kind of object: a DOM reading of a point in the world. Its job is to answer
   "which one am I looking at" - the question the two lit structures in the opening frame could not
   answer before, which is why the client read them as decoration.
   Placed by js/entity-labels.js; hidden below 761 px where there is no room for it beside the copy. */
.waypoint {
  position: absolute; left: 0; top: 0; z-index: 3; pointer-events: none;
  display: flex; align-items: baseline; gap: .5em;
  padding: .34em .72em .36em; border: 1px solid rgba(150, 190, 255, .30);
  border-radius: 2px; background: rgba(6, 12, 26, .58); backdrop-filter: blur(3px);
  color: #e8eefc; font-size: 11.5px; font-weight: 500; letter-spacing: .11em; text-transform: uppercase;
  white-space: nowrap; will-change: transform, opacity;
  transition: opacity 240ms var(--mo-ease, ease);
  box-shadow: 0 0 22px rgba(90, 140, 255, .14);
}
.waypoint__n { color: #cfe2ff; font-weight: 700; font-variant-numeric: tabular-nums; }
.waypoint__sep { color: rgba(159, 196, 255, .55); }
.waypoint__t { color: rgba(232, 238, 252, .86); }
/* THE LEADER LINE, AND WHY IT IS NOT A FIXED 18 px.
   First cut used the pillar labels' fixed 18 px hairline and the review read the result as "孤零零浮着
   ... 平贴在 UI 层" - because the pill hangs 26 px above the TOP OF THE STRUCTURE, and for a 238 px
   structure that is ~170 px of empty sky between the pill and the thing it names. The height is now fed
   in per frame (--lead) so the line spans the whole gap and ends exactly on the structure's top. */
.waypoint::after {
  content: ""; position: absolute; left: 50%; top: 100%; width: 1px;
  height: var(--lead, 18px);
  background: linear-gradient(rgba(150, 190, 255, .62), rgba(150, 190, 255, .10));
}
/* the anchor dot: the same 6 px marker the lake anchor uses, so the line has something to land on */
.waypoint__dot {
  position: absolute; left: 50%; top: calc(100% + var(--lead, 18px)); width: 6px; height: 6px;
  margin-left: -3px; margin-top: -3px; border-radius: 50%;
  background: #9FC8FF; box-shadow: 0 0 10px rgba(159, 200, 255, .85);
}
@media (max-width: 760px) { .waypoint { display: none; } }

/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   r45 · THE SPLIT FRAME. This is the client's complaint answered in one rule.

   What was measured (qa/r42-hero.cjs, 1440x900, frame 0): the type column ran from x=411 to x=1029,
   and the section-04 monolith - a 3D model the page is about - projected to x=399, i.e. 630 px
   INSIDE the type column, with the 01 and 03 structures landing on its right edge at 1039 and 1180.
   Type and models occupied the same half of the screen, which is exactly "建模也看不清，字也不好看".

   The fix is not a camera move and not a new object: the frame is SPLIT ONCE, on the page level.
   The copy lives in the left column (411 -> 811 px); everything to the right of it belongs to the
   scene. The headline is sized to that column instead of to the viewport, so it gains a deliberate
   4-line shape rather than the ragged 2-3 line block the review called "标题断行灾难".
   The scene is not covered by anything: no plate, no scrim, no panel - only the column got narrower.
   ═══════════════════════════════════════════════════════════════════════════════════════════════ */
:root {
  /* 320px, not 400: at 400 the column ran to x=811 and the 04 monolith (enlarged to 0.22R this round)
     projected its full 140 px width inside it at frames 0 and 0.11 (measured 399..539). At 320 the
     column ends at x=731 and only the monolith's left edge at 399 crosses it - the model is readable
     and the type is not sitting on it. */
  --col-type: 320px;          /* the type column; the world owns everything to its right          */
  --ink-hero: rgba(255,255,255,.96);
}
@media (min-width: 761px) {
  /* max-width is on the CHILD, not on .section-shell: the shell carries 100.8 px of padding at 1440,
     and capping the shell capped the content box with it (measured: a 400 px shell left a 198 px
     column and a 4-line headline collapsed into 9 lines). The shell keeps its full width, so the
     text's left edge stays at 411 px and only its right edge moves in to 811 px. */
  .hero > .fx, .hero h1, .hero-copy, .hero-detail, .eyebrow, .hero-actions { max-width: var(--col-type); }
  /* EXPLICIT LINE BREAKS. The headline used to wrap on its own inside the 320 px column and produced
     "Learn what / matters / in a changing / world." - four ragged lines of 210, 146, 257 and 118 px with
     "matters" alone, called out by three separate reviews. The breaks are in the markup now (one clause
     per line), so the shape is deliberate instead of a leftover of the column width. */
  .hero h1 { font-size: clamp(34px, 3.05vw, 46px); letter-spacing: -0.022em; line-height: 1.09; }
  .hero-copy { font-size: 17.5px; line-height: 1.38; }
  .hero-detail { font-size: 15px; line-height: 1.5; }
  .eyebrow { font-size: 11.5px; letter-spacing: .17em; }
  .stage-caption { max-width: calc(var(--col-type) + 40px); }
  /* FULL-WIDTH STACKED ACTIONS. At the new column width two buttons sat side by side at different
     widths with their right edges out of line ("两个按钮宽度不一、右端不齐" - the review). One column,
     one width, no ragged edge. */
  .hero-actions { flex-direction: column; align-items: stretch; gap: 10px; max-width: var(--col-type); }
  .hero-actions .button { width: 100%; justify-content: space-between; }
  .button { padding: 14px 18px; font-size: 11px; letter-spacing: .1em; }
}
/* The gradient is dropped inside the column: a horizontal white-to-blue ramp across four short lines
   reads as a colour band, and the review read the blue right-hand end as a lighting error. One ink,
   one weight, the depth carried by the scene behind it. */
@media (min-width: 761px) {
  .hero h1 {
    background: none; -webkit-background-clip: border-box; background-clip: border-box;
    color: var(--ink-hero);
    text-shadow: 0 2px 30px rgba(3,7,18,0.62), 0 1px 2px rgba(3,7,18,0.55);
  }
}



/* r55 · the design-stage selector is debug-only. It is a judging control, not navigation: its own markup
   says "Design stage selector", and its five buttons were wired to a stale keyframe table in js/ui.js
   ([0, .16, .30, .44, .56, .68, .80, 1.0] against the stage's [0, .11, .30, .44, .62, .68, .94, 1.0]).
   Visitors never see it; ?debug=1, which the QA harnesses already pass, brings it back. */
.frame-selector { display: none; }
html.has-debug .frame-selector { display: flex; }

/* r56 · THE BOOT VEIL. Covers the gap between the HTML painting and the first WebGL frame (measured
   ~3.5 s on a cold load). It is the page's own night ground, with the wordmark's warm dot breathing so it
   reads as a considered opening rather than a stall, and it fades out over 700 ms on the first-frame event. */
.boot-veil {
  position: fixed; inset: 0; z-index: 9; pointer-events: none;
  background: radial-gradient(120% 90% at 62% 38%, #0d1730 0%, #060a18 58%, #04060f 100%);
  opacity: 1; transition: opacity 700ms cubic-bezier(.22,.61,.36,1);
}
/* A bare dot read as a hang; the site's own wordmark reads as an opening. The mark is the page's mark -
   same size, weight and tracking, the dot in the brand's warm colour, breathing - over one soft pool of
   that same warm light. The curtain is not a separate loading screen, it is the site holding still. */
.boot-veil__mark {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  font-size: 26px; font-weight: 800; letter-spacing: .24em; color: rgba(244, 248, 255, .74);
}
/* the mark's warm dot, drawn as a real 8 px light rather than the 26 px font's middot, which read as dust */
.boot-veil__mark i {
  font-style: normal; display: inline-block; width: 9px; height: 9px; margin-left: .38em;
  border-radius: 50%; background: var(--warm, #FF9F0A); vertical-align: middle;
  box-shadow: 0 0 20px rgba(255, 159, 10, .85);
  animation: veil-dot 1.5s ease-in-out infinite;
}
.boot-veil::after {
  content: ""; position: absolute; left: 50%; top: 50%; width: min(64vw, 540px); height: 200px;
  border-radius: 50%; transform: translate(-50%, -50%);
  background: radial-gradient(closest-side, rgba(255, 159, 10, .11), rgba(255, 159, 10, 0) 72%);
  animation: veil-breathe 3.2s ease-in-out infinite;
}
@keyframes veil-breathe {
  0%, 100% { transform: translate(-50%, -50%) scale(.94); opacity: .5; }
  50% { transform: translate(-50%, -50%) scale(1.06); opacity: 1; }
}
@keyframes veil-dot { 0%, 100% { opacity: .5; transform: scale(1); } 50% { opacity: 1; transform: scale(1.45); } }
.boot-veil.is-gone { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .boot-veil { transition: none; }
  .boot-veil::after, .boot-veil__mark i { animation: none; opacity: .8; }
}
