/* ═══════════════════════════════════════════════════════════════════════════
   rzv.studio — base.css  (FOUNDATION, owns tokens + every shared surface)

   Everything here is lifted from the rzv dashboard so the two products share
   one material language. Source of each recipe is credited in the comment
   above it — go read the original before changing a number.

   Load order on every page:   base.css → intro.css → constellation.css →
   work.css   (section stylesheets may override, never re-declare tokens).

   TWO TRAPS, obeyed throughout:
   1. Never `backdrop-filter` on an ancestor of a `mix-blend-mode` child —
      iOS Safari isolates the blend group against transparent-black and paints
      a solid black box. The ≤760px block at the bottom of this file strips
      the blur from every glass surface and drops the blends with it.
   2. GSAP owns `transform` on tiles / the pearl / mesh blooms. `transform` is
      deliberately ABSENT from those elements' CSS transition lists — a CSS
      transition on a property GSAP writes every frame smears each frame into
      the next.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Tokens (BRIEF.md §Design tokens — dashboard DNA, focus.html :root) ──── */
:root{
  /* Tells the UA which way the built-in widgets, scrollbars and form controls
     should render. Without it a dark page gets light scrollbars. */
  color-scheme:dark;

  --bg:#08080A; --bg-2:#0C0C10;
  --surface:rgba(255,255,255,0.025); --surface-2:rgba(255,255,255,0.045); --surface-hi:rgba(255,255,255,0.07);
  --border:rgba(255,255,255,0.06); --border-hi:rgba(255,255,255,0.13);
  --text:#FAFAFA; --muted:rgba(250,250,250,0.5); --dim:rgba(250,250,250,0.28); --faint:rgba(250,250,250,0.1);
  --card:rgba(22,22,28,0.58); --card-hi:rgba(30,30,38,0.68); --tile-sheen:rgba(255,255,255,0.07);
  --ease:cubic-bezier(0.16,1,0.3,1);            /* house curve (expo.out) */
  --ease-spring:cubic-bezier(0.34,1.56,0.64,1); /* overshoot */
  --mono:'Space Mono',ui-monospace,SFMono-Regular,Menlo,monospace;

  /* Semantic pair, dashboard names (focus.html --done/--over, portal.css
     --ptl-pos/--ptl-neg). Nothing on this site is a status readout yet; they
     exist so the first thing that needs one does not invent its own green. */
  --pos:#4ADE80; --neg:#F87171;

  /* The page aura wash — focus.html:63. One very soft ellipse bled off the top
     edge, which is what stops a flat #08080A field reading as a black
     rectangle. Painted by body::before below. */
  --aura:rgba(167,139,250,0.05);

  /* Aurora palette — the six, in order. Named so JS can read them off :root. */
  --a1:#4F46E5; --a2:#818CF8; --a3:#22D3EE; --a4:#A78BFA; --a5:#F472B6; --a6:#6366F1;

  /* Radii scale (BRIEF §Design tokens) */
  --r-tile:24px; --r-modal:28px; --r-card:14px; --r-pill:100px;

  /* Rhythm */
  --wrap:1360px;
  --gutter:clamp(20px,4vw,56px);
  --section-y:clamp(96px,14vh,180px);
  --header-h:72px;
}

/* ── PHASE-LIGHT ──────────────────────────────────────────────────────────
   The white takeover (#constellation). Implemented as a token swap on
   <body> rather than a pile of per-element overrides: every rule below reads
   --text/--border/--surface, so the whole site is correct in both phases with
   nothing extra to maintain. intro.js / constellation.js flip it through
   RZV.phase('light') / RZV.phase('dark').
   `html:has()` carries the same background out to the overscroll gutter, which
   <body> alone cannot reach. ── */
