/* cinzel-latin-ext-400-normal */
@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(/assets/cinzel-latin-ext-400-normal-XQK_CSAr.woff2) format('woff2'), url(/assets/cinzel-latin-ext-400-normal-DJ0Lq8y-.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* cinzel-latin-400-normal */
@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(/assets/cinzel-latin-400-normal-DnUIPmzd.woff2) format('woff2'), url(/assets/cinzel-latin-400-normal-C8jUSQqm.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}/* cinzel-latin-ext-600-normal */
@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-display: swap;
  font-weight: 600;
  src: url(/assets/cinzel-latin-ext-600-normal-BEFdM_VE.woff2) format('woff2'), url(/assets/cinzel-latin-ext-600-normal-BY9Mq9iK.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* cinzel-latin-600-normal */
@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-display: swap;
  font-weight: 600;
  src: url(/assets/cinzel-latin-600-normal-Dd5YO2UX.woff2) format('woff2'), url(/assets/cinzel-latin-600-normal-CH_LB4su.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}/* space-grotesk-vietnamese-300-normal */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-display: swap;
  font-weight: 300;
  src: url(/assets/space-grotesk-vietnamese-300-normal-B9OJnXKM.woff2) format('woff2'), url(/assets/space-grotesk-vietnamese-300-normal-F-NWLUie.woff) format('woff');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* space-grotesk-latin-ext-300-normal */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-display: swap;
  font-weight: 300;
  src: url(/assets/space-grotesk-latin-ext-300-normal-4ZO_JR7W.woff2) format('woff2'), url(/assets/space-grotesk-latin-ext-300-normal-CxWNGEn7.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* space-grotesk-latin-300-normal */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-display: swap;
  font-weight: 300;
  src: url(/assets/space-grotesk-latin-300-normal-DiSf0yqz.woff2) format('woff2'), url(/assets/space-grotesk-latin-300-normal-Byd1NLsn.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}/* space-grotesk-vietnamese-400-normal */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(/assets/space-grotesk-vietnamese-400-normal-B7xT_GF5.woff2) format('woff2'), url(/assets/space-grotesk-vietnamese-400-normal-BIWiOVfw.woff) format('woff');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* space-grotesk-latin-ext-400-normal */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(/assets/space-grotesk-latin-ext-400-normal-CfP_5XZW.woff2) format('woff2'), url(/assets/space-grotesk-latin-ext-400-normal-DRPE3kg4.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* space-grotesk-latin-400-normal */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2) format('woff2'), url(/assets/space-grotesk-latin-400-normal-BnQMeOim.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}/* space-grotesk-vietnamese-500-normal */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2) format('woff2'), url(/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff) format('woff');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* space-grotesk-latin-ext-500-normal */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2) format('woff2'), url(/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* space-grotesk-latin-500-normal */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url(/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2) format('woff2'), url(/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}/* ibm-plex-mono-cyrillic-ext-400-normal */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2) format('woff2'), url(/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff) format('woff');
  unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}

/* ibm-plex-mono-cyrillic-400-normal */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2) format('woff2'), url(/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff) format('woff');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

/* ibm-plex-mono-vietnamese-400-normal */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2) format('woff2'), url(/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff) format('woff');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* ibm-plex-mono-latin-ext-400-normal */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2) format('woff2'), url(/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff) format('woff');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* ibm-plex-mono-latin-400-normal */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url(/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2) format('woff2'), url(/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff) format('woff');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}/* Lance Integrated Systems — global styles.
   Tinctures (DIRECTION.md): Or on Sable. Gold is rationed. */

:root {
  /* brand (concept/branding concept.png): charcoal / crimson / silver */
  --obsidian: #0a0b0e;
  --graphite: #21262b;
  --steel: #c8cdd6;
  --crimson: #c8102e;
  --silver: #e6e7e9;
  --slate: #6a7280;

  /* dark ink for copy over the white frosted pane — RETIRED with the .frost
     veil (App.jsx keeps it disabled); copy is silver-on-dark-panel now.
     Kept for when the client revisits the full-bleed frost direction. */
  --ink: #191d22;
  --ink-soft: #333b44;
  --ink-mute: #566069;

  --font-display: "Cinzel", serif;
  --font-body: "Space Grotesk", system-ui, sans-serif;
  --font-mono: "IBM Plex Mono", monospace;

  /* Scroll length: one viewport per section (LNC-00..05). */
  --scroll-pages: 6;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  background: var(--obsidian);
  color: var(--steel);
  scrollbar-width: thin;
  scrollbar-color: var(--slate) transparent;
}

/* Lenis owns scrolling; keep native behavior as the fallback. */
html.lenis,
html.lenis body {
  height: auto;
}
.lenis.lenis-smooth {
  scroll-behavior: auto !important;
}

body {
  font-family: var(--font-body);
  font-weight: 300;
  line-height: 1.6;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

#root {
  position: relative;
}

/* The pinned experience canvas — everything scrolls "through" it. */
.experience {
  position: fixed;
  inset: 0;
  z-index: 0;
}

/* Full-screen FROSTED GLASS veil (client: "white frosted glass over it all").
   The whole storm sits behind a cold white pane: a real backdrop blur softens
   the canvas beneath, a cool-white tint frosts it, and the tint thickens
   toward the edges (a frosted window is clearest where you press against it).
   z-1: above the canvas (z-0), below the rain-on-glass + copy — so the copy
   stays razor-crisp ON the glass, never blurred. */
.frost {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  backdrop-filter: blur(4px) saturate(0.8) brightness(1.13);
  -webkit-backdrop-filter: blur(4px) saturate(0.8) brightness(1.13);
  /* a strong flat cool-white wash for uniform milkiness, plus a radial that
     thickens the frost toward the edges (clearest where you press on it) */
  background:
    radial-gradient(
      128% 128% at 50% 42%,
      rgba(237, 243, 250, 0.34) 0%,
      rgba(233, 240, 247, 0.46) 52%,
      rgba(221, 231, 241, 0.62) 100%
    ),
    rgba(239, 244, 250, 0.24);
}
/* fine frost grain — feTurbulence baked to a data URI, blended so it reads as
   ground-glass tooth rather than video noise */
.frost::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  opacity: 0.05;
  mix-blend-mode: overlay;
}

/* Rain on the pane — baked alpha-video loop. Sits on the frost (z-1), above
   the blur, below the copy. True-alpha, so it composites with normal blending. */