body.phase-light{
  color-scheme:light;

  /* Every value below is the dashboard's light theme verbatim (focus.html
     html[data-theme="light"]) — EXCEPT --bg. The dashboard's light page is
     #FAFAF8, a warm paper; this one is a deliberate #FFFFFF because the
     takeover is a white-out, not a theme. Keep them in sync when the
     dashboard's light values move; keep --bg pure. */
  --bg:#FFFFFF; --bg-2:#F3F3F6;
  --surface:rgba(0,0,0,0.022); --surface-2:rgba(0,0,0,0.04); --surface-hi:rgba(0,0,0,0.06);
  --border:rgba(0,0,0,0.08); --border-hi:rgba(0,0,0,0.16);
  --text:#17171A; --muted:rgba(20,20,25,0.52); --dim:rgba(20,20,25,0.32); --faint:rgba(20,20,25,0.12);
  --card:rgba(255,255,255,0.62); --card-hi:rgba(255,255,255,0.8); --tile-sheen:rgba(0,0,0,0.045);
  --pos:#16A34A; --neg:#DC2626;
  /* Indigo, not violet: on white the violet wash greys out, and indigo is what
     the dashboard's light aura reads as. */
  --aura:rgba(79,70,229,0.05);
}
html:has(body.phase-light){background:#FFFFFF}

/* ── Reset ──────────────────────────────────────────────────────────────── */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{background:var(--bg);-webkit-text-size-adjust:100%}
html:focus-within{scroll-behavior:smooth}
body{
  font-family:'Inter',system-ui,-apple-system,'Segoe UI',sans-serif;
  background:var(--bg);color:var(--text);
  line-height:1.55;font-weight:400;
  -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
  font-feature-settings:"ss01","cv11";
  overflow-x:hidden;min-height:100vh;
  transition:background-color .9s var(--ease),color .9s var(--ease);
}
/* PAGE AURA WASH — focus.html:63, verbatim. Fixed, so it stays anchored to the
   top of the VIEWPORT rather than scrolling away with the document: the whole
   site keeps one faint source of light overhead. z-index 0 and it is the first
   box in body, so every section (position:relative, z auto) paints over it.
   body::before is unclaimed on this site — nothing else may take it. */
body::before{content:'';position:fixed;inset:0;
  background:radial-gradient(ellipse 900px 500px at 50% -15%,var(--aura),transparent 62%);
  pointer-events:none;z-index:0}
/* Custom properties do not interpolate, so a phase swap would otherwise be a
   hard cut on every child. This softens the elements that have no opinion of
   their own about it.

   IT ONLY REACHES ELEMENTS WITH NO `transition` SHORTHAND OF THEIR OWN. These
   are longhands; any later shorthand at equal-or-higher specificity resets all
   of them wholesale. That is not a bug to route around — an element that
   declares its own transition has already said how it fades, and it keeps
   saying it. .nav-link, .btn, .ft-link, .wt-name (work.css:141) and .pp-name
   (work.css:178) each carry `color` in their own shorthand, so they cross-fade
   on their own clock; .site-header carries only opacity/transform and does
   hard-cut its background, which is invisible because html.hdr-veil has it
   hidden for the whole light phase. All six were listed here and are not, now.

   So: add a selector here only if it has NO transition shorthand anywhere. */
body,.brand-text,.overline,p,h1,h2,h3,h4,li,a,.wt-meta{
  transition-property:color,border-color,background-color;
  transition-duration:.7s;transition-timing-function:var(--ease);
}
body.loading{overflow:hidden}
img,svg,video{display:block;max-width:100%}
img{height:auto}
a{color:inherit;text-decoration:none}
button,input,select,textarea{font:inherit;color:inherit}
button{background:none;border:none;cursor:pointer}
ul,ol{list-style:none}
:focus-visible{outline:2px solid var(--a2);outline-offset:3px;border-radius:4px}
::selection{background:rgba(129,140,248,0.28);color:var(--text)}
/* A 0.28-alpha indigo over white is nearly invisible, and --text has flipped
   to near-black underneath it — both halves have to move together. */
body.phase-light ::selection{background:rgba(79,70,229,0.22);color:#17171A}

/* ── Layout ─────────────────────────────────────────────────────────────── */
.wrap{width:100%;max-width:var(--wrap);margin:0 auto;padding:0 var(--gutter);position:relative;z-index:1}
.section{position:relative;padding:var(--section-y) 0}
.section-head{display:flex;flex-direction:column;gap:18px;margin-bottom:clamp(44px,6vw,86px)}

/* ── Typography ──────────────────────────────────────────────────────────
   Display = Inter 300 at −0.04em. Weight is the whole voice here: 300 at
   5.6rem reads as drawn, 400 reads as a system font blown up. ── */
.display,h1,h2{font-weight:300;letter-spacing:-0.04em;line-height:1.04}
h1{font-size:clamp(3rem,6vw,5.6rem)}
h2,.t-xl{font-size:clamp(2.1rem,4.4vw,3.6rem)}
h3,.t-lg{font-size:clamp(1.3rem,2.2vw,1.95rem);font-weight:400;letter-spacing:-0.03em;line-height:1.15}
.t-md{font-size:clamp(1.05rem,1.6vw,1.35rem);font-weight:300;letter-spacing:-0.02em;line-height:1.4}
.t-body{font-size:clamp(0.95rem,1.05vw,1.0625rem);line-height:1.65;color:var(--muted)}
.measure{max-width:56ch}

/* HOUSE RULE (BRIEF §Design tokens): every overline / eyebrow label on this
   site is Space Mono, uppercase, 8.5–10.5px, .10–.18em tracking. Space Mono
   ships 400/700 only — 700 is what a `600` request resolves to anyway, so it
   is declared honestly. Recipe: settings.js `.sd-overline`. */
.overline{
  font-family:var(--mono);font-size:clamp(8.5px,0.62vw,10.5px);font-weight:700;
  letter-spacing:0.16em;text-transform:uppercase;color:var(--muted);
  display:block;line-height:1.4;
}
.overline--dim{color:var(--dim)}

/* ── PEARL ───────────────────────────────────────────────────────────────
   Verbatim material from portal.css `.ptl-pearl` (the 3-layer iridescent
   stack) — a white specular highlight, a 240%-wide aurora sweep that the
   iriDrift keyframe walks left→right, and the base pearl gradient underneath.
   Parameterised by --pearl-size so ONE class covers the 9px header dot, the
   25px hero pearl and the full-screen moon of the loader: every offset is a
   ratio of the size, floored with max() so the inner highlight survives at
   9px. --pearl-glow is the outer bloom's alpha — intro.js pumps it to sell
   the dying-star climax.
   `transform` is absent from the transition list on purpose (trap 2). ── */
.pearl{
  --pearl-size:34px;
  --pearl-glow:0.45;
  position:relative;flex:none;
  width:var(--pearl-size);height:var(--pearl-size);border-radius:50%;
  border:1px solid rgba(255,255,255,0.65);
  background:
    radial-gradient(circle at 32% 26%, rgba(255,255,255,0.95), rgba(255,255,255,0) 58%),
    linear-gradient(120deg, rgba(129,140,248,0.34), rgba(244,114,182,0.26) 32%, rgba(34,211,238,0.30) 62%, rgba(167,139,250,0.32) 95%),
    linear-gradient(160deg,#ffffff,#e9ecf6);
  background-size:100% 100%, 240% 240%, 100% 100%;
  box-shadow:
    inset 0 max(1px, calc(var(--pearl-size) * 0.03)) max(1px, calc(var(--pearl-size) * 0.03)) rgba(255,255,255,0.9),
    inset 0 calc(var(--pearl-size) * -0.059) calc(var(--pearl-size) * 0.176) rgba(167,139,250,0.28),
    0 calc(var(--pearl-size) * 0.117) calc(var(--pearl-size) * 0.412) calc(var(--pearl-size) * -0.117) rgba(129,140,248,var(--pearl-glow));
  animation:iriDrift 7s ease-in-out infinite;
  transition:filter .3s var(--ease),box-shadow .4s var(--ease);
}
@keyframes iriDrift{0%,100%{background-position:0 0,0% 50%,0 0}50%{background-position:0 0,100% 50%,0 0}}
/* On white the indigo bloom disappears and the pearl loses its edge — swap
   the outer shadow for a soft graphite one. */
body.phase-light .pearl{
  border-color:rgba(10,8,10,0.14);
  box-shadow:
    inset 0 max(1px, calc(var(--pearl-size) * 0.03)) max(1px, calc(var(--pearl-size) * 0.03)) rgba(255,255,255,0.95),
    inset 0 calc(var(--pearl-size) * -0.059) calc(var(--pearl-size) * 0.176) rgba(129,140,248,0.34),
    0 calc(var(--pearl-size) * 0.117) calc(var(--pearl-size) * 0.412) calc(var(--pearl-size) * -0.117) rgba(10,8,10,calc(var(--pearl-glow) * 0.4));
}

/* ── GLASS ───────────────────────────────────────────────────────────────
   portal.css `.ptl-glass` — the one surface shared by chips, tiles and the
   composer, so every floating thing on the site is literally the same
   material.

   ⚠️ NOT DEAD — DO NOT DELETE. Nothing currently carries `class="glass"`, so a
   usage census will call this unused. It is the CANONICAL recipe: work.css
   re-implements it twice (`.pf-input`, `.pf-chip`) and the copies have already
   drifted apart (box-shadow -16px here and in .pf-input, -18px in .pf-chip).
   Those two should compose onto this class instead; the consolidation is
   dispatched separately. Delete this and the drift becomes permanent. ── */
.glass{
  border-radius:var(--r-tile);
  border:1px solid rgba(255,255,255,0.12);
  background:linear-gradient(160deg,rgba(255,255,255,0.09),rgba(255,255,255,0.02) 50%);
  backdrop-filter:blur(20px) saturate(1.45);-webkit-backdrop-filter:blur(20px) saturate(1.45);
  box-shadow:inset 0 1px 0 rgba(255,255,255,0.14), 0 10px 28px -16px rgba(0,0,0,0.4);
  /* The spring on transform is part of the recipe, not a flourish: .ptl-glass
     is a PRESSABLE surface in the dashboard and the overshoot is what makes it
     feel like one. It was missing here, so glass read as inert. Unlike .tilt,
     nothing GSAP-driven owns transform on .glass — trap 2 does not apply. */
  transition:border-color .35s var(--ease),background .35s var(--ease),transform .25s var(--ease-spring);
}
.glass:hover,.glass:focus-visible{
  border-color:rgba(255,255,255,0.22);
  background:linear-gradient(160deg,rgba(255,255,255,0.13),rgba(255,255,255,0.04) 50%);
}
body.phase-light .glass{
  border-color:rgba(10,8,10,0.10);
  background:linear-gradient(160deg,rgba(255,255,255,0.9),rgba(255,255,255,0.45) 50%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,0.9), 0 10px 28px -18px rgba(10,8,10,0.22);
}
body.phase-light .glass:hover{border-color:rgba(10,8,10,0.2)}

/* ── BUTTONS / PILLS ────────────────────────────────────────────────────
   Glass at rest. The aurora face (portal.css `.ptl-cbtn.primary`) lives on a
   ::before and fades in — gradients cannot interpolate, an opacity ramp on a
   second layer can. ── */
.btn{
  position:relative;isolation:isolate;overflow:hidden;
  display:inline-flex;align-items:center;justify-content:center;gap:10px;
  padding:15px 28px;border-radius:var(--r-pill);
  font-size:14px;font-weight:500;letter-spacing:-0.01em;color:var(--text);
  border:1px solid var(--border-hi);
  background:linear-gradient(160deg,rgba(255,255,255,0.09),rgba(255,255,255,0.02) 50%);
  backdrop-filter:blur(20px) saturate(1.45);-webkit-backdrop-filter:blur(20px) saturate(1.45);
  cursor:pointer;
  transition:transform .28s var(--ease-spring),border-color .35s var(--ease),color .35s var(--ease);
}
.btn>*{position:relative;z-index:1}
/* Rest opacity is 0.35, not 0. The dashboard's .ptl-cbtn wears its aurora
   permanently — the colour is what the button IS, and hovering only brings it
   to full. Starting at 0 made every button a grey capsule until touched, which
   is the one thing the house material never does. */
.btn::before{
  content:'';position:absolute;inset:0;z-index:0;opacity:0.35;pointer-events:none;
  background:
    linear-gradient(160deg, rgba(255,255,255,0.45), rgba(255,255,255,0) 44%),
    linear-gradient(115deg, rgba(244,114,182,0.92), rgba(167,139,250,0.86) 48%, rgba(34,211,238,0.94));
  background-size:100% 100%, 200% 200%;
  transition:opacity .4s var(--ease);
}
.btn:hover{transform:translateY(-2px);border-color:rgba(255,255,255,0.42)}
.btn:hover::before{opacity:1}
.btn:active{transform:translateY(0) scale(0.985)}
body.phase-light .btn{border-color:rgba(10,8,10,0.14);
  background:linear-gradient(160deg,rgba(255,255,255,0.92),rgba(255,255,255,0.5) 50%)}

/* The Start-project CTA: same recipe, bigger, and it goes fully aurora with
   near-black type on hover so it reads as the one committed action on the
   page. `--btn-drift` is idle motion on the aurora layer, frozen when motion
   is off. */
.btn-primary{padding:19px 38px;font-size:15px;font-weight:600}
.btn-primary::before{animation:btnDrift 9s ease-in-out infinite}
.btn-primary:hover{color:#0b0b14;border-color:rgba(255,255,255,0.42)}
body.phase-light .btn-primary:hover{color:#0b0b14}
@keyframes btnDrift{0%,100%{background-position:0 0,0% 50%}50%{background-position:0 0,100% 50%}}

.btn-lg{padding:24px 46px;font-size:clamp(15px,1.5vw,18px);border-radius:var(--r-pill)}

/* THE SECONDARY — portal.css `.ptl-cbtn` (non-primary). Quarter-strength
   aurora, painted into the surface rather than layered on a ::before, so it is
   permanent: this button is coloured at rest and stays that strength on hover.
   That is the whole distinction it carries. Letting the shared ::before run
   would stack a second aurora on top and ramp it to primary strength on hover,
   collapsing the hierarchy the pair exists to express — so it is suppressed in
   both states, and hover is the lift plus a brighter rim instead. */
.btn-ghost{
  color:var(--text);
  border-color:rgba(255,255,255,0.16);
  background:
    linear-gradient(160deg, rgba(255,255,255,0.22), rgba(255,255,255,0) 42%),
    linear-gradient(135deg, rgba(244,114,182,0.15), rgba(129,140,248,0.10) 45%, rgba(34,211,238,0.15));
}
.btn-ghost::before,.btn-ghost:hover::before{opacity:0}
.btn-ghost:hover{color:var(--text);border-color:rgba(255,255,255,0.30)}
body.phase-light .btn-ghost{border-color:rgba(0,0,0,0.12)}
body.phase-light .btn-ghost:hover{border-color:rgba(0,0,0,0.24)}

/* A primary and a secondary standing side by side. Wraps to a column before
   the pair can ever overflow the gutter. */
.contact-ctas{display:flex;flex-wrap:wrap;align-items:center;gap:14px}

/* ── SWITCH (settings.js `.sd-switch`) ──────────────────────────────────
   Wired on this site as MOTION ON/OFF, persisted at localStorage
   'rzv-motion' — see nav.js. ── */
.rzv-switch{position:relative;width:38px;height:22px;flex-shrink:0;display:inline-block}
.rzv-switch input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer;z-index:1}
.rzv-switch-track{position:absolute;inset:0;background:var(--surface-hi);border:1px solid var(--border);
  border-radius:var(--r-pill);transition:background .25s var(--ease),border-color .25s var(--ease)}
.rzv-switch-thumb{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;
  background:var(--muted);transition:transform .25s var(--ease-spring),background .25s var(--ease)}
.rzv-switch input:checked ~ .rzv-switch-track{background:var(--text);border-color:var(--text)}
.rzv-switch input:checked ~ .rzv-switch-thumb{transform:translateX(16px);background:var(--bg)}
.rzv-switch input:focus-visible ~ .rzv-switch-track{outline:2px solid var(--a2);outline-offset:3px}

/* ── MESH GRADIENTS (client-page.css:12–39) ─────────────────────────────
   Three brand-coloured blooms drifting on 26/34/41s alternate loops, tinted
   per host from --m1/--m2/--m3. Default is `position:absolute` here (work
   tiles, case heroes); `.mesh--page` restores the fixed, masked page-backdrop
   form the dashboard uses.
   mix-blend-mode:screen is what makes them read as light rather than paint —
   it is dropped, not faked, on mobile (trap 1, bottom of file). ── */
.mesh{position:absolute;inset:0;z-index:0;pointer-events:none;overflow:hidden;
  opacity:0;transition:opacity 1.2s var(--ease)}
.mesh.on{opacity:1}
.mesh--page{position:fixed;top:0;left:0;right:0;bottom:auto;height:62vh;
  -webkit-mask-image:linear-gradient(to bottom,#000 0%,rgba(0,0,0,0.62) 38%,transparent 88%);
  mask-image:linear-gradient(to bottom,#000 0%,rgba(0,0,0,0.62) 38%,transparent 88%)}
body.phase-light .mesh.on{opacity:0.28}
.mb{position:absolute;border-radius:50%;mix-blend-mode:screen;filter:blur(var(--mesh-blur,42px));will-change:transform}
.mb.m1{background:radial-gradient(circle, color-mix(in srgb, var(--m1,transparent) 62%, transparent), transparent 66%);
  width:78%;height:150%;left:-22%;top:-58%;animation:meshA 26s ease-in-out infinite alternate}
.mb.m2{background:radial-gradient(circle, color-mix(in srgb, var(--m2,transparent) 48%, transparent), transparent 66%);
  width:66%;height:135%;left:28%;top:-66%;animation:meshB 34s ease-in-out infinite alternate}
.mb.m3{background:radial-gradient(circle, color-mix(in srgb, var(--m3,transparent) 40%, transparent), transparent 68%);
  width:70%;height:140%;left:60%;top:-60%;animation:meshC 41s ease-in-out infinite alternate}
@keyframes meshA{0%{transform:none}50%{transform:translate(9%,5%) scale(1.14)}100%{transform:translate(-6%,-3%) scale(0.94)}}
@keyframes meshB{0%{transform:translate(0,0) scale(1.06)}50%{transform:translate(-11%,6%) scale(0.9)}100%{transform:translate(7%,-4%) scale(1.18)}}
@keyframes meshC{0%{transform:translate(0,0) scale(0.96)}50%{transform:translate(8%,-6%) scale(1.2)}100%{transform:translate(-9%,4%) scale(1.02)}}

/* ── AURORA BLOBS (portal.css:90–119) ───────────────────────────────────
   Six screen-blended radials with a five-stop falloff. The five stops are the
   point: a single `transparent 64%` leaves a visible ring where the
   interpolation stops, and these are large enough that the ring would show.

   ARCHITECTURAL RULE — the blob geometry below is ASPECT-RATIO-LOCKED. Every
   width/height/left/bottom is a percentage of this box, tuned as one
   composition against the dashboard's panel proportions. NEVER adapt a blob's
   size or position to fit a new home: size and anchor the HOST instead (see
   .au), and the composition arrives intact. A blob nudged to "look right" in
   one host is a blob that is wrong in every other. ── */
.aurora{position:absolute;inset:-22%;pointer-events:none;z-index:0;
  transition:opacity .55s var(--ease);
  /* Load-bearing, and not only for softness: a filter makes this element a
     stacking context, which isolates the six mix-blend-mode:screen children
     into their own group. They then composite against each OTHER — which is
     the light-on-light bloom the dashboard shows — instead of screening
     individually against whatever the page happens to have behind them. */
  filter:blur(var(--au-blur,12px)) saturate(var(--au-sat,1.6)) brightness(1.05)}
.aurora .ab{position:absolute;mix-blend-mode:screen;border-radius:50%;will-change:transform,opacity}
.aurora .ab.a1{width:124%;height:110%;left:-41%;bottom:-41%;
  background:radial-gradient(circle closest-side,rgba(79,70,229,1),rgba(79,70,229,0.82) 22%,rgba(79,70,229,0.5) 42%,rgba(79,70,229,0.22) 60%,rgba(79,70,229,0.06) 78%,rgba(79,70,229,0) 92%)}
.aurora .ab.a2{width:104%;height:92%;left:-3%;bottom:3%;
  background:radial-gradient(circle closest-side,rgba(129,140,248,1),rgba(129,140,248,0.8) 22%,rgba(129,140,248,0.48) 42%,rgba(129,140,248,0.2) 60%,rgba(129,140,248,0.05) 78%,rgba(129,140,248,0) 92%)}
.aurora .ab.a3{width:98%;height:86%;left:37%;bottom:-31%;
  background:radial-gradient(circle closest-side,rgba(34,211,238,1),rgba(34,211,238,0.78) 22%,rgba(34,211,238,0.46) 42%,rgba(34,211,238,0.19) 60%,rgba(34,211,238,0.05) 78%,rgba(34,211,238,0) 92%)}
.aurora .ab.a4{width:108%;height:96%;left:25%;top:-25%;
  background:radial-gradient(circle closest-side,rgba(167,139,250,1),rgba(167,139,250,0.8) 22%,rgba(167,139,250,0.48) 42%,rgba(167,139,250,0.2) 60%,rgba(167,139,250,0.05) 78%,rgba(167,139,250,0) 92%)}
.aurora .ab.a5{width:92%;height:84%;left:-26%;bottom:14%;
  background:radial-gradient(circle closest-side,rgba(244,114,182,1),rgba(244,114,182,0.76) 22%,rgba(244,114,182,0.44) 42%,rgba(244,114,182,0.18) 60%,rgba(244,114,182,0.04) 78%,rgba(244,114,182,0) 92%)}
.aurora .ab.a6{width:96%;height:86%;left:8%;bottom:-44%;
  background:radial-gradient(circle closest-side,rgba(99,102,241,1),rgba(99,102,241,0.8) 22%,rgba(99,102,241,0.48) 42%,rgba(99,102,241,0.2) 60%,rgba(99,102,241,0.05) 78%,rgba(99,102,241,0) 92%)}

/* ── .au — THE AURORA HOST ──────────────────────────────────────────────
   Mirrors .mesh's contract exactly: an empty positioned box that RZV.aurora()
   fills with the six blobs, invisible until something adds .on. Strength is
   --au-o so each host picks its own without touching the blobs.

   LOAD-BEARING: a host MUST pin its own aspect-ratio (and its own size and
   anchor). The blob composition is ratio-locked — see the rule above — so a
   host left to be whatever shape its content makes it will stretch the whole
   arrangement into a smear. Every host declares `aspect-ratio` and a width;
   #contact > .au below is the worked example. Never fix a bad-looking aurora
   by editing .ab; fix the host's box. ── */
.au{position:absolute;z-index:0;pointer-events:none;overflow:hidden;
  opacity:0;transition:opacity 1.2s var(--ease)}
.au.on{opacity:var(--au-o,0.42)}

/* ── FILM GRAIN (portal.css:290–296) ────────────────────────────────────
   feTurbulence as a data URI, `screen` at 0.045. Screen and not overlay:
   overlay(0) = 0, i.e. invisible on exactly the near-black surface that needs
   the texture. Screen lifts instead. ── */
.grain{
  position:fixed;inset:0;z-index:60;pointer-events:none;
  mix-blend-mode:screen;opacity:0.045;
  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.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size:160px 160px;background-repeat:repeat;
}
/* Screen on white is a no-op — flip to multiply so the grain still bites. */
body.phase-light .grain{mix-blend-mode:multiply;opacity:0.05}

/* ── TILT TILE (focus.html:77–86 CSS + perf-sections.js:607–627 JS) ─────
   The sheen tracks the cursor through --mx/--my; the ::after is a soft outer
   glow parked behind the translucent card. `transform` is NOT in the
   transition list — RZV.attachTilt() writes rotationX/rotationY/y/scale every
   frame (trap 2). ── */
.tilt{
  position:relative;
  background:var(--card);border:1px solid var(--border);border-radius:var(--r-tile);
  overflow:hidden;backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
  transition:border-color .3s ease,background .3s ease;
}
.tilt::before{content:'';position:absolute;inset:0;pointer-events:none;opacity:0;
  background:radial-gradient(150% 150% at var(--mx,50%) var(--my,50%),var(--tile-sheen),rgba(255,255,255,0.04) 28%,rgba(255,255,255,0.012) 55%,transparent 82%);
  transition:opacity .45s var(--ease)}
.tilt::after{content:'';position:absolute;top:-1px;left:-1px;right:-1px;bottom:-1px;
  border-radius:var(--r-tile);box-shadow:0 0 32px var(--border-hi);
  opacity:0;z-index:-1;pointer-events:none;transition:opacity .5s var(--ease)}
.tilt>*{position:relative;z-index:1}
.tilt:hover{border-color:var(--border-hi);background:var(--card-hi)}
.tilt:hover::before{opacity:0.55}
.tilt:hover::after{opacity:0.4}

/* ═══ LOADER ════════════════════════════════════════════════════════════
   An ANTI-FLASH PLATE, nothing more. The opening film runs on the hero stage,
   not in here (see intro.css's header note) — intro.js dismisses this on its
   first frame, once the runtime hidden states are set, and it fades out black
   onto black. It stays empty for its whole life.
   It only exists when the page can animate (html.rzv-anim, decided pre-paint
   by the inline head script): with motion off nothing is hidden, so there is
   nothing to cover. index.html carries an inline 2.5s fallback that forces
   .is-done if intro.js never claims it, so a missing or failed intro.js can
   never leave the site behind a black rectangle. ═══ */
#loader{
  position:fixed;inset:0;z-index:9500;
  background:var(--bg);
  display:flex;align-items:center;justify-content:center;
  transition:opacity .9s var(--ease),visibility 0s linear .9s;
}
html:not(.rzv-anim) #loader{display:none}
#loader.is-done{opacity:0;visibility:hidden;pointer-events:none}

/* ═══ HEADER ════════════════════════════════════════════════════════════
   Fixed glass bar. view-transition-name:appheader (set inline in the HTML, as
   in the dashboard) keeps it as one persistent element across the index →
   case-page navigation while the rest of the document dips. ═══ */
.site-header{
  position:fixed;top:0;left:0;right:0;z-index:500;
  height:var(--header-h);display:flex;align-items:center;
  background:color-mix(in srgb, var(--bg) 62%, transparent);
  backdrop-filter:blur(22px) saturate(1.35);-webkit-backdrop-filter:blur(22px) saturate(1.35);
  border-bottom:1px solid transparent;
  transition:opacity 0.9s var(--ease),transform 0.9s var(--ease);
}
.site-header.scrolled{border-bottom-color:var(--border)}
/* The opening cinematic owns the screen chrome-free: intro.js veils the
   header (RZV.veilHeader) and the first return to dark after the white
   phase — the constellation collapse — unveils it for good (nav.js). */
html.hdr-veil .site-header{opacity:0;transform:translateY(-12px);pointer-events:none}
.header-inner{display:flex;align-items:center;justify-content:space-between;gap:18px}

.brand{display:inline-flex;align-items:center;gap:11px}
/* The dot IS the pearl — same material as the hero's, 9px. It arrives with
   the header itself, when nav.js unveils at the constellation collapse. */
.brand-pearl{--pearl-size:9px;--pearl-glow:0.9}
.brand-text{font-family:var(--mono);font-size:13px;font-weight:400;letter-spacing:0;color:var(--text)}

.nav-center{position:relative;display:inline-flex;gap:2px;padding:3px;
  background:var(--surface);border:1px solid var(--border);border-radius:var(--r-pill);
  backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}
/* Fixed px height, not inherited line-height, so the pill's geometry is exact
   and reproducible (dashboard header.html makes the same choice). */
.nav-link{position:relative;z-index:1;min-width:96px;height:34px;
  display:inline-flex;align-items:center;justify-content:center;
  font-size:11.5px;font-weight:500;letter-spacing:-0.005em;padding:0 10px;
  border-radius:var(--r-pill);color:var(--muted);transition:color .3s var(--ease)}
.nav-link:hover:not(.active){color:var(--text)}
.nav-link.active{color:var(--bg)}
/* Position + width are written by nav.js off the live link geometry (the
   dashboard hardcodes them; here the labels differ in width and the nav is
   built once, so measuring is both safer and shorter). */
.nav-pill{position:absolute;top:3px;bottom:3px;left:3px;width:96px;border-radius:var(--r-pill);
  background:var(--text);z-index:0;opacity:0;
  transition:left .38s var(--ease-spring),width .38s var(--ease-spring),opacity .3s var(--ease)}
.nav-center.has-active .nav-pill{opacity:1}

.nav-meta{display:flex;align-items:center;gap:10px}
.nav-meta-label{color:var(--dim)}

/* ═══ HERO ══════════════════════════════════════════════════════════════ */
#hero{min-height:100svh;display:flex;flex-direction:column;justify-content:center;
  padding-top:calc(var(--header-h) + clamp(60px,10vh,120px));padding-bottom:clamp(48px,8vh,96px)}
/* intro.js draws the pearl + its dotted orbit rings in here. Full-bleed and
   inert so the copy above it stays selectable. */
.hero-stage{position:absolute;inset:0;z-index:0;pointer-events:none;overflow:hidden}
/* The headline and CTA live in #hero (index.html), but their layout does NOT
   live here: .hero-inner / .hero-copy / .hero-title moved to intro.css, which
   pins .hero-inner to the viewport while the film runs and so has to own the
   whole box. See intro.css's own note at its .hero-inner rule.
   .hero-loc is the one hero-named rule still owned here, and it is not in
   #hero at all — index.html uses it inside .cn-salam, down in #constellation.
   The name is kept rather than renamed to .cn-loc because constellation.css
   and .js are another agent's lane and may already select it. */
.hero-loc{font-family:var(--mono);font-size:clamp(11px,1vw,13px);letter-spacing:0.1em;
  text-transform:uppercase;color:var(--dim);white-space:nowrap}

/* ═══ #constellation ════════════════════════════════════════════════════
   The section's box, and nothing else. Every .cn-* rule that gates on
   html.constellation-live now lives in constellation.css, with the class that
   switches it. ═══ */
/* The pre-claim box only. Once constellation.js claims the section,
   constellation.css:87 replaces every one of these (sticky, 100svh,
   min-height:0). No perspective / transform-style here: the stage went flat —
   "there is no 3D here at all any more" (constellation.css:80) — and the two
   declarations existed purely to be overridden. */
.constellation-stage{position:relative;width:100%;min-height:clamp(420px,62vh,720px)}

/* ═══ #work ═════════════════════════════════════════════════════════════ */
.work-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:clamp(22px,3vw,44px)}
.work-tile{display:flex;flex-direction:column;gap:16px;position:relative}
/* Editorial stagger — every second tile drops, so the grid reads as a column
   pair rather than a table. Collapsed on one column. */
.work-grid>.work-tile:nth-child(even){margin-top:clamp(0px,6vw,90px)}
.wt-media{position:relative;overflow:hidden;border-radius:var(--r-tile);
  aspect-ratio:4/3;background:var(--bg-2);border:1px solid var(--border);
  --mesh-blur:34px}
/* The mesh is BOTH the tint over a real cover and the whole picture when the
   cover is missing — .wt-media.no-img (set by the img's inline onerror) just
   turns it up. No broken-image icon ever ships. */
.wt-media .mesh{opacity:0.85}
.wt-media.no-img .mesh{opacity:1}
.wt-img{position:relative;z-index:1;width:100%;height:100%;object-fit:cover;
  transition:opacity .6s var(--ease)}
.wt-name{font-size:clamp(1.15rem,1.8vw,1.55rem);font-weight:300;letter-spacing:-0.03em;color:var(--text)}
.wt-meta{color:var(--dim)}
.wt-text{display:flex;flex-direction:column;gap:7px}

/* ═══ #purpose ══════════════════════════════════════════════════════════ */
.purpose-lines{display:flex;flex-direction:column;gap:clamp(8px,1vw,14px);max-width:22ch}
.purpose-projects{display:flex;flex-direction:column;margin-top:clamp(50px,7vw,100px)}
.pp-row{display:grid;grid-template-columns:minmax(200px,0.7fr) minmax(0,1.6fr);
  gap:clamp(20px,4vw,64px);padding:clamp(28px,3.4vw,44px) 0;border-top:1px solid var(--border)}
.pp-row:last-child{border-bottom:1px solid var(--border)}
.pp-name{font-size:clamp(1.2rem,2vw,1.7rem);font-weight:300;letter-spacing:-0.03em;color:var(--text)}

/* ═══ #contact + footer ═════════════════════════════════════════════════ */
#contact{padding-bottom:clamp(70px,9vh,120px)}
/* The aurora host, anchored bottom-left and pushed a little off-canvas so the
   composition's brightest corner sits behind the CTA rather than beside it.
   aspect-ratio is mandatory, not styling — it is what keeps the ratio-locked
   blob arrangement in proportion (see .au). The mask fades the top edge out so
   the wash dies into the page instead of ending on a line. */
#contact > .au{
  left:0;bottom:0;width:min(760px,94%);aspect-ratio:5/4;
  --au-o:0.34;
  transform:translateX(-16%);
  -webkit-mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,0.74) 48%,transparent 96%);
  mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,0.74) 48%,transparent 96%);
}
/* REQUIRED, not defensive. .au is a positioned sibling; an unpositioned block
   paints BELOW every positioned sibling regardless of source order, so the
   copy would sit under the wash. This is portal.css:283's exact lesson.
   .wrap already carries these — restated here so the constraint is visible at
   the point it matters and survives any future edit to .wrap. */