.rain-glass-video {
  position: fixed;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  /* motion-keyed clip: only occasional falling drops are opaque. Mask keeps
     drops on the LEFT and RIGHT sides and clears the CENTRE (where the copy +
     lance live). Drops still fall clear off the bottom (no vertical mask). */
  opacity: 0.7;
  /* r148: the intro-end handover used to SNAP this layer 0 → 0.7 in a single
     frame (see body.intro-active below, which has no transition) — the whole
     droplet field appeared at once, a real full-screen "static" pop. Fade it
     in instead. Safe as a CSS transition: it is a one-shot at the end of the
     load, not a looping ornament (those must run on the clamped delta clock,
     or they LEAP after a compositor stall). */
  transition: opacity 420ms ease-out;
  /* r146: clear zone widened (44-56% → 36-78%) — a drop's pale trail
     composited over the crimson banner at INSTITUTE (banner lives
     right-of-center at most waypoints; sweep caught a pink streak ON the
     cloth). Drops now live in the true margins only. */
  -webkit-mask-image: linear-gradient(
    to right,
    #000 0%,
    #000 22%,
    rgba(0, 0, 0, 0) 36%,
    rgba(0, 0, 0, 0) 78%,
    #000 90%,
    #000 100%
  );
  mask-image: linear-gradient(
    to right,
    #000 0%,
    #000 22%,
    rgba(0, 0, 0, 0) 36%,
    rgba(0, 0, 0, 0) 78%,
    #000 90%,
    #000 100%
  );
}
@media (prefers-reduced-motion: reduce) {
  .rain-glass-video {
    display: none;
  }
}

/* Invisible spacer that creates the scroll runway driving the camera.
   svh (not dvh): dvh resizes when the mobile URL bar collapses, which
   rescaled the runway mid-scroll — progress jumped and the camera lurched
   on phones. svh is frozen to the small-viewport size. */
.scroll-track {
  height: calc(var(--scroll-pages) * 100svh);
  pointer-events: none;
}

/* DOM copy overlay sits above the canvas. FIXED + transform-scrolled (see
   App.jsx overlayY): native scroll steps in uneven whole device pixels; the
   sub-pixel GPU transform is what makes the words glide like the canvas. */
/* copy container — the sections inside are their own fixed layers (see
   Sections.jsx); this only establishes stacking + pointer policy */
.overlay {
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  /* r213: the RETURN leg of the portal hide — gentle and delayed so the
     copy re-appears only as the close descent lands (the hide leg's timing
     lives on body.portal-open .overlay, which wins while the class is on) */
  transition: opacity 0.45s ease 0.3s;
  /* r150 — copy must never collide with the wordmark (video #14 sweep, HIGH:
     the BUILD title and section paragraphs scrolled THROUGH "LANCE INTEGRATED
     SYSTEMS" at full opacity, interleaving the two lines into garble). The
     overlay is a fixed full-viewport group, so a viewport-space mask fades
     every section layer out across the header band while it slides past —
     anchored to the header's own metrics. r156: titles now anchor as high as
     10vh (≈108px on 1080), so the mask is TIGHTENED to protect only the wordmark
     band (top 1.4rem ≈22px + ~28px tall) — fades out 26→60px, well above any
     resting title — while still dissolving a title as it SLIDES up through the
     header during a section transition. A gradient mask on the composited group
     is a shader op, not a re-raster (r138 layer economics untouched). */
  -webkit-mask-image: linear-gradient(
    to bottom,
    transparent calc(1.4rem + 4px),
    #000 calc(1.4rem + 38px)
  );
  mask-image: linear-gradient(
    to bottom,
    transparent calc(1.4rem + 4px),
    #000 calc(1.4rem + 38px)
  );
}
.overlay a,
.overlay button {
  pointer-events: auto;
}

/* Spec-sheet voice: mono small-caps section indices. Muted silver — reads
   bare on the raw scene, not the white frost pane the old --ink-mute was
   mixed for. */
.spec-label {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.35em;
  text-transform: uppercase;
  /* r147: 0.6 → 0.85 + dark grounding (the DARK-shadow pattern from the
     wordmark fix — never a white halo). The hero eyebrow washed out over
     bright cloud banks; the client's cursor went straight to it. */
  color: rgba(230, 231, 233, 0.85);
  text-shadow: 0 1px 2px rgba(10, 11, 14, 0.8), 0 1px 6px rgba(10, 11, 14, 0.5);
}

h1,
h2,
h3 {
  font-family: var(--font-display);
  font-weight: 400;
  color: var(--silver);
  letter-spacing: 0.08em;
  /* NO white halo (the r144 doubled-ghost). See .section-title for the r156
     heraldic outline the client asked for. */
}

/* Title treatment — "ENGRAVED ON THE SKY" (client video #23: the subtle dark
   edge was "heading in the right direction… make it look engraved into the
   screen, not blending into the background; play around with that").
   Chisel logic for light glyphs on a dark ground: a dark INSET edge along the
   upper contour (the cut), a faint light lip along the lower contour (the
   bevel catching the sky light), and a soft wide seat that settles the plate
   into the clouds. Neutral tones only — the crimson outline was rejected. */
.section-title {
  /* r164 (client video #28): "generic shadowing" rejected — "think about what
     the BACKGROUND looks like and create a CUSTOM solution." The background is
     a dark storm sky, so a raised-plate shadow stack can never read carved.
     CUSTOM: the recession happens INSIDE each glyph — a vertical gradient
     clipped to the text tilts the glyph face away from the sky-light (storm-
     toned darker at the top of each letter, bright toward the lower face,
     exactly how a groove cut into a lit surface shades), with one crisp cut
     line above and one lit lip below. Bright white lives in the LOWER half of
     every glyph, so legibility holds while the face reads sunken. */
  /* r165 (video #29): the r164 gradient read WORSE — reverted to the clean
     bright cut he preferred, and the "subject" job moved to the SCENE (the 3D
     dims slightly at text stops — CameraRig dwell-dim), so the type no longer
     needs heavy effects to win the frame. */
  color: #eef0f4;
  -webkit-text-stroke: 0.7px rgba(5, 7, 11, 0.72);
  paint-order: stroke fill;
  text-shadow:
    0 -1.5px 1.5px rgba(3, 5, 9, 0.85),
    0 1.5px 1px rgba(250, 252, 255, 0.36);
}

/* r162: the subtext treatment — HIS THIRD ASK; the r160/r161 versions
   technically existed but were a 1px whisper, invisible at body size over the
   busy scene ("the text still looks basic"). Now unmistakable: same incised
   logic scaled for body text — brighter fill, real stroke, visible cut. The
   perceptibility is VERIFIED at 1:1 crop this round, not assumed. */