#contact > .wrap{position:relative;z-index:1}
.contact-inner{display:flex;flex-direction:column;gap:clamp(26px,3.4vw,44px);align-items:flex-start}
.contact-line{max-width:20ch}
.site-footer{border-top:1px solid var(--border);padding:clamp(52px,6vw,80px) 0 clamp(30px,4vw,44px)}
.footer-cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:clamp(26px,3vw,48px)}
.ft-col{display:flex;flex-direction:column;gap:14px}
.ft-col ul{display:flex;flex-direction:column;gap:9px}
.ft-link{font-size:13.5px;color:var(--muted);transition:color .25s var(--ease)}
.ft-link:hover{color:var(--text)}
.footer-base{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:20px;
  margin-top:clamp(46px,6vw,80px);padding-top:26px;border-top:1px solid var(--border)}
.ft-socials{display:flex;gap:8px}
.ft-social{padding:8px 16px;border-radius:var(--r-pill);border:1px solid var(--border);
  background:var(--surface);color:var(--muted);
  font-family:var(--mono);font-size:9.5px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;
  transition:color .25s var(--ease),border-color .25s var(--ease),transform .25s var(--ease-spring)}
.ft-social:hover{color:var(--text);border-color:var(--border-hi);transform:translateY(-2px)}
.ft-signoff{display:flex;flex-wrap:wrap;gap:18px;align-items:center}

/* ═══ CASE PAGES (work/*.html) ══════════════════════════════════════════
   Presentational only — the WORK agent duplicates work/_template.html and
   fills it. `view-transition-name:workhero` on .case-cover is one half of the
   shared-element morph; the other half is stamped onto the clicked tile's
   .wt-media at click time (see the contract comment in work/_template.html). ═══ */
.case-main{padding-top:var(--header-h)}
.case-hero{padding:clamp(48px,7vh,96px) 0 clamp(30px,4vw,52px)}
.case-title{max-width:18ch}
.case-intro{margin-top:clamp(22px,3vw,36px);max-width:62ch;font-size:clamp(1rem,1.35vw,1.2rem);
  font-weight:300;letter-spacing:-0.015em;line-height:1.55;color:var(--muted)}
.case-cover{position:relative;overflow:hidden;border-radius:var(--r-tile);
  aspect-ratio:16/9;background:var(--bg-2);border:1px solid var(--border);--mesh-blur:60px}
.case-cover .wt-img{width:100%;height:100%;object-fit:cover}
.case-meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
  gap:clamp(18px,2.4vw,36px);padding:clamp(30px,4vw,50px) 0;
  border-top:1px solid var(--border);border-bottom:1px solid var(--border);
  margin:clamp(38px,5vw,64px) 0}