.overlay p {
  /* r165: clean + bright ("the shadow around it looks really bad, difficult
     to read"). The dwell-dim carries the subject/background separation; the copy is
     simply crisp: bright fill, hairline stroke, one tight grounding shadow. */
  color: #dde1e8;
  -webkit-text-stroke: 0.4px rgba(5, 7, 11, 0.55);
  paint-order: stroke fill;
  text-shadow: 0 1px 2px rgba(4, 6, 10, 0.7);
}

.overlay p {
  color: var(--steel);
  /* no halo — same doubled-ghost mechanism as h1-h3 above; var(--steel)
     reads fine bare on the raw scene */
}

/* .copy-block panel REMOVED — client rejected the frosted glass over the lance (r145); do not reintroduce. */

::selection {
  background: var(--crimson);
  color: var(--silver);
}

.brand {
  position: fixed;
  top: 1.4rem;
  left: 1.6rem;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 0.7rem;
  pointer-events: none;
}
.brand span {
  font-family: var(--font-display);
  font-size: 0.78rem;
  letter-spacing: 0.28em;
  /* stable 0.85 silver on a dark grounding shadow. This was var(--ink) under
     a wide white halo — dark ink mixed for the white frost pane. With the
     .frost veil disabled (App.jsx) the near-black glyphs sat on near-black
     storm and only stray halo fragments showed: the wordmark hovered at
     ghost strength, "flickered" as clouds/particles drifted behind it, and
     read garbled where only some letters caught a bright patch. No animation
     was ever involved — pure contrast collapse. The two-layer dark shadow
     keeps it legible when the bright marble crosses the top-left. */
  color: rgba(230, 231, 233, 0.85);
  text-shadow: 0 1px 2px rgba(10, 11, 14, 0.55), 0 1px 8px rgba(10, 11, 14, 0.45);
}

/* ---- scroll-points rail (igloo.inc pattern) ------------------------------
   Six fixed markers down the right edge, one per LNC section. The mark is
   the heraldic lozenge (rotated square) from the banner; active = filled
   crimson with a soft glow. Labels slide in from the right on hover/active. */
.points-rail {
  position: fixed;
  right: 1.9rem;
  top: 50%;
  transform: translateY(-50%);
  z-index: 4;
  display: grid;
  gap: 1.35rem;
  justify-items: end;
}
.point {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  background: none;
  border: 0;
  cursor: pointer;
  padding: 0.15rem 0;
}
.point-mark {
  position: relative;
  width: 9px;
  height: 9px;
  transform: rotate(45deg);
  border: 1px solid rgba(230, 231, 233, 0.55);
  background: rgba(230, 231, 233, 0.08);
  /* r146: dark grounding halo — silver-outline diamonds went white-on-white
     wherever the bright marble shaft crossed the rail (video #11 sweep) */
  filter: drop-shadow(0 0 2px rgba(10, 12, 15, 0.85));
  /* background/border transitions remain for HOVER only (silver → brighter
     silver, same hue — safe). The active crimson never rides these. */
  transition:
    background 0.35s ease,
    border-color 0.35s ease,
    box-shadow 0.35s ease,
    scale 0.35s ease;
}
/* the crimson fill lives on its OWN layer and fades via OPACITY. It used to
   ride the background transition — silver→crimson interpolation passed
   through a half-alpha crimson that composited PINK over the white marble /
   banner border mid-crossfade (pink is banned; DIRECTION.md tinctures).
   Full crimson at varying opacity only ever deepens toward the backdrop. */
.point-mark::after {
  content: "";
  position: absolute;
  inset: -1px; /* covers the silver border too — no light rim mid-fade */
  background: var(--crimson);
  opacity: 0;
  transition: opacity 0.35s ease;
}
.point:hover .point-mark {
  border-color: var(--silver);
  background: rgba(230, 231, 233, 0.4);
}
.point.is-active .point-mark {
  box-shadow: 0 0 12px rgba(200, 16, 46, 0.6);
  scale: 1.25;
}
.point.is-active .point-mark::after {
  opacity: 1;
}
.point-label {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.24em;
  color: var(--silver);
  /* dark frosted CHIP (blur joins in the @supports block below):
     the bare silver mono went white-on-white wherever the marble shaft
     crossed the rail. The chip is the background of THIS span — text plus
     padding — so no glyph can ever land outside it (the '04' spill in the
     client video). padding-right runs slightly short because 0.24em
     letter-spacing trails the last glyph with phantom advance width. */
  padding: 0.35rem 0.4rem 0.35rem 0.6rem;
  /* r146: 0.6 → 0.8 — over the white marble the 0.6 wash sampled as a
     light-grey pill (~2:1 text contrast, self-review sweep) */
  background: rgba(14, 17, 22, 0.8);
  border: 1px solid rgba(230, 231, 233, 0.08);
  border-radius: 4px;
  text-shadow: 0 1px 6px rgba(10, 11, 14, 0.8);
  opacity: 0;
  transform: translateX(6px);
  transition:
    opacity 0.3s ease,
    transform 0.3s ease;
  pointer-events: none;
  white-space: nowrap;
}
.point:hover .point-label,
.point.is-active .point-label {
  opacity: 0.9;
  transform: translateX(0);
}
/* the chip's frosted variant — MUST sit after the .point-label base rule
   (same specificity; source order decides, @supports does not raise it).
   Six tiny regions: negligible raster. */
@supports (backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px)) {
  .point-label {
    background: rgba(14, 17, 22, 0.62);
    backdrop-filter: blur(10px) saturate(1.1);
    -webkit-backdrop-filter: blur(10px) saturate(1.1);
  }
}
@media (max-width: 720px) {
  .points-rail {
    right: 0.9rem;
    gap: 1.1rem;
  }
  .point-label {
    display: none;
  }
}

/* ---- intro ceremony (loader + staged UI entrance) ------------------------ */
.loader {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: var(--obsidian);
  display: grid;
  place-items: center;
  transition: opacity 1.15s ease 0.1s;
}
.loader[data-phase="reveal"] {
  opacity: 0;
  pointer-events: none;
}
.loader-inner {
  display: grid;
  justify-items: center;
  gap: 1.7rem;
}
/* the mark draws itself: stroke outlines sweep in (pathLength=1 makes the
   dash math unit), then the fills flood */