.cm-item{display:flex;flex-direction:column;gap:9px}
.cm-value{font-size:14.5px;color:var(--text)}
.case-body{display:flex;flex-direction:column;gap:clamp(46px,6vw,84px);max-width:70ch}
.case-block{display:flex;flex-direction:column;gap:16px}
.case-block p + p{margin-top:12px}
.case-figure{margin:clamp(38px,5vw,70px) 0;border-radius:var(--r-tile);overflow:hidden;
  border:1px solid var(--border);background:var(--bg-2)}
.case-next{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap;
  padding:clamp(38px,5vw,64px) 0;border-top:1px solid var(--border);margin-top:clamp(46px,6vw,90px)}
.case-next>span:first-child{display:flex;flex-direction:column;gap:10px}
.case-next-name{font-size:clamp(1.5rem,3.4vw,2.6rem);font-weight:300;letter-spacing:-0.04em}
.case-next:hover .case-next-name{color:var(--text)}

/* ═══ REDUCED MOTION ════════════════════════════════════════════════════
   Freeze, don't remove (trap 3): the pearl keeps its iridescent stack and the
   mesh keeps its blooms, they simply stop travelling. Anything that was only
   ever visible mid-animation must therefore already be legible at rest — which
   is why nothing on this page starts at opacity:0 in CSS. ═══ */