.loader-path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  stroke-width: 2.5;
  fill-opacity: 0;
  animation:
    markdraw 1.05s cubic-bezier(0.4, 0, 0.2, 1) forwards,
    markfill 0.7s ease forwards;
  animation-delay: calc(var(--i) * 0.11s), calc(0.85s + var(--i) * 0.07s);
}
/* the mark's breathe + all astrolabe loops are driven by the Loader's
   delta-clock rAF (wall-clock CSS loops LEAP after compositor stalls) */
.loader-inner svg {
  will-change: opacity;
}
.loader-crimson {
  stroke: var(--crimson);
  fill: var(--crimson);
}
.loader-silver {
  stroke: var(--silver);
  fill: var(--silver);
}
@keyframes markdraw {
  to {
    stroke-dashoffset: 0;
  }
}
@keyframes markfill {
  to {
    fill-opacity: 1;
  }
}
/* the turning seal: six heraldic lozenges (one per LNC section — the scroll
   rail's diamonds) orbit the emblem on a slow ring while a crimson pulse
   chases around them. ONE rotating transform + six opacity loops — all
   compositor properties, so the seal keeps turning through main-thread
   stalls (and the staged scene assembly keeps the GPU bites small enough
   that the compositor itself stays fed). */
.loader-emblem {
  position: relative;
  width: 200px;
  height: 200px;
  display: grid;
  place-items: center;
}
/* the tick bezel — two dashed circles (fine minute-ticks + 6 majors, one
   per section) painted once as SVG, then rotated as a single compositor
   layer. Clockwise, slow: the instrument's outer wheel. */
.loader-bezel {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  will-change: transform;
}
.bezel-fine {
  fill: none;
  stroke: rgba(230, 231, 233, 0.22);
  stroke-width: 3.5;
  stroke-dasharray: 0.45 4.55; /* 72 fine ticks (pathLength 360) */
}
.bezel-major {
  fill: none;
  stroke: rgba(200, 16, 46, 0.5);
  stroke-width: 7;
  stroke-dasharray: 1.1 58.9; /* 6 major ticks — the sections */
}
.loader-orbit {
  position: absolute;
  inset: 0;
  will-change: transform;
}
/* the seeker — one bright gem sweeping the outer ring like a second hand */
.loader-seeker {
  position: absolute;
  inset: 0;
  will-change: transform;
}
.loader-seeker i {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 6px;
  height: 6px;
  margin: -3px;
  /* r146: crimson, not silver — the lone white square sweeping the
     all-crimson ring read as a stray/uncolored particle (sweep) */
  background: var(--crimson);
  box-shadow: 0 0 9px rgba(200, 16, 46, 0.85);
  transform: translateY(-96px) rotate(45deg);
}
.loader-orbit i {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 8px;
  height: 8px;
  margin: -4px;
  background: var(--crimson);
  transform: rotate(calc(var(--i) * 60deg)) translateY(-86px) rotate(45deg);
  opacity: 0.16;
  will-change: opacity;
}
.loader-status {
  font-family: var(--font-mono);
  font-size: 0.55rem;
  letter-spacing: 0.3em;
  /* crimson @ .75 on obsidian was ~2:1 contrast — unreadable in the client's
     video. The status is the honest-work line; it must READ. Crimson stays
     in the lozenge chase above it. */
  color: rgba(230, 231, 233, 0.68);
  margin-top: 0.4rem;
  text-align: center;
  min-height: 1em;
}
.loader-meta {
  width: 100%;
  display: flex;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.26em;
  color: var(--slate);
  white-space: nowrap;
}

/* chrome holds back until the camera arrives, then eases in */
.brand,
.points-rail {
  transition: opacity 0.9s ease;
}
body.intro-active .brand,
body.intro-active .points-rail {
  opacity: 0;
  pointer-events: none;
}
body.intro-active .rain-glass-video {
  opacity: 0;
}

/* ── r198 — CONTACT ctas (client: billing was "transparent, difficult to
   see" → both crimson; GET IN TOUCH centred, BILLING below-left; "cool
   hover" = molten crimson fill sweeping in from the left + a soft bloom.
   The sweep is a layered background-size animation — no pseudo-elements,
   so it composes with the buttons' translucent resting fill. ─────────── */
.contact-cta {
  display: inline-block;
  padding: 0.85rem 2.2rem;
  border: 1px solid var(--crimson);
  color: var(--crimson);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.3em;
  text-decoration: none;
  background:
    linear-gradient(var(--crimson), var(--crimson)) left center / 0% 100% no-repeat,
    rgba(200, 16, 46, 0.12);
  transition:
    background-size 0.32s cubic-bezier(0.4, 0, 0.2, 1),
    color 0.26s ease,
    box-shadow 0.32s ease,
    transform 0.26s ease;
}
.contact-cta:hover,
.contact-cta:focus-visible {
  background-size: 100% 100%, 100% 100%;
  color: #f5f0ea;
  box-shadow: 0 0 26px rgba(200, 16, 46, 0.38), 0 0 4px rgba(200, 16, 46, 0.5);
  transform: translateY(-1px);
}
.contact-cta:active {
  transform: translateY(0);
  box-shadow: 0 0 14px rgba(200, 16, 46, 0.3);
}

/* r206 — small screens: keep the ctas on one line (video #48 "buttons look
   a little awkward"): tighter tracking + padding under 640px. */
@media (max-width: 640px) {
  .contact-cta {
    padding: 0.85rem 1.5rem;
    letter-spacing: 0.22em;
    white-space: nowrap;
  }
}

/* ══ r211→r213 — THE PORTAL (client videos #51-#53, igloo.inc reference) ═════
   Detail pages that open IN the journey: CameraRig lifts the rig into the
   storm (portalK), the live weather stays sharp behind a frosted-glass pane,
   and the DOM adds only vignette, mist, dust and the pointer light. z-50:
   above all site chrome, below the loader ceremony (z-60). */