@media(prefers-reduced-motion:reduce){
  html:focus-within{scroll-behavior:auto}
  *,*::before,*::after{
    animation-duration:.001ms!important;animation-iteration-count:1!important;
    transition-duration:.001ms!important;scroll-behavior:auto!important;
  }
  .pearl,.mb,.aurora .ab,.btn-primary::before{animation:none}
  .mesh{transition:none}
  /* RZV.aurora never starts a timeline here, so the blobs are permanently
     static — a promoted layer per blob costs memory and buys nothing. */
  .aurora .ab{will-change:auto}
}
/* Same freeze when the header switch is off, whatever the OS says. nav.js sets
   html.rzv-no-motion. */
html.rzv-no-motion .pearl,
html.rzv-no-motion .mb,
html.rzv-no-motion .aurora .ab,
html.rzv-no-motion .btn-primary::before{animation:none}
html.rzv-no-motion .aurora .ab{will-change:auto}

/* ═══ MOBILE (≤760px) ═══════════════════════════════════════════════════
   Single column, and painted-aurora-safe: every backdrop-filter that could
   sit above a mix-blend-mode child is removed and the blends go with it (trap
   1 — the iOS black box). The surfaces become genuinely opaque instead of
   translucent, which is also simply cheaper on a phone GPU. ═══ */