.portal {
  position: fixed;
  inset: 0;
  z-index: 50;
  animation: portal-in 0.3s ease both;
  /* touch scrolling in the portal is DRIVEN (Portal.jsx), not native:
     Chromium's compositor kept latching portal drags to the ROOT scroller
     (the frozen journey) at touchstart — before JS could preventDefault
     (non-cancelable moves) — in headless/WSLg even with an idle main thread.
     touch-action: none is evaluated by the compositor BEFORE latching, so
     nothing native ever scrolls; the drive owns the column, exactly as
     Lenis's syncTouch owns the journey. Wheel stays native (verified). */
  touch-action: none;
}
.portal[data-closing] {
  pointer-events: none;
}
@keyframes portal-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* r213 — the backdrop is the LIVE SCENE (video #53: igloo's page "looks like
   it's part of the scene… ours looks like it's sitting over the scene").
   There is NO plate element: CameraRig has lifted the rig into the storm and
   the real weather breathes behind the DOM. The veil is only an edge
   vignette + a top scrim for the chrome — the middle stays CLEAR so the
   storm reads sharp (his "beautiful HD images… I don't know why there's a
   blur effect" — the frost lives on the glass pane, nowhere else). Grain
   rides ::after (ground-glass tooth, not video noise). */
.portal-veil {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(
      120% 120% at 50% 44%,
      rgba(5, 6, 9, 0) 0%,
      rgba(5, 6, 9, 0.1) 58%,
      rgba(4, 5, 8, 0.52) 100%
    ),
    linear-gradient(to bottom, rgba(5, 6, 9, 0.44) 0%, rgba(5, 6, 9, 0) 22%, rgba(5, 6, 9, 0) 82%, rgba(4, 5, 8, 0.3) 100%);
  transition: opacity 0.5s ease;
}
.portal[data-closing] .portal-veil,
.portal[data-closing] .portal-mist,
.portal[data-closing] .portal-dust,
.portal[data-closing] .portal-smart,
.portal[data-closing] .portal-chrome {
  opacity: 0;
  transition: opacity 0.45s ease;
}
.portal[data-closing] .portal-scroll {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.4s ease, transform 0.55s cubic-bezier(0.5, 0, 0.75, 0.6);
}
.portal-veil::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  opacity: 0.04;
  mix-blend-mode: overlay;
}

/* smoke drifting through the weather, full-bleed BEHIND the glass — the
   glass is what keeps the copy clear now, so the mist needs no corridor.
   screen-blend over the dark plate = light fog, never a grey wash. */
.portal-mist {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}
.portal-mist img {
  position: absolute;
  width: 130vmax;
  opacity: 0.07; /* r213: the live scene carries its own smoke now */
  filter: blur(16px) saturate(0.75);
  mix-blend-mode: screen;
  will-change: transform;
}
.portal-mist img:first-child {
  left: -46vmax;
  bottom: -42vmax;
  animation: portal-drift-a 87s ease-in-out infinite alternate;
}
.portal-mist img:last-child {
  right: -50vmax;
  top: -46vmax;
  animation: portal-drift-b 113s ease-in-out infinite alternate;
}
@keyframes portal-drift-a {
  from {
    transform: translate3d(0, 0, 0) rotate(0deg) scale(1);
  }
  to {
    transform: translate3d(7vmax, -5vmax, 0) rotate(9deg) scale(1.14);
  }
}
@keyframes portal-drift-b {
  from {
    transform: translate3d(0, 0, 0) rotate(0deg) scale(1.1);
  }
  to {
    transform: translate3d(-8vmax, 6vmax, 0) rotate(-7deg) scale(1);
  }
}

/* r213 — the DUST FIELD (portalDust.js): canvas-drawn silt + near-field
   bokeh on the scene's own wind. It sits UNDER the glass pane, so grains
   passing behind the copy are frosted by the pane's backdrop-filter — real
   depth of field from real compositing. (The r211 CSS motes died here:
   video #53 "some kind of particle system that he made himself which is
   disgusting… deliberately boring and lazy looking".) */
.portal-dust {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* r214→r217 — the SMART FILTER is now BLACK GLASS OVER THE ENTIRE DISPLAY
   (video #57, his own words: "maybe try to apply a black glass filter over
   this entire display and we'll see if that works"): uniform, unmasked,
   dark and desaturating — kills the green cast at the compositor while the
   plate grade kills it at the shader. The storm still breathes through. */
.portal-smart {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* r220: darker still (his word, twice) — a properly tinted window */
  backdrop-filter: blur(7px) brightness(0.33) saturate(0.5);
  -webkit-backdrop-filter: blur(7px) brightness(0.33) saturate(0.5);
  background: rgba(3, 4, 6, 0.34);
}

/* chrome — brand (redraws itself each open) left, bracket CLOSE right, on a
   top scrim so both stay legible over mist and scrolled copy */
.portal-chrome {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 4;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.05rem 1.6rem;
  background: linear-gradient(to bottom, rgba(6, 7, 10, 0.78), rgba(6, 7, 10, 0));
  pointer-events: none;
}
.portal-chrome > * {
  pointer-events: auto;
}
.portal-brand {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  background: none;
  border: 0;
  cursor: pointer;
  padding: 0.2rem 0.3rem;
}
.portal-brand svg {
  transition: filter 0.32s ease, transform 0.32s ease;
}
.portal-brand span {
  font-family: var(--font-display);
  font-size: 0.72rem;
  letter-spacing: 0.28em;
  color: rgba(230, 231, 233, 0.85);
  text-shadow: 0 1px 2px rgba(10, 11, 14, 0.55), 0 1px 8px rgba(10, 11, 14, 0.45);
  transition: color 0.3s ease;
}
/* r216: the MARK holds still on hover (video #56 killed both the redraw
   and the glow); the WORDMARK carries the moment — churnOnce type-static
   (Portal.jsx) + this cold light sweeping through the letters. */
.portal-brand:hover .portal-brand-word,
.portal-brand:focus-visible .portal-brand-word {
  background-image: linear-gradient(
    100deg,
    rgba(230, 231, 233, 0.85) 0%,
    #ffffff 18%,
    rgba(230, 231, 233, 0.85) 36%
  );
  background-size: 260% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: brand-sweep 0.9s ease-out;
}
@keyframes brand-sweep {
  from {
    background-position: 120% 0;
  }
  to {
    background-position: -60% 0;
  }
}
.portal-brand:hover span,
.portal-brand:focus-visible span {
  color: var(--silver);
}

/* CLOSE — bracket corners (the igloo close language) around a mono label
   that boils on hover (churnOnce). Corners breathe outward on approach. */
.portal-close {
  position: relative;
  background: rgba(14, 17, 22, 0.55);
  border: 0;
  cursor: pointer;
  padding: 0.78rem 1.45rem;
}
.portal-close-label {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.32em;
  color: rgba(230, 231, 233, 0.85);
  transition: color 0.28s ease;
}
.portal-close i,
.pt-corner {
  position: absolute;
  width: 9px;
  height: 9px;
  border: 0 solid rgba(230, 231, 233, 0.55);
  transition: transform 0.28s ease, border-color 0.28s ease;
  pointer-events: none;
}
.portal-close i:nth-of-type(1) {
  top: 0;
  left: 0;
  border-top-width: 1px;
  border-left-width: 1px;
}
.portal-close i:nth-of-type(2) {
  top: 0;
  right: 0;
  border-top-width: 1px;
  border-right-width: 1px;
}
.portal-close i:nth-of-type(3) {
  bottom: 0;
  left: 0;
  border-bottom-width: 1px;
  border-left-width: 1px;
}
.portal-close i:nth-of-type(4) {
  bottom: 0;
  right: 0;
  border-bottom-width: 1px;
  border-right-width: 1px;
}
.portal-close:hover .portal-close-label,
.portal-close:focus-visible .portal-close-label {
  color: #fff;
}
.portal-close:hover i,
.portal-close:focus-visible i {
  border-color: var(--crimson);
}
.portal-close:hover i:nth-of-type(1),
.portal-close:focus-visible i:nth-of-type(1) {
  transform: translate(-3px, -3px);
}
.portal-close:hover i:nth-of-type(2),
.portal-close:focus-visible i:nth-of-type(2) {
  transform: translate(3px, -3px);
}
.portal-close:hover i:nth-of-type(3),
.portal-close:focus-visible i:nth-of-type(3) {
  transform: translate(-3px, 3px);
}
.portal-close:hover i:nth-of-type(4),
.portal-close:focus-visible i:nth-of-type(4) {
  transform: translate(3px, 3px);
}
.portal :focus-visible {
  outline: 1px solid var(--crimson);
  outline-offset: 3px;
}

/* the page column — native scroll (data-lenis-prevent on the root).
   will-change promotes it to a COMPOSITED scroller: without its own
   compositor scroll node, a touch drag over it needs a main-thread hit-test
   confirmation, and on a saturated main thread (the WebGPU storm keeps
   rendering under the veil) Chrome binds the gesture to the always-composited
   ROOT scroller instead — the drag scrolled the frozen journey, not the
   portal (r211 compact forensics: non-cancelable touchmoves, window scroll
   events, portal at 0). Compositor-owned, the drag latches correctly however
   busy the main thread is — the client's S22 matters as much as headless. */
.portal-scroll {
  position: absolute;
  inset: 0;
  z-index: 3;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  /* touch-action computes target → NEAREST SCROLLABLE ANCESTOR, so the
     .portal root's `none` never reaches touches over this column — it must
     be declared ON the scroller for the compositor to keep its hands off
     (r211: without it, drags moved BOTH the driven column AND the journey) */
  touch-action: none;
  will-change: scroll-position, transform;
  /* r214: NO visible scrollbar — the thin track over the dark storm was the
     "static on the right" he asked to debug (video #54, t≈100). Desktop
     fits without scrolling; compact still scrolls by touch. */
  scrollbar-width: none;
  /* one screen, centred: grid + margin:auto on the stage (NOT place-items:
     center, which clips the top beyond scroll reach on overflow) */
  display: grid;
  padding: 5.2rem 1.6rem 2rem;
  /* the r150 header-band lesson, portal edition: scrolled copy dissolves
     before it reaches the chrome (the wordmark/BACK band) instead of
     interleaving with it. Shader op on the composited scroller. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 3.4rem, #000 6.4rem);
  mask-image: linear-gradient(to bottom, transparent 3.4rem, #000 6.4rem);
}
.portal-scroll::-webkit-scrollbar {
  display: none;
}

/* r214 — the STAGE: a bare centred column, no pane, no borders (video #54
   killed the box; the smart layer beneath carries legibility). The rise is
   delayed a beat so the camera's climb into the storm leads and the copy
   arrives INTO the new sky. */
.portal-stage {
  position: relative;
  margin: auto;
  /* r215→r216 (video #56, again): the measure narrows to ~56ch — judged at
     HIS 125% zoom against the inspiration column. */
  width: min(92vw, 33rem);
  animation: portal-stage-in 0.62s cubic-bezier(0.16, 0.7, 0.24, 1) 0.28s both;
}
@keyframes portal-stage-in {
  from {
    opacity: 0;
    transform: translateY(26px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.portal-page {
  max-width: none;
}
.pp-eyebrow {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.35em;
  text-transform: uppercase;
  color: rgba(230, 231, 233, 0.8);
}
.pp-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.45rem, 2.6vw, 2.05rem);
  letter-spacing: 0.08em;
  margin-top: 0.7rem;
  /* the engraved cut (r165 title law) — reads on the flat veil too */
  color: #eef0f4;
  -webkit-text-stroke: 0.7px rgba(5, 7, 11, 0.72);
  paint-order: stroke fill;
  text-shadow:
    0 -1.5px 1.5px rgba(3, 5, 9, 0.85),
    0 1.5px 1px rgba(250, 252, 255, 0.36);
}
/* r213 — the page speaks the SITE's body voice now (video #53: "I don't
   like this font that we're using… the title and logo font looks good" —
   the wall-to-wall terminal mono is gone; mono stays on the spec layer:
   eyebrow, section marks, reference chips, BACK). */
.pp-standfirst {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 1.02rem;
  line-height: 1.66;
  letter-spacing: 0.012em;
  color: #e3e7ee;
  margin-top: 1.05rem;
}
/* r214 — the section marks are GONE (video #54: "the programme, the proof,
   standard, whatever — don't use any of that"); the read is clean
   paragraphs with their own rhythm. */
.pp-body {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 0.92rem;
  line-height: 1.72;
  letter-spacing: 0.012em;
  color: #ccd2db;
  margin-top: 1.15rem;
}
/* r213 — the word spans mounted after the decode settles: they carry the
   light. --lit (0..1, quantized) is written by portalReveal's tick for
   whatever sits inside the pool around the pointer's resting point —
   brightness and a cold halo that FADE WITH DISTANCE (the igloo behavior
   he pointed at, twice). Everything degrades to plain type at --lit: 0. */
.portal-page .pw {
  display: inline-block; /* transforms + the width-locked scramble box */
  /* r217: a hyphenated word ("all-staff") mid-boil kept its hyphen as an
     INTERNAL wrap point and broke to two lines inside the locked box —
     words never wrap internally, whatever glyphs they carry */
  white-space: nowrap;
}
.portal-page .pw,
.portal-page .pc,
.portal-page .pp-fedlink {
  color: color-mix(
    in oklab,
    currentColor calc(100% - var(--lit, 0) * 30%),
    #fbfdff calc(var(--lit, 0) * 30%)
  );
  text-shadow: 0 0 calc(var(--lit, 0) * 13px) rgba(202, 220, 248, calc(var(--lit, 0) * 0.45));
  /* the crest travels — short transitions; filter carries the glassy
     shimmer (pixels refract, layout never moves) */
  transition: color 0.12s ease-out, text-shadow 0.12s ease-out, filter 0.14s ease-out;
}
/* r215 (video #55: "that boring gray line… come on man") — the underline
   is a live element now: a hairline that CARRIES A CRIMSON CURRENT from the
   left on approach (the house engraved-light language, one line high), and
   every link wears a quiet spec ↗. Still calm at rest — silver text, thin
   line — but it answers the pointer. */
.pp-fedlink {
  color: #e2e6ee;
  font-weight: 400;
  text-decoration: none;
  padding-bottom: 1px;
  background-image: linear-gradient(to right, var(--crimson), #ff5a70), linear-gradient(rgba(230, 231, 233, 0.3), rgba(230, 231, 233, 0.3));
  background-repeat: no-repeat;
  background-position: left bottom, left bottom;
  background-size: 0% 1px, 100% 1px;
  transition: background-size 0.34s cubic-bezier(0.3, 0.6, 0.2, 1), color 0.24s ease, text-shadow 0.24s ease;
}
/* a link and its sentence period never separate across lines */
.pp-hold {
  white-space: nowrap;
}
.pp-fedlink::after {
  content: "↗";
  font-family: var(--font-mono);
  font-size: 0.62em;
  vertical-align: super;
  margin-left: 0.18em;
  color: rgba(230, 231, 233, 0.55);
  transition: color 0.24s ease;
}
.pp-fedlink:hover,
.pp-fedlink:focus-visible {
  color: #fff;
  background-size: 100% 1px, 100% 1px;
  text-shadow: 0 0 12px rgba(200, 16, 46, 0.3);
}
.pp-fedlink:hover::after,
.pp-fedlink:focus-visible::after {
  color: var(--crimson);
}
/* r215 (video #55: "this button is so fucking boring… what kind of
   engagement is this") — the CTA speaks the approved tile language: bracket
   corners that breathe outward, the molten crimson sweep rising through it
   on approach, the label brightening to white over the fill. */
.portal-page .contact-cta {
  position: relative;
  background:
    linear-gradient(var(--crimson), var(--crimson)) left center / 0% 100% no-repeat,
    rgba(200, 16, 46, 0.08);
  border: 1px solid rgba(200, 16, 46, 0.55);
  transition: background-size 0.5s cubic-bezier(0.25, 0.6, 0.2, 1), color 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
}
.portal-page .contact-cta::before,
.portal-page .contact-cta::after {
  content: "";
  position: absolute;
  width: 9px;
  height: 9px;
  border: 0 solid var(--silver);
  opacity: 0.75;
  transition: transform 0.28s ease, border-color 0.28s ease;
  pointer-events: none;
}
.portal-page .contact-cta::before {
  top: -4px;
  left: -4px;
  border-top-width: 1px;
  border-left-width: 1px;
}
.portal-page .contact-cta::after {
  bottom: -4px;
  right: -4px;
  border-bottom-width: 1px;
  border-right-width: 1px;
}
.portal-page .contact-cta:hover,
.portal-page .contact-cta:focus-visible {
  background-size: 100% 100%, 100% 100%;
  color: #fff;
  border-color: var(--crimson);
  box-shadow: 0 0 26px rgba(200, 16, 46, 0.35);
}
.portal-page .contact-cta:hover::before,
.portal-page .contact-cta:focus-visible::before {
  transform: translate(-3px, -3px);
  border-color: var(--crimson);
}
.portal-page .contact-cta:hover::after,
.portal-page .contact-cta:focus-visible::after {
  transform: translate(3px, 3px);
  border-color: var(--crimson);
}

/* the foot: the CTA leads, references trail as quiet spec chips */
.pp-foot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 1.1rem 2.2rem;
  margin-top: 1.9rem;
}
.pp-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
}
.pp-links a {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.16em;
  color: rgba(230, 231, 233, 0.66);
  text-decoration: none;
  border-bottom: 1px solid rgba(230, 231, 233, 0.22);
  padding-bottom: 2px;
  transition: color 0.24s ease, border-color 0.24s ease;
}
.pp-links a span {
  display: inline-block;
  transition: transform 0.24s ease;
}
.pp-links a:hover,
.pp-links a:focus-visible {
  color: var(--silver);
  border-color: var(--crimson);
}
.pp-links a:hover span,
.pp-links a:focus-visible span {
  transform: translate(2px, -2px);
}

/* r215 — the ROOT (journey) scrollbar hides while a portal is open: the
   6-page runway kept the browser's scrollbar on screen through the portal
   (video #55's "screw bar on the side"). Scroll state is untouched — the
   bar is cosmetic; Lenis holds the position. */
html.portal-open {
  scrollbar-width: none;
}
/* r217 — THE STATIC, root-caused by the client himself (video #57: "I think
   it's related to the video playing, the water drop"): the rain-glass alpha
   video keeps drops on the right margin (z-1) and they read as white static
   through the portal's clear veil over the dark storm. Hidden while any
   portal is open. */
body.portal-open .rain-glass-video {
  opacity: 0;
}
html.portal-open::-webkit-scrollbar {
  display: none;
}

/* site chrome yields while a portal is open (the veil would half-ghost it).
   r213: the JOURNEY COPY too — the veil is a clear edge-vignette now (the
   live storm must read), so without this the section's own title/body/tile
   ghost straight through the frosted glass. */
body.portal-open .brand,
body.portal-open .points-rail,
body.portal-open .overlay {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}

/* ── the INSTITUTE portal tile (video #51: "this entire thing is a button…
   very cool hover animation") — spec plate + bracket corners + the approved
   r198 molten sweep. Lives inside the section's body block, on its grid. */
.portal-tile {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "eyebrow arrow"
    "label   arrow"
    "sub     arrow";
  column-gap: 1.1rem;
  row-gap: 0.3rem;
  align-items: center;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  /* the body Block's flex gap (1rem) already spaces it from the paragraph */
  margin-top: 0.15rem;
  padding: 0.95rem 1.2rem;
  background:
    linear-gradient(var(--crimson), var(--crimson)) left center / 0% 100% no-repeat,
    rgba(14, 17, 22, 0.58);
  border: 1px solid rgba(230, 231, 233, 0.22);
  transition:
    background-size 0.38s cubic-bezier(0.4, 0, 0.2, 1),
    border-color 0.3s ease,
    box-shadow 0.38s ease,
    transform 0.26s ease;
}
@supports (backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px)) {
  .portal-tile {
    background:
      linear-gradient(var(--crimson), var(--crimson)) left center / 0% 100% no-repeat,
      rgba(14, 17, 22, 0.45);
    backdrop-filter: blur(10px) saturate(1.1);
    -webkit-backdrop-filter: blur(10px) saturate(1.1);
  }
}
.pt-tl {
  top: -1px;
  left: -1px;
  border-top-width: 1px;
  border-left-width: 1px;
}
.pt-tr {
  top: -1px;
  right: -1px;
  border-top-width: 1px;
  border-right-width: 1px;
}
.pt-bl {
  bottom: -1px;
  left: -1px;
  border-bottom-width: 1px;
  border-left-width: 1px;
}
.pt-br {
  bottom: -1px;
  right: -1px;
  border-bottom-width: 1px;
  border-right-width: 1px;
}
.pt-eyebrow {
  grid-area: eyebrow;
  font-family: var(--font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.3em;
  color: rgba(230, 231, 233, 0.75);
  transition: color 0.3s ease;
}
.pt-label {
  grid-area: label;
  font-family: var(--font-mono);
  font-size: 0.84rem;
  letter-spacing: 0.2em;
  color: var(--silver);
  transition: color 0.3s ease;
  white-space: nowrap;
}
.pt-sub {
  grid-area: sub;
  font-family: var(--font-mono);
  font-size: 0.56rem;
  letter-spacing: 0.2em;
  color: rgba(230, 231, 233, 0.5);
  transition: color 0.3s ease;
}
.pt-arrow {
  grid-area: arrow;
  font-family: var(--font-mono);
  font-size: 1.05rem;
  color: var(--crimson);
  transition: transform 0.3s ease, color 0.3s ease;
}
.portal-tile:hover,
.portal-tile:focus-visible {
  background-size: 100% 100%, 100% 100%;
  border-color: rgba(200, 16, 46, 0.4);
  box-shadow: 0 0 30px rgba(200, 16, 46, 0.35), 0 0 5px rgba(200, 16, 46, 0.5);
  transform: translateY(-1px);
}
.portal-tile:hover .pt-corner,
.portal-tile:focus-visible .pt-corner {
  border-color: #f5f0ea;
}
.portal-tile:hover .pt-tl,
.portal-tile:focus-visible .pt-tl {
  transform: translate(-3px, -3px);
}
.portal-tile:hover .pt-tr,
.portal-tile:focus-visible .pt-tr {
  transform: translate(3px, -3px);
}
.portal-tile:hover .pt-bl,
.portal-tile:focus-visible .pt-bl {
  transform: translate(-3px, 3px);
}
.portal-tile:hover .pt-br,
.portal-tile:focus-visible .pt-br {
  transform: translate(3px, 3px);
}
.portal-tile:hover .pt-eyebrow,
.portal-tile:hover .pt-label,
.portal-tile:focus-visible .pt-eyebrow,
.portal-tile:focus-visible .pt-label {
  color: #f5f0ea;
}
.portal-tile:hover .pt-sub,
.portal-tile:focus-visible .pt-sub {
  color: rgba(255, 240, 240, 0.78);
}
.portal-tile:hover .pt-arrow,
.portal-tile:focus-visible .pt-arrow {
  color: #fff;
  transform: translateX(4px);
}
.portal-tile:active {
  transform: translateY(0);
}

/* r214 — DESKTOP SHORT tier: his real machine is ~1536×775 CSS (Windows
   125% scale — the r213 one-screen pass at 1600×900 missed it and the
   clipped CTA + scrollbar were on camera in video #54). Trim the vertical
   rhythm so the whole read still lands on one screen down to ~720px tall. */
@media (max-height: 830px) and (min-width: 641px) {
  .portal-scroll {
    padding-top: 4.4rem;
    padding-bottom: 1.4rem;
  }
  .pp-title {
    font-size: clamp(1.3rem, 2.2vw, 1.7rem);
    margin-top: 0.55rem;
  }
  .pp-standfirst {
    font-size: 0.96rem;
    line-height: 1.58;
    margin-top: 0.85rem;
  }
  .pp-body {
    font-size: 0.88rem;
    line-height: 1.62;
    margin-top: 0.95rem;
  }
  .pp-foot {
    margin-top: 1.5rem;
  }
}

/* tiers — compact stacks the tile full-width in the bottom band; shallow
   (landscape phones) trims the quiet layers to keep the band short */
@media (max-width: 640px) {
  .portal-scroll {
    padding: 5.6rem 3.5vw 3rem;
  }
  .portal-stage {
    width: 94vw;
  }
  .pp-foot {
    gap: 1rem;
  }
  .portal-mist img {
    opacity: 0.06;
  }
  /* phones: the smart layer's blur radius is the compositor cost — halve it
     and lean on a deeper brightness press instead */
  .portal-smart {
    backdrop-filter: blur(4px) brightness(0.34) saturate(0.5);
    -webkit-backdrop-filter: blur(4px) brightness(0.34) saturate(0.5);
  }
  .portal-wake {
    display: none; /* the wake is a mouse behavior */
  }
  .portal-chrome {
    padding: 0.95rem 1rem;
  }
  /* igloo mobile chrome: the drawn mark alone — the two-line wordmark
     crowded the band and deepened the mask the copy must clear */
  .portal-brand span {
    display: none;
  }
  .portal-tile {
    width: 100%;
    padding: 0.85rem 1rem;
  }
  .pt-label {
    font-size: 0.76rem;
    letter-spacing: 0.16em;
    white-space: normal;
  }
}
.portal-tile[data-tier="shallow"] {
  margin-top: 0.55rem;
  padding: 0.62rem 0.95rem;
  row-gap: 0.18rem;
}
.portal-tile[data-tier="shallow"] .pt-sub {
  display: none;
}

/* landscape phones: tighter still — most of the read fits the 390px band */
@media (max-height: 520px) and (min-width: 641px) {
  .portal-scroll {
    padding: 4.2rem 1.6rem 1.2rem;
  }
  .pp-body {
    font-size: 0.84rem;
  }
  .pp-foot {
    margin-top: 1.2rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .portal,
  .portal-stage {
    animation: none;
  }
  .portal-mist img {
    animation: none;
  }
  .portal-wake {
    display: none;
  }
}