@media(max-width:760px){
  :root{--header-h:62px}

  /* Half the blobs are display:none at this width and the blend strip goes
     opaque — the host widens and brightens to compensate (aurora spec §2). */
  #contact > .au{--au-o:0.46;width:120%;aspect-ratio:1/1}

  .site-header,.nav-center,.glass,.btn,.tilt{
    backdrop-filter:none;-webkit-backdrop-filter:none;
  }
  .site-header{background:color-mix(in srgb, var(--bg) 92%, transparent)}
  .glass{background:var(--bg-2)}
  /* :not(.btn-ghost) — the ghost's aurora IS its surface, so flattening it to
     --surface-hi here would delete the button rather than de-blur it. */
  .btn:not(.btn-ghost){background:var(--surface-hi)}
  .tilt{background:var(--bg-2)}
  /* No blend group anywhere near a blurred ancestor. Alphas go up because a
     screen-blended blob and a painted one are not the same brightness. */
  .mb,.aurora .ab{mix-blend-mode:normal;opacity:0.72}
  .mesh{--mesh-blur:38px}
  .grain{opacity:0.035}

  /* Six painted, blurred, animated layers is a real cost on a phone. Three of
     them carry the composition (indigo / periwinkle / cyan); the violet,
     magenta and second indigo are the depth pass and are the ones to lose.
     Dropped, NOT resized — the survivors keep their authored geometry, per the
     ratio-lock rule on .aurora. */
  .au .aurora .ab.a4,.au .aurora .ab.a5,.au .aurora .ab.a6{display:none}
  .au .aurora .ab{will-change:auto}

  .nav-link{min-width:0;padding:0 13px;font-size:11px}
  .nav-pill{width:auto}
  .nav-meta-label{display:none}

  .contact-ctas{gap:12px}
  .contact-ctas .btn{flex:1 1 auto;justify-content:center}

  .work-grid{grid-template-columns:1fr;gap:34px}
  .work-grid>.work-tile:nth-child(even){margin-top:0}
  .wt-media{aspect-ratio:3/2}

  .pp-row{grid-template-columns:1fr;gap:12px}
  .case-cover{aspect-ratio:4/3}
  .footer-base{flex-direction:column;align-items:flex-start}
}
@media(max-width:460px){
  .brand-text{display:none}
  .nav-link{padding:0 10px;font-size:10.5px}
}
