/* ═══════════════════════════════════════════════════════════════
   Sanchit & Riya · Shubh Vivah — KINETIC layer (over TECHNO base)
   "A precision-engineering console booting a love story."
   techno.css loads FIRST (obsidian palette, section layout, gate /
   scratch / gallery / seal primitives). This file only LAYERS the
   saifullah.dev console look on top: a magnetic crosshair cursor,
   hairline instrument HUD, film-grain, a terminal boot readout in
   the gate, section-index markers, and scramble-text chrome.
   Reuse the techno :root vars (var(--accent), --hairline, --label,
   --font-mono, …). Add/override ONLY what the console look needs;
   rules that could collide with techno base are scoped under
   html[data-variant="kinetic"]; additive .k-* rules stay unscoped.

   Motion identity: saifullah's UI ease is cubic-bezier(.16,1,.3,1)
   at ~.6s — reserved for HUD/cursor transitions (--k-ease below).
   ═══════════════════════════════════════════════════════════════ */

:root {
  /* saifullah "settle" ease — used ONLY for the cursor ring + HUD trail so the
     console furniture glides while techno's own --ease-luxe drives the content */
  --k-ease: cubic-bezier(.16, 1, .3, 1);
  --k-ease-dur: .6s;
}

/* ═══ CROSSHAIR CURSOR (#k-cursor) ═══════════════════════════════
   Full-viewport hairline crosshair + a small magnetic ring and a mono
   coord readout. Pointer-events:none; sits just under the boot loader
   (9999) so it never eats clicks. JS (kinetic.js) writes the transforms;
   CSS supplies the resting look + a soft settle on the ring/readout only.
   Hidden on touch and under reduced-motion (native cursor restored). */
#k-cursor {
  position: fixed; inset: 0;
  z-index: 9998;              /* below the boot loader (9999), above all else */
  pointer-events: none;
}
/* the two hairlines span the whole viewport (100% of the fixed #k-cursor, NOT
   100vw — avoids a scrollbar-width horizontal overflow). JS translates them. */
.k-cursor-h {
  position: absolute; top: 0; left: 0;
  width: 100%; height: 1px;
  background: rgba(var(--accent-rgb), .18);
}
.k-cursor-v {
  position: absolute; top: 0; left: 0;
  width: 1px; height: 100%;
  background: rgba(var(--accent-rgb), .18);
}
/* The pointer itself — an arrow glyph, since this REPLACES the native cursor
   (cursor:none below) rather than decorating it. Two rules follow from that:

   1. HOTSPOT AT THE TIP. The arrow's point must sit exactly on the true pointer
      coordinate, so the shape is drawn with its tip at the box's 0,0 origin and JS
      translates the box unshifted — NO xPercent/yPercent -50, no negative margins.
      (Centring a 26px ring on the pointer is what previously drew it 13px up-and-left
      of the real hit point and made the 16px-tall side-menu rows unclickable.)
   2. NO EASING. The old ring lagged the pointer by a 0.35s quickTo "settle", which is
      charming on a decoration and unusable on the thing you aim with. JS writes this
      element with an instant quickSetter; only the crosshair hairlines and the readout
      keep their soft character.

   clip-path over inline SVG so the glyph stays in CSS with the rest of the look, and
   the drop-shadow keeps it legible over the bright MilkDrop presets. */
.k-cursor-arrow {
  position: absolute; top: 0; left: 0;
  width: 14px; height: 20px;
  background: rgba(var(--accent-rgb), .92);
  clip-path: polygon(0 0, 0 80%, 28% 60%, 50% 96%, 74% 86%, 50% 55%, 88% 55%);
  filter: drop-shadow(0 0 2px rgba(0, 0, 0, .85));  /* readable on a lit backdrop */
  transition: opacity var(--k-ease-dur) var(--k-ease),
              filter var(--k-ease-dur) var(--k-ease);
}
/* over interactive targets: brighten + halo. kinetic.js has always toggled .is-hot
   here but nothing in css/ styled it, so the hover feedback was dead code. Deliberately
   NOT a transform — GSAP owns this element's transform for the position write. */
#k-cursor.is-hot .k-cursor-arrow {
  background: rgb(var(--accent-rgb));
  filter: drop-shadow(0 0 3px rgba(0, 0, 0, .9)) drop-shadow(0 0 6px rgba(var(--accent-rgb), .55));
}
/* tiny mono coordinate label, offset from the ring toward lower-right */
.k-cursor-read {
  position: absolute; top: 0; left: 0; margin: 12px 0 0 16px;
  font-family: var(--font-mono);
  font-size: .6rem; letter-spacing: .12em;
  line-height: 1; white-space: nowrap;
  color: var(--label); opacity: .5;
  transition: opacity var(--k-ease-dur) var(--k-ease);
}

/* Hide the native cursor ONLY where the custom arrow is active (fine pointer with
   hover) — never on touch, so tap targets keep their default affordance.
   The universal selector is NOT redundant with the root rule: `cursor` inherits, but
   any descendant with its OWN cursor wins on specificity — and .k-menu-item, .k-wz-btn
   and friends all declare `cursor: pointer`. Inheriting `none` from the root therefore
   left the OS arrow showing over precisely the things you click. Hence `*` + !important,
   which is the documented pattern for a full cursor replacement. */
@media (hover: hover) and (pointer: fine) {
  html[data-variant="kinetic"],
  html[data-variant="kinetic"] *,
  html[data-variant="kinetic"] *::before,
  html[data-variant="kinetic"] *::after { cursor: none !important; }
}
/* touch / coarse pointers: no crosshair, restore the native cursor */
@media (hover: none), (pointer: coarse) {
  #k-cursor { display: none; }
}

/* ═══ HUD INSTRUMENT FRAME (#k-hud) ══════════════════════════════
   Fixed hairline chrome: four corner ticks + three whisper-quiet mono
   readouts (brand, section marker, local time). Decorative, pointer-
   events:none; above content but BELOW the gate (60) and cursor. This
   is instrument chrome — keep it faint, never a decorative border. */
#k-hud {
  position: fixed; inset: 0;
  z-index: 58;               /* > #smooth-wrapper (2) & #right-dock (55), < #gate (60) */
  pointer-events: none;
}
/* L-shaped corner ticks — two borders on a small box, ~14px inset from each edge */
.k-hud-corner {
  position: absolute;
  width: 18px; height: 18px;
  border: 0 solid var(--hairline);
}
.k-hud-tl { top: 14px; left: 14px;  border-top-width: 1px; border-left-width: 1px; }
.k-hud-tr { top: 14px; right: 14px; border-top-width: 1px; border-right-width: 1px; }
.k-hud-bl { bottom: 14px; left: 14px;  border-bottom-width: 1px; border-left-width: 1px; }
.k-hud-br { bottom: 14px; right: 14px; border-bottom-width: 1px; border-right-width: 1px; }

/* shared readout voice — mono, wide-tracked, upper, label-dim */
.k-hud-brand, .k-hud-sec, .k-hud-time {
  position: absolute;
  font-family: var(--font-mono);
  font-size: .62rem; letter-spacing: .2em;
  line-height: 1; text-transform: uppercase;
  color: var(--label);
}
.k-hud-brand { top: 1.05rem; left: 2.4rem; }              /* clear of the TL tick */
.k-hud-sec   { bottom: 1.05rem; left: 2.4rem; }           /* clear of the BL tick */
.k-hud-time  { top: 1.05rem; right: 2.4rem; text-align: right; font-variant-numeric: tabular-nums; }

/* ═══ FILM GRAIN (.k-grain) ══════════════════════════════════════
   A static fractal-noise texture so the obsidian never reads as flat.
   Layered ABOVE the light-show canvases (#milkdrop/#lightshow z0,
   .ls-vignette z1) but BELOW page content: z-index:2 matches
   #smooth-wrapper, and .k-grain precedes it in the DOM, so the content
   paints on top while the grain still covers the canvas haze. (techno's
   own body::after grain sits higher at z40 — this is an extra, softer
   pass tuned to the console look, deliberately kept faint.) */
.k-grain {
  position: fixed; inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: .045;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='kn'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23kn)'/%3E%3C/svg%3E");
  animation: k-grain-drift 1.1s steps(1) infinite;
}
/* stepped position jitter reads as live film grain (steps(1) = hard jumps) */
@keyframes k-grain-drift {
  0%   { background-position:   0    0; }
  12%  { background-position: -12%  -8%; }
  25%  { background-position:   9%  -14%; }
  37%  { background-position: -6%   11%; }
  50%  { background-position:  13%   6%; }
  62%  { background-position: -11%  -5%; }
  75%  { background-position:   5%  13%; }
  87%  { background-position: -9%   -11%; }
  100% { background-position:   0    0; }
}

/* ═══ GATE CONSOLE (.gate--kinetic / .k-console) ═════════════════
   techno already paints the obsidian stage + card. Here the card body
   becomes a terminal readout: a WEDDING_OS header with a pulsing dot,
   a boot progress bar with %, and a rounded-square "console" seal. */

/* Opaque console lock. techno's #gate is deliberately translucent so its
   abstract WebGL backdrop shows through before the tap — but the kinetic hero
   is large TEXT (the couple's names), which would bleed through that translucent
   gate behind the console. A boot console should fully occlude, so make the
   kinetic gate solid obsidian (the gate-stage grid/shaft still reads on top). */
html[data-variant="kinetic"] #gate {
  background:
    radial-gradient(120% 90% at 50% 40%, #101319 0%, var(--bg) 58%, var(--bg-deep) 100%);
}

/* header line: OS name + pulsing status dot + cyan version, mono & wide */
.k-console-head {
  display: flex; align-items: center; justify-content: center; gap: .5em;
  font-family: var(--font-mono);
  font-size: .6rem; letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--label);
  margin-bottom: 1rem;
}
.k-console-ver { color: var(--accent); letter-spacing: .14em; }
.k-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 6px rgba(var(--accent-rgb), .7);
  animation: k-dot-pulse 1.6s ease-in-out infinite;
}
@keyframes k-dot-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }

/* boot block — a thin hairline track with a cyan fill (JS animates width),
   and a single justified status/percent line beneath it */
.k-console-boot { margin: 1.2rem 0 1.3rem; }
.k-console-track {
  position: relative;
  width: 100%; height: 2px;
  background: var(--hairline);
  overflow: hidden;
  margin-bottom: .55rem;
}
.k-console-fill {
  display: block; height: 100%; width: 0;      /* JS animates width 0 → 100% */
  background: var(--accent);
  box-shadow: 0 0 8px rgba(var(--accent-rgb), .55);   /* soft cyan glow on the fill */
  transition: width .3s var(--k-ease);
}
.k-console-line {
  display: flex; align-items: baseline; justify-content: space-between;
  font-family: var(--font-mono);
}
.k-console-status {
  font-size: .6rem; letter-spacing: .2em;
  text-transform: uppercase; color: var(--accent);
}
.k-console-pct {
  font-size: .6rem; letter-spacing: .08em;
  color: var(--muted); font-variant-numeric: tabular-nums;
}

/* seal — keep techno's large tappable target, but swap the round ring for a
   rounded-SQUARE "console" frame (subtle). Scoped so it beats .seal--techno. */
html[data-variant="kinetic"] .seal--kinetic { border-radius: 16px; }
html[data-variant="kinetic"] .seal--kinetic .seal-ring { border-radius: 11px; }
html[data-variant="kinetic"] .seal--kinetic .seal-ripple { border-radius: 18px; }

/* ═══ SECTION INDEX (.k-index) ═══════════════════════════════════
   A console list marker ("01 / 05") above each section kicker: a short
   cyan leading rule + dim-cyan mono numerals. aria-hidden chrome. */
.k-index {
  display: block;
  margin-bottom: .9rem;
  font-family: var(--font-mono);
  font-size: .62rem; letter-spacing: .28em;
  color: rgba(var(--accent-rgb), .6);
}
.k-index::before {
  content: ""; display: inline-block;
  width: 1.4em; height: 1px; margin-right: .7em;
  vertical-align: middle;
  background: rgba(var(--accent-rgb), .55);
}
/* defensive: on desktop techno turns .countdown .wrap and .interlude .wrap--narrow
   into grids — make the index span the full row so it never steals a column and
   breaks the split beneath it (harmless when those wraps aren't grids) */
html[data-variant="kinetic"] .countdown .k-index,
html[data-variant="kinetic"] .interlude .k-index { grid-column: 1 / -1; }

/* ═══ SCRAMBLE-TEXT CHROME ═══════════════════════════════════════
   JS swaps characters, toggling .is-scrambling mid-decode. Mono during
   the swap = "terminal decoding", resolving back to the rest font. Keep
   the display font at REST (names must not stay monospace). */
html[data-variant="kinetic"] .is-scrambling { font-family: var(--font-mono); }
/* stop layout jump while characters swap: the names hold a stable box and never
   wrap mid-decode. (Left as inline-block on .hero-name only — .hero-amp keeps
   techno's display:flex, which draws its flanking hairlines on mobile.) */
html[data-variant="kinetic"] .hero-name { display: inline-block; white-space: nowrap; }
html[data-variant="kinetic"] .hero-amp { white-space: nowrap; }

/* ═══ MAGNETIC TARGETS ([data-magnetic]) ═════════════════════════
   JS drives the transform; CSS only hints the compositor. */
html[data-variant="kinetic"] [data-magnetic] { will-change: transform; }

/* ═══ REDUCED MOTION ═════════════════════════════════════════════
   No grain drift, no dot pulse, no crosshair — native cursor restored,
   everything static. (Content visibility: techno.css only hides .fade-up
   under html.js and reveals it under .reduce-motion, so a no-JS visitor
   already sees all content — no safety-net override needed here.) */
@media (prefers-reduced-motion: reduce) {
  .k-grain { animation: none; }
  .k-dot { animation: none; }
  #k-cursor { display: none; }
  /* restore the OS cursor — must mirror the universal selector + !important above,
     or the `*` rule keeps winning and reduced-motion visitors get no cursor at all */
  html[data-variant="kinetic"],
  html[data-variant="kinetic"] *,
  html[data-variant="kinetic"] *::before,
  html[data-variant="kinetic"] *::after { cursor: auto !important; }
}

/* ═══ RESPONSIVE — mobile HUD declutter ══════════════════════════
   On small screens keep only the corner ticks + the live time; the brand
   and section readouts would crowd the ~375px frame. */
@media (max-width: 640px) {
  /* the live time collides with section content inside the narrow crop-frame,
     so on mobile keep only the corner ticks — the chrome stays whisper-quiet */
  .k-hud-brand, .k-hud-sec, .k-hud-time { display: none; }
  .k-hud-corner { width: 14px; height: 14px; }   /* smaller ticks, tighter frame */
}

/* ═══════════════════════════════════════════════════════════════════
   ADVERSARIAL-REVIEW FIXES (pass 2)
   Scoped to kinetic; each addresses a specific finding from the design
   critique. techno/regency are untouched (kinetic.css loads only here).
   ═══════════════════════════════════════════════════════════════════ */

/* [#2/#3] The inline "11 & 12" ampersand inherited the name-lockup's shrunk,
   baseline-shifted cyan treatment and read as a typo ("11₈12"). Reset it to
   normal size/baseline/colour inline; the oversized cyan ampersand is reserved
   for the name lockup + the monogram only (one deliberate rule). */
html[data-variant="kinetic"] .hero-date .hero-amp {
  font-size: inherit;
  color: inherit;
  vertical-align: baseline;
  display: inline;
  margin: 0;
  padding: 0;
  transform: none;
}
html[data-variant="kinetic"] .hero-date .hero-amp::before,
html[data-variant="kinetic"] .hero-date .hero-amp::after { content: none; }

/* [#8] Kicker/eyebrow sentences sat at ~--label (reads as disabled placeholder).
   Lift the READABLE labels to --muted (~7.8:1); the tiny mono micro-labels
   (TRACK/DRESS/count-label) stay quiet by design, carrying the "instrument"
   register through tracking, not dimness. */
html[data-variant="kinetic"] .kicker,
html[data-variant="kinetic"] .scratch-hint { color: var(--muted); }

/* [#7 — safe part only] Lift the card surfaces so they read as material, not a
   flat wash. (Deliberately NOT desaturating the cyan or adding a serif: obsidian
   + electric cyan + geometric sans is faithful to BOTH named references — the
   techno base and saifullah.dev — so warming it would betray the brief.) */
html[data-variant="kinetic"] .event-card {
  background: linear-gradient(180deg, rgba(26, 30, 39, .72), rgba(13, 15, 20, .55));
  border-color: var(--hairline);
}
html[data-variant="kinetic"] .count-cell {
  background: linear-gradient(180deg, rgba(26, 30, 39, .6), rgba(13, 15, 20, .35));
}

/* [#7 — backdrop] The techno milkdrop viz reads loud + green behind the hero
   type and tips toward "gamer". Dim + de-saturate it for kinetic so the type
   stays dominant and the field reads as quiet obsidian atmosphere, not a
   reactive light wall. (JS drives #milkdrop opacity per frame; a CSS filter
   composes with it without fighting the inline style.) */
html[data-variant="kinetic"] #milkdrop {
  filter: saturate(0.35) hue-rotate(155deg) brightness(0.5) contrast(1.02) blur(16px); /* blur softens upscale artefact into atmosphere */
}

/* [#11] Widow/orphan control on the display quotes ("…with us.", "…coming."). */
html[data-variant="kinetic"] .scratch-prompt,
html[data-variant="kinetic"] .interlude-line,
html[data-variant="kinetic"] .footer-line { text-wrap: balance; }

/* [#12] Pointer-aware enter copy: fine-pointer users click, everyone else taps.
   Template ships both words in spans; show one per input modality. */
html[data-variant="kinetic"] .gate-hint .k-click { display: none; }
@media (hover: hover) and (pointer: fine) {
  html[data-variant="kinetic"] .gate-hint .k-tap { display: none; }
  html[data-variant="kinetic"] .gate-hint .k-click { display: inline; }
}

@media (min-width: 1024px) {
  /* [#6] Desktop countdown read hollow: techno stacks the 4 cells vertically in
     a narrow column (one tiny digit far-left, label far-right). For the kinetic
     HUD a tight 2×2 instrument cluster with centred tiles reads as a real
     counter. Overrides only techno's desktop countdown rules. */
  html[data-variant="kinetic"] .countdown .count-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-auto-rows: 1fr;
    gap: .8rem;
  }
  html[data-variant="kinetic"] .countdown .count-cell {
    flex-direction: column; align-items: center; justify-content: center;
    padding: 1.15rem .8rem; gap: .5rem;
  }

  /* [#5] The section index clipped at the left viewport edge on the interlude
     (its desktop grid zeroes wrap padding). The fixed HUD marker already labels
     this section, so drop the redundant clipped index here. */
  html[data-variant="kinetic"] .interlude .k-index { display: none; }
}

/* ═══════════════════════════════════════════════════════════════════
   SIDE WIREFRAME DANCER (#k-dancer-canvas) — RETIRED
   The two featured "hero" dancers used to render on this narrow right-side
   strip via their own WebGL context. They now render into the full-viewport
   #k-ambient-dancers canvas alongside the ambient crowd (one context total —
   see js/app/kinetic-dancer.js). This element is kept in the DOM but rendered
   to nothing; hidden here so the many per-panel opacity/transform rules below
   that still reference it are simply dead (harmless) selectors. Everything
   #k-dancer-canvas is now display:none — DO NOT rely on it for layout.
   ═══════════════════════════════════════════════════════════════════ */
#k-dancer-canvas {
  display: none;            /* retired — featured pair moved to #k-ambient-dancers */
  position: fixed;
  top: 0; right: 0;
  width: min(38vw, 560px); height: 100svh;
  z-index: 1;               /* above backdrop/vignette (0–1), below panels (2) */
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.1s var(--k-ease);
}
#k-dancer-canvas.is-live { opacity: 1; }
@media (prefers-reduced-motion: reduce) { #k-dancer-canvas { display: none; } }

/* ── AMBIENT armadrillo crowd (#k-ambient-dancers) ──
   Full-viewport canvas: lone armadrillo dancers fade in at random spots across
   the whole window, groove, and fade out (js/app/kinetic-dancer.js). Its own
   renderer/scene/camera so it never disturbs the duet's narrow strip above.
   Sits in the SAME z-band as the duet (above backdrop, below panels) and comes
   FIRST in the DOM so the duet paints over it. Tablet + desktop only: hidden
   below 768px and under reduced-motion (JS also never inits the crowd there),
   so phones stay a clean duet-only page. */
#k-ambient-dancers {
  position: fixed;
  inset: 0;
  width: 100vw; height: 100svh;
  z-index: 1;               /* same band as #k-dancer-canvas: above backdrop, below panels */
  pointer-events: none;     /* page interaction must still work through it */
  opacity: 0;
  transition: opacity 1.4s var(--k-ease);
}
#k-ambient-dancers.is-live { opacity: .5; }
@media (prefers-reduced-motion: reduce) { #k-ambient-dancers { display: none; } }
@media (max-width: 767.98px) { #k-ambient-dancers { display: none; } }

/* ═══ PRESENTER TAKEOVER (html.hud-hidden) ═══════════════════════════
   The giant "presenter" armadrillo (js/app/kinetic-dancer.js) claims the
   stage twice: a WELCOME right after the gate opens, and on every music
   DROP. While it performs, the console furniture AND the #hero header — HUD,
   side menu, wizard controls, right dock, crosshair cursor, hero — fade OUT
   together so ONLY the giant is on stage, then fade back IN as it leaves so the
   HUD and #hero reappear (the welcome giant leaving is what first reveals the
   hero). Toggled
   by a single root class the dancer module adds/removes on the show/hide
   edge. The dancer CANVASES (#k-ambient-dancers, #k-dancer-canvas), #milkdrop,
   #lightshow, .k-grain and the panel/act content are deliberately NOT hidden —
   only the chrome. Reduced-motion / no-GSAP never add this class (that module
   bails under REDUCED), so the HUD never vanishes for those users. Console
   --k-ease glide, both directions. */
#k-hud, #k-panel-menu, #k-panel-controls, #right-dock, #k-cursor, #hero {
  transition: opacity var(--k-ease-dur) var(--k-ease);
}
html.hud-hidden #k-hud,
html.hud-hidden #k-panel-menu,
html.hud-hidden #k-panel-controls,
html.hud-hidden #right-dock,
html.hud-hidden #k-cursor,
html.hud-hidden #hero {
  opacity: 0 !important;   /* !important beats #k-cursor's inline opacity (set by kinetic.js) */
  pointer-events: none;
}
/* Let the presenter read at full strength while it holds the stage (the
   ambient canvas otherwise paints at .5). Reverts when the class is dropped. */
html.hud-hidden #k-ambient-dancers.is-live { opacity: 1; }

/* HUD idle auto-hide (html.hud-idle, toggled by kinetic.js after IDLE_MS of no
   pointer/key activity, cleared on the next move). Hides ONLY the console chrome
   — NOT #hero (unlike the presenter takeover), so an idle viewer still sees the
   title over the dance scene. Same --k-ease glide as above (shared transition
   rule). Deck/full-motion only; reduced-motion never adds the class. */
/* #right-dock (music player) is deliberately NOT idle-hidden: on phones there
   is no hover, so a faded-out player read as "the music selector is gone". */
html.hud-idle #k-hud,
html.hud-idle #k-panel-menu,
html.hud-idle #k-panel-controls,
html.hud-idle #k-cursor {
  opacity: 0 !important;
  pointer-events: none;
}

/* ═══════════════════════════════════════════════════════════════════
   CONTROL PANEL / SETUP-WIZARD (html.k-deck — added by kinetic.js in the
   full-motion path only; reduced-motion / no-JS keep a normal scrolling
   page). The page never scrolls and has NO scroll effect: each act is a
   fixed full-viewport panel; navigation is a labelled side menu + Back/Next
   buttons + a step counter (no wheel / swipe / arrow keys). Panels crossfade.
   ═══════════════════════════════════════════════════════════════════ */
html.k-deck, html.k-deck body {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}
/* the ScrollSmoother wrapper is inert here — acts are positioned directly */
html.k-deck #smooth-wrapper,
html.k-deck #smooth-content { transform: none !important; height: auto; }

/* every act is a full-viewport layer; only .act-current takes pointer input.
   Tall acts (e.g. events on a small phone) may exceed the viewport, so allow
   internal scroll as a safety — kinetic.js only advances the deck once the act
   is scrolled to its edge. */
html.k-deck .hero,
html.k-deck .band,
html.k-deck .footer {
  position: fixed; inset: 0;
  min-height: 0; height: 100svh; margin: 0;
  /* a control panel does NOT scroll — every panel is a fixed viewport with no
     internal scroll (was overflow-y:auto, which let the hero scroll). All panels
     (incl. the content-heavy run-of-show) are sized to fit the viewport. */
  overflow: hidden;
  pointer-events: none;
  z-index: 1;
  justify-content: safe center;
}
html.k-deck .hero::-webkit-scrollbar,
html.k-deck .band::-webkit-scrollbar,
html.k-deck .footer::-webkit-scrollbar { width: 0; height: 0; }
html.k-deck .act-current { pointer-events: auto; z-index: 2; }

/* scroll affordances are meaningless in a control panel */
html.k-deck #floating-cue,
html.k-deck .scroll-cue { display: none; }
/* the per-section index (e.g. "02 / 06") is superseded by the wizard's menu +
   step counter — hide it in panel mode so numbering isn't shown three ways */
html.k-deck .k-index { display: none; }
/* chrome only exists in panel mode */
html:not(.k-deck) #k-panel-menu,
html:not(.k-deck) #k-panel-controls { display: none; }

/* ── Music player: prev / play-pause / next in one console pill, with a mono
   "NN / NN" track readout above it (kinetic only; the main site keeps the bare
   play/pause FAB). The three buttons live inside a single hairline pill so the
   cluster reads as one instrument, not three loose FABs. ── */
#music-dock { gap: .3rem; }
#music-player {
  display: flex; align-items: center; gap: .25rem;
  padding: .26rem .3rem;
  background: rgba(13, 15, 20, .92);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  box-shadow: 0 8px 24px -10px rgba(0, 0, 0, .9);
}
/* the toggle is a chromed FAB in techno.css — reskin it as the DOMINANT centre
   of the transport: larger than the skips, accent-tinted disc so the primary
   action outweighs the two quiet flankers (hierarchy fix) */
#music-player #music-toggle {
  width: 46px; height: 46px; border-radius: 50%;
  color: var(--accent);
  background: rgba(var(--accent-rgb), .12);
  border: 1px solid rgba(var(--accent-rgb), .5);
  box-shadow: none;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  transition: background .3s var(--k-ease), border-color .3s var(--k-ease);
}
#music-player #music-toggle:hover { background: rgba(var(--accent-rgb), .2); }
/* play/pause transport glyph: aria-pressed="true" ⇒ audio playing ⇒ show the
   PAUSE bars (the action); paused ⇒ show the PLAY triangle. Driven by
   setPlaying() in ui.js. */
#music-toggle .icon-pause { display: none; }
#music-toggle[aria-pressed="true"] .icon-play { display: none; }
#music-toggle[aria-pressed="true"] .icon-pause { display: block; }
.k-mskip {
  width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
  color: var(--muted);
  transition: color .3s var(--k-ease), transform .18s var(--k-ease);
}
.k-mskip:hover { color: var(--accent); }
.k-mskip:active { transform: scale(.88); }
.k-mskip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.k-track {
  align-self: center;                 /* centre the readout over the pill */
  font-family: var(--font-mono);
  font-size: .56rem; letter-spacing: .24em;
  color: var(--label); font-variant-numeric: tabular-nums;
  padding: 0 .3rem; min-height: 1em; text-align: center;
  text-transform: uppercase;
  text-shadow: 0 1px 6px rgba(0, 0, 0, .9);  /* legible over any bright frame */
}
.k-track::before {
  content: "TRK "; opacity: .55; letter-spacing: .18em;
}
/* touch: real 44px targets for the skip buttons (icon stays small via grid) */
@media (max-width: 720px) {
  .k-mskip { width: 44px; height: 44px; }
  #music-player #music-toggle { width: 48px; height: 48px; }
}

/* ── side menu: jump to any panel (left rail, mono labels) ── */
#k-panel-menu {
  position: fixed; left: clamp(1rem, 2.5vw, 2.2rem); top: 50%;
  transform: translateY(-50%);
  z-index: 57;
  display: flex; flex-direction: column; gap: .5rem;
  font-family: var(--font-mono);
}
.k-menu-item {
  display: flex; align-items: center; gap: .55rem;
  background: none; border: 0; padding: .18rem 0; margin: 0;
  cursor: pointer; text-align: left; color: var(--label);
  font-family: var(--font-mono); font-size: .62rem;
  letter-spacing: .2em; text-transform: uppercase; line-height: 1;
  transition: color .3s var(--k-ease);
}
/* a leading tick that lights on the active panel (console list marker) */
.k-menu-item::before {
  content: ""; width: 0; height: 1px; background: var(--accent);
  transition: width .3s var(--k-ease); flex: none;
}
.k-menu-idx { font-size: .5rem; opacity: .55; letter-spacing: .1em; }
.k-menu-item:hover { color: var(--muted); }
.k-menu-item.is-current { color: var(--accent); }
.k-menu-item.is-current::before { width: 16px; }
.k-menu-item.is-current .k-menu-idx { opacity: 1; color: var(--accent); }
.k-menu-item.is-current .k-menu-label { text-shadow: 0 0 8px rgba(var(--accent-rgb), .45); }

/* ── Back / Next + step counter (bottom-right console controls) ── */
#k-panel-controls {
  position: fixed; right: clamp(1rem, 3vw, 2.4rem); bottom: clamp(1rem, 3vw, 2rem);
  z-index: 57;
  display: flex; align-items: center; gap: 1rem;
  font-family: var(--font-mono);
}
.k-wz-btn {
  background: rgba(20, 22, 28, .6);
  border: 1px solid var(--hairline); color: var(--text);
  font-family: var(--font-mono); font-size: .62rem;
  letter-spacing: .18em; text-transform: uppercase;
  padding: .6rem 1.05rem; cursor: pointer;
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  transition: border-color .3s var(--k-ease), color .3s var(--k-ease), opacity .3s var(--k-ease);
}
.k-wz-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.k-wz-btn:disabled { opacity: .28; cursor: not-allowed; }
.k-step {
  font-size: .6rem; letter-spacing: .22em; color: var(--muted);
  font-variant-numeric: tabular-nums;
  background: none; border: 0; padding: 0; cursor: default;
}
/* the jump-sheet label is a mobile-only affordance (the desktop rail is
   already visible) — hidden here, revealed at ≤720px below */
.k-step-label { display: none; }

/* mobile: the labelled rail crowds a narrow frame — hide it and give a compact
   bottom control bar (Back / Next + counter). The panel's own heading + HUD
   still name the section. */
@media (max-width: 720px) {
  #k-panel-menu { display: none; }
  #k-panel-controls {
    left: 0; right: 0; bottom: 0; justify-content: center; gap: .9rem;
    padding: .7rem 1rem calc(.7rem + env(safe-area-inset-bottom, 0px));
    background: linear-gradient(0deg, rgba(6, 7, 10, .92), rgba(6, 7, 10, 0));
  }
}
@media (prefers-reduced-motion: reduce) {
  .k-menu-item, .k-menu-item::before, .k-wz-btn { transition: none; }
}

/* ═══════════════════════════════════════════════════════════════════
   ART-DIRECTION REDESIGN (per-panel composition pass)
   Golden split: content lives in the LEFT ~60% column; the wireframe dancer
   owns the right ~38% (dimmed + scrimmed so content always wins). Each panel
   gets a distinct anchor. Borderless HUD language (hairline label/value rows)
   replaces filled cards. Scoped to the kinetic control panel (html.k-deck).
   ═══════════════════════════════════════════════════════════════════ */

/* ── Type: exploit the wide-mono / tight-display contrast ── */
html[data-variant="kinetic"] .hero-names,
html[data-variant="kinetic"] .display-head,
html[data-variant="kinetic"] .script-head { letter-spacing: -0.025em; line-height: 1.04; }
html[data-variant="kinetic"] .kicker,
html[data-variant="kinetic"] .k-eyebrow { letter-spacing: .16em; }

/* ── Dancer: drop to atmosphere + obsidian scrim on its left edge ── */
#k-dancer-canvas.is-live { opacity: .55; }
/* RETIRED with #k-dancer-canvas: this scrim darkened the LEFT + RIGHT edges of
   the viewport to back the old right-side dancer strip. With the featured pair
   now spread ACROSS the full viewport, that side-darkening would mask them into
   a central band — so the scrim is fully disabled. Kept in the DOM; the desktop
   background overrides further down are dead (harmless) selectors. */
#k-dancer-scrim {
  display: none !important;
  position: fixed; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(90deg, var(--bg) 0%, var(--bg) 38%, rgba(11,12,15,0) 80%);
}
html:not(.k-deck) #k-dancer-scrim { display: none; }
@media (max-width: 899px) { #k-dancer-scrim { display: none; } }
/* per-panel dancer treatment */
html.k-deck[data-panel="interlude"] #k-dancer-canvas.is-live { opacity: .14; }
html.k-deck[data-panel="archive"]   #k-dancer-canvas.is-live { opacity: .42; }
html.k-deck[data-panel="rsvp"]      #k-dancer-canvas.is-live { opacity: .32; }

/* ── Golden-split left column (≥900; the dancer is hidden below that) ── */
@media (min-width: 900px) {
  html.k-deck .hero,
  html.k-deck .band,
  html.k-deck .footer {
    align-items: stretch;                          /* .wrap controls its own width */
    padding-left: clamp(4rem, 11vw, 12rem);        /* clear the left menu */
    padding-right: min(40vw, 600px);               /* reserve the figure's column */
  }
  html.k-deck .wrap,
  html.k-deck .wrap--narrow {
    max-width: 52rem; width: 100%; margin: 0; text-align: left;
  }
  /* menu legibility over any bright panel (interlude painting) */
  .k-menu-item { text-shadow: 0 1px 8px rgba(0,0,0,.85); }

  /* INTERLUDE keeps its portrait|quote split, so it opts OUT of the reserve
     and the dancer nearly vanishes there. */
  html.k-deck[data-panel="interlude"] .interlude { padding: 0; }
  html.k-deck[data-panel="interlude"] .interlude > .wrap--narrow {
    max-width: none; text-align: left;
  }

  /* ARCHIVE anchors low-left; RSVP sits a touch lower for gravity. */
  html.k-deck[data-panel="archive"] .gallery { justify-content: safe flex-end; padding-bottom: clamp(4rem,10vh,8rem); }
}

/* ── Archive gallery: duotone + RGB-split (chromatic aberration) console
   accent ── #k-chroma/#k-chroma-hover (defined once in the kinetic template)
   now do the full job — duotone (shadows→#1a1c22, highlights→--accent
   #22d3ee) feeding the RGB split — so the CSS-level saturate/contrast this
   used to lean on is gone; it would just refight the SVG filter's own
   colour mapping. Ambient split is a 1.1px static offset (cheap, always on,
   still subtle); hover/focus swaps to a legible 2.4px split. Both are
   static filter graphs — no animated primitives — so there's no jitter loop
   for reduced-motion to strip. object-fit:cover fills the frame edge-to-edge
   with zero bleed, so an offset channel has no real pixel to sample at the
   true image edge and clips to a solid colour bar; scaling the img 3%
   oversize gives the filter real content to pull from (the parent
   .gframe's overflow:hidden clips the 3% excess, same as it already clips
   object-fit:cover). */
html[data-variant="kinetic"] .gframe img {
  filter: url(#k-chroma);
  transform: scale(1.03);
  transition: filter .15s ease;
}
html[data-variant="kinetic"] .gframe:hover img,
html[data-variant="kinetic"] .gframe:focus-within img {
  filter: url(#k-chroma-hover);
}
/* Touch has no hover — give it the fuller split as the resting state instead
   of never showing the effect's payoff (fine-pointer devices keep the subtle
   ambient rest state + hover reveal). */
@media (hover: none) and (pointer: coarse) {
  html[data-variant="kinetic"] .gframe img {
    filter: url(#k-chroma-hover);
  }
}

/* ── Borderless HUD: strip filled cards → hairline label/value language ── */
/* Countdown counter tiles → borderless value/label, one row */
html.k-deck .count-cell {
  background: none; border: 0; border-top: 1px solid rgba(var(--accent-rgb), .35);
  -webkit-backdrop-filter: none; backdrop-filter: none;
  padding: .7rem .2rem 0; align-items: flex-start;
}
@media (min-width: 900px) {
  html.k-deck[data-panel="countdown"] .countdown .wrap { display: block; }
  html.k-deck[data-panel="countdown"] .count-grid {
    display: flex; gap: clamp(1rem, 3vw, 2.2rem); grid-template-columns: none; margin: 1.6rem 0 2.4rem;
  }
  html.k-deck[data-panel="countdown"] .count-cell { flex: 1; min-width: 0; }
  html.k-deck[data-panel="countdown"] .scratch { max-width: 34rem; margin: 0; width: 100%; }
}

/* RUN OF SHOW: three floating cards → one borderless tracklist (fixes the
   card-on-dancer collision + the boxing inconsistency; it IS a tracklist). */
html.k-deck[data-panel="run-of-show"] .event-grid {
  display: flex; flex-direction: column; gap: 0; max-width: 46rem;
  grid-template-columns: none;
}
html.k-deck[data-panel="run-of-show"] .event-card {
  display: grid; grid-template-columns: 6.5rem 1fr; column-gap: 1.6rem; align-items: baseline;
  background: none; border: 0; border-top: 1px solid var(--hairline);
  border-radius: 0; box-shadow: none; min-height: 0;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  padding: 1.35rem 0;
}
html.k-deck[data-panel="run-of-show"] .event-card:last-child { border-bottom: 1px solid var(--hairline); }
html.k-deck[data-panel="run-of-show"] .event-no { grid-column: 1; grid-row: 1 / span 6; margin: 0; align-self: start; color: var(--accent); }
html.k-deck[data-panel="run-of-show"] .event-name { grid-column: 2; margin: 0 0 .35rem; font-size: clamp(1.35rem, 2.4vw, 1.7rem); }
html.k-deck[data-panel="run-of-show"] .event-when,
html.k-deck[data-panel="run-of-show"] .event-venue,
html.k-deck[data-panel="run-of-show"] .event-dress { grid-column: 2; margin: .12rem 0; }
html.k-deck[data-panel="run-of-show"] .event-actions { grid-column: 2; margin-top: .7rem; }
/* the headliner: a cyan left tick instead of a filled card */
html.k-deck[data-panel="run-of-show"] .event-card--main { position: relative; padding-left: 1rem; }
html.k-deck[data-panel="run-of-show"] .event-card--main::before {
  content: ""; position: absolute; left: 0; top: 1.35rem; bottom: 1.35rem; width: 2px;
  background: var(--accent); box-shadow: 0 0 8px rgba(var(--accent-rgb), .6);
}

/* ARCHIVE sealed panel → lighter HUD frame (not a filled modal) */
html.k-deck .gframe--veiled {
  background: none; border: 1px solid var(--hairline);
  -webkit-backdrop-filter: none; backdrop-filter: none;
}

/* ── Consistency: left-anchor the panels that techno centres (hero/footer/
      gallery) so the whole deck reads as one left-column system ── */
@media (min-width: 900px) {
  /* Footer + gallery are smaller text → flush-left with the column. The HERO is
     NOT flush-left: its huge names would collide with the left menu, so it stays
     centred within the left column (classic title panel, clears the menu). */
  html.k-deck .footer .wrap--narrow,
  html.k-deck .gallery .wrap {
    align-items: flex-start; text-align: left; margin: 0; max-width: 52rem;
  }
  html.k-deck .footer-invocation { align-items: flex-start; text-align: left; }
  html.k-deck .footer-rule { margin-left: 0; }

  /* Archive: left-anchor the veil frame + a sensible width so it isn't a
     centred modal floating in dead space */
  html.k-deck .gallery-grid { justify-items: start; }
  html.k-deck .gframe--veiled { max-width: 30rem; margin: 0; text-align: left; align-items: flex-start; }

  /* Interlude menu legibility: dark scrim strip behind the left menu over the
     bright painting (WCAG contrast) */
  html.k-deck[data-panel="interlude"] #k-panel-menu {
    padding: .9rem 1.4rem .9rem .4rem;
    background: linear-gradient(90deg, rgba(6,7,10,.9) 0%, rgba(6,7,10,.7) 55%, rgba(6,7,10,0) 100%);
  }
}

/* footer: left-align the invocation crest with the rest of the left-anchored
   RSVP content (techno centres it) */
@media (min-width: 900px) {
  html.k-deck .footer-invocation { align-items: flex-start; }
  html.k-deck .footer-invocation .invocation-motif,
  html.k-deck .footer-monogram { margin-left: 0; }
}

/* ═══════════════════════════════════════════════════════════════════
   9.5 PUNCH-LIST (adversarial review round). Fixes: dancer consistency,
   events fit, contrast, RSVP void, HUD framing of the two boxes, button
   affordance, mobile floor + overflow.
   ═══════════════════════════════════════════════════════════════════ */

/* B2/B3 — the dancer is ONE constant motif: same brightness on every panel,
   shifted right so it never crosses content, additive glow. Overrides the
   earlier per-panel dimming. */
#k-dancer-canvas { mix-blend-mode: screen; transform: translateX(8%); }
#k-dancer-canvas.is-live,
html.k-deck[data-panel="interlude"] #k-dancer-canvas.is-live,
html.k-deck[data-panel="archive"]   #k-dancer-canvas.is-live,
html.k-deck[data-panel="rsvp"]       #k-dancer-canvas.is-live { opacity: .3; }
html.k-deck[data-panel="interlude"] #k-dancer-canvas { transform: translateX(42%); } /* clear the quote */
html.k-deck[data-panel="rsvp"]       #k-dancer-canvas { transform: translateX(24%); }

/* B6 — informational text to AA; brighter inactive nav + body-weight venue text */
html.k-deck .k-menu-item { color: #98a2b1; }
html.k-deck .event-when,
html.k-deck .event-venue { color: var(--body); }
/* P2 — countdown digits: crisp cyan glow (not the beat-driven greenish bloom) */
html.k-deck .count-num { color: var(--text); text-shadow: 0 0 10px rgba(var(--accent-rgb), .4); }

/* P4 — GET DIRECTIONS / ADD TO CALENDAR read as real controls */
html.k-deck .btn-quiet {
  border: 1px solid rgba(var(--accent-rgb), .3);
  padding: .5rem .95rem; border-radius: 2px; text-decoration: none;
  transition: border-color .3s var(--k-ease), color .3s var(--k-ease), background .3s var(--k-ease);
}
html.k-deck .btn-quiet:hover { border-color: var(--accent); color: var(--accent); background: rgba(var(--accent-rgb), .06); }

/* P1 — HUD framing on the scratch card + sealed archive box (corner brackets) */
html.k-deck .scratch-frame,
html.k-deck .gframe--veiled { position: relative; }
html.k-deck .scratch-frame::before,
html.k-deck .gframe--veiled::before {
  content: ""; position: absolute; top: -1px; left: -1px; width: 15px; height: 15px;
  border-top: 1px solid var(--accent); border-left: 1px solid var(--accent); opacity: .65; pointer-events: none;
}
html.k-deck .scratch-frame::after,
html.k-deck .gframe--veiled::after {
  content: ""; position: absolute; bottom: -1px; right: -1px; width: 15px; height: 15px;
  border-bottom: 1px solid var(--accent); border-right: 1px solid var(--accent); opacity: .65; pointer-events: none;
}

@media (min-width: 900px) {
  /* B1 — RUN OF SHOW: tighten so all three tracks + header fit one frame;
     hard bottom clearance so nothing reaches the Back/Next chrome */
  html.k-deck[data-panel="run-of-show"] .events .display-head { font-size: clamp(2.2rem, 3.6vw, 3rem); margin-bottom: 1rem; line-height: 1.02; }
  html.k-deck[data-panel="run-of-show"] .events .kicker { margin-bottom: .5rem; }
  html.k-deck[data-panel="run-of-show"] .event-card { padding: .95rem 0; }
  html.k-deck[data-panel="run-of-show"] .event-name { font-size: 1.3rem; margin-bottom: .15rem; }
  html.k-deck[data-panel="run-of-show"] .event-when,
  html.k-deck[data-panel="run-of-show"] .event-venue,
  html.k-deck[data-panel="run-of-show"] .event-dress { margin: .04rem 0; font-size: .82rem; }
  html.k-deck[data-panel="run-of-show"] .event-actions { margin-top: .5rem; }
  html.k-deck[data-panel="run-of-show"] .events { padding-bottom: 4rem; }

  /* P3 — interlude: vertically centre the quote; soften the menu scrim to a
     gradient (not a visible box) */
  html.k-deck[data-panel="interlude"] .interlude > .wrap--narrow { align-items: center; }
  html.k-deck[data-panel="interlude"] #k-panel-menu {
    background: linear-gradient(90deg, rgba(6,7,10,.92) 0%, rgba(6,7,10,.55) 60%, rgba(6,7,10,0) 100%);
    padding: 1rem 3.2rem 1rem .4rem;
  }

  /* B5 — RSVP: vertically centre content, scale the closing line, so the panel
     isn't 65% void (the dancer, now constant, fills the right) */
  html.k-deck[data-panel="rsvp"] .footer { justify-content: safe center; }
  html.k-deck .footer-line { font-size: clamp(1.1rem, 1.7vw, 1.45rem); max-width: 34rem; line-height: 1.35; }
  html.k-deck .footer-sign span { font-size: clamp(1.3rem, 2.2vw, 1.8rem); }
}

/* B2 (root cause) — panel backgrounds were veiling the dancer inconsistently
   (band--tint + footer have their own fills). Make every panel transparent in
   deck mode so the dancer reads at the SAME brightness through all of them. */
html.k-deck .band--tint,
html.k-deck .footer { background: transparent; }
/* RSVP: truly centre the closing block (not top-anchored) */
@media (min-width: 900px) {
  html.k-deck[data-panel="rsvp"] .footer { justify-content: center; }
}

/* B5 — centre the RSVP block vertically in the flex-column footer (auto margins) */
@media (min-width: 900px) {
  html.k-deck[data-panel="rsvp"] .footer .wrap--narrow { margin-top: auto; margin-bottom: auto; }
}

/* B4 — the coord readout was pinned top-left (never followed the cursor) and
   piled onto the wordmark. It references nothing meaningful → cut it; the
   crosshair lines + ring are the cursor. */
#k-cursor .k-cursor-read { display: none; }

/* ═══════ 9.5 PUNCH-LIST — round 2 ═══════ */

/* B3/consistency — ONE dancer everywhere: same flat opacity, same anchor, no
   mix-blend (screen made it ride the backdrop → inconsistent). Removes the
   extreme interlude/rsvp right-shifts that fragmented / hid the figure. */
#k-dancer-canvas { mix-blend-mode: normal; transform: translateX(8%); }
html.k-deck[data-panel="interlude"] #k-dancer-canvas,
html.k-deck[data-panel="archive"]   #k-dancer-canvas,
html.k-deck[data-panel="rsvp"]       #k-dancer-canvas { transform: translateX(8%); }
#k-dancer-canvas.is-live,
html.k-deck[data-panel="interlude"] #k-dancer-canvas.is-live,
html.k-deck[data-panel="archive"]   #k-dancer-canvas.is-live,
html.k-deck[data-panel="rsvp"]       #k-dancer-canvas.is-live { opacity: .34; }

/* Interlude: the quote sits over the dancer now — keep the quote plate legible
   with a soft backing, and feather the menu scrim (was a hard rectangle). */
@media (min-width: 900px) {
  html.k-deck[data-panel="interlude"] #k-panel-menu {
    background: none; padding: 1rem 3.2rem 1rem .4rem;
  }
  html.k-deck[data-panel="interlude"] #k-panel-menu::before {
    content: ""; position: absolute; inset: -2rem -5rem -2rem -3rem; z-index: -1;
    background: radial-gradient(ellipse at left center, rgba(6,7,10,.92) 0%, rgba(6,7,10,.55) 45%, rgba(6,7,10,0) 80%);
  }
}

/* B5 — footer: with the brighter uniform dancer, nudge content toward centre so
   the panel isn't hard-left + right-void */
@media (min-width: 900px) {
  html.k-deck[data-panel="rsvp"] .footer { padding-left: clamp(6rem, 16vw, 18rem); }
}

/* mobile archive: centre the SEALED cluster inside its box */
html.k-deck .gframe--veiled { justify-content: center; }

/* B1 — mobile RUN OF SHOW: real clearance (nav ~5rem + FAB) + move the music
   FAB to the TOP-right on mobile so it never overlaps the tracklist/nav */
@media (max-width: 720px) {
  html.k-deck[data-panel="run-of-show"] .events { padding-bottom: 9.5rem; }
  html.k-deck #right-dock { top: 3.6rem; bottom: auto; right: 1rem; }
}

/* Desktop: dock the music player TOP-LEFT (under the HUD brand wordmark),
   left-aligned, instead of the default bottom-right corner. */
@media (min-width: 721px) {
  html.k-deck #right-dock {
    left: 2.4rem; right: auto; top: 2.7rem; bottom: auto;
    align-items: flex-start;
  }
  html.k-deck #music-dock { align-items: flex-start; }
  html.k-deck .k-track { align-self: flex-start; text-align: left; }
}

/* ═══════ 9.5 PUNCH-LIST — round 2b: mobile events fit ═══════ */
@media (max-width: 720px) {
  /* compact the tracklist so all three fit without fighting the nav; FAB goes to
     the empty bottom-right corner of the nav bar (content clears it) */
  html.k-deck #right-dock { top: auto; bottom: 1.15rem; right: .9rem; }
  html.k-deck[data-panel="run-of-show"] .events .display-head { font-size: 1.9rem; margin-bottom: .5rem; line-height: 1.05; }
  html.k-deck[data-panel="run-of-show"] .events .kicker { font-size: .58rem; margin-bottom: .3rem; }
  html.k-deck[data-panel="run-of-show"] .event-card { grid-template-columns: 1fr; padding: .7rem 0; }
  html.k-deck[data-panel="run-of-show"] .event-no { grid-row: auto; margin-bottom: .2rem; }
  html.k-deck[data-panel="run-of-show"] .event-name { font-size: 1.1rem; margin-bottom: .1rem; }
  html.k-deck[data-panel="run-of-show"] .event-when,
  html.k-deck[data-panel="run-of-show"] .event-venue,
  html.k-deck[data-panel="run-of-show"] .event-dress { font-size: .72rem; margin: .02rem 0; }
  html.k-deck[data-panel="run-of-show"] .event-dress span { display: inline; }
  html.k-deck[data-panel="run-of-show"] .event-actions { display: flex; flex-direction: row; gap: .5rem; margin-top: .4rem; }
  html.k-deck[data-panel="run-of-show"] .btn-quiet { flex: 1; padding: .45rem .3rem; font-size: .56rem; text-align: center; }
  html.k-deck[data-panel="run-of-show"] .events { padding-bottom: 5.5rem; }
}

/* ═══════ 9.5 PUNCH-LIST — round 3 ═══════ */

/* #1/#3 — dancer brighter so it holds its column (incl. footer/rsvp) at every pose */
#k-dancer-canvas.is-live,
html.k-deck[data-panel="interlude"] #k-dancer-canvas.is-live,
html.k-deck[data-panel="archive"]   #k-dancer-canvas.is-live,
html.k-deck[data-panel="rsvp"]       #k-dancer-canvas.is-live { opacity: .44; }

/* #2 — interlude: a full-height left-gutter scrim so BOTH the top wordmark and
   all six nav rows stay legible over the bright painting (not just the menu) */
@media (min-width: 900px) {
  html.k-deck[data-panel="interlude"]::before {
    content: ""; position: fixed; left: 0; top: 0; bottom: 0; width: min(32vw, 380px);
    z-index: 56; pointer-events: none;
    background: linear-gradient(90deg, rgba(6,7,10,.96) 0%, rgba(6,7,10,.82) 40%, rgba(6,7,10,.4) 70%, rgba(6,7,10,0) 100%);
  }
  html.k-deck[data-panel="interlude"] #k-panel-menu::before { display: none; }
}

/* #6 — the crosshair is a fine-pointer desktop device; hide it on narrow
   viewports too (kills the centre gridline through NEXT on the mobile shots;
   real touch devices already hide it) */
@media (max-width: 900px) { #k-cursor { display: none; } }

/* #4 — mobile track labels: keep "TRACK 0X" on one line with a clean gap, no
   dangling middot on the headliner */
@media (max-width: 720px) {
  html.k-deck[data-panel="run-of-show"] .event-no { white-space: nowrap; margin-bottom: .35rem; line-height: 1.2; }
}

/* #5 — gallery: contain the grid (veil OR revealed photos) so the heading stays
   in view and nothing hard-clips; the grid scrolls within, panel doesn't */
@media (min-width: 900px) {
  html.k-deck .gallery .wrap { max-width: 60rem; }
  html.k-deck .gallery-grid {
    max-height: 60vh; overflow-y: auto; overflow-x: hidden; scrollbar-width: none;
    padding-right: .5rem; -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 4%, #000 96%, transparent 100%);
            mask-image: linear-gradient(180deg, transparent 0, #000 4%, #000 96%, transparent 100%);
  }
  html.k-deck .gallery-grid::-webkit-scrollbar { width: 0; }
}
@media (max-width: 899px) {
  html.k-deck .gallery-grid { max-height: 62vh; overflow-y: auto; scrollbar-width: none;
    -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 5%, #000 95%, transparent 100%);
            mask-image: linear-gradient(180deg, transparent 0, #000 5%, #000 95%, transparent 100%); }
  html.k-deck .gallery-grid::-webkit-scrollbar { width: 0; }
}

/* ═══════ 9.5 PUNCH-LIST — round 4 ═══════ */

/* #2/#3 — dancer: brighter so it reads pure cyan over any backdrop (kills the
   footer olive cast + faintness); milkdrop backdrop dimmed further so nothing
   green shows between the wireframe lines */
#k-dancer-canvas.is-live,
html.k-deck[data-panel="interlude"] #k-dancer-canvas.is-live,
html.k-deck[data-panel="archive"]   #k-dancer-canvas.is-live,
html.k-deck[data-panel="rsvp"]       #k-dancer-canvas.is-live { opacity: .5; }
/* blur softens artefact; screen preserves blacks */
html[data-variant="kinetic"] #milkdrop {
  filter: saturate(0.28) hue-rotate(160deg) brightness(0.32) contrast(1.02) blur(16px);
  mix-blend-mode: screen;
}

/* #4 — interlude: nudge the figure right so its forearm clears "BETWEEN THE SETS" */
html.k-deck[data-panel="interlude"] #k-dancer-canvas { transform: translateX(15%); }

/* #1 — mobile RUN OF SHOW: stack TRACK label above the title with a real gap
   (display:block guarantees it), and keep both action buttons on one line each */
@media (max-width: 720px) {
  html.k-deck[data-panel="run-of-show"] .event-card { display: block; }
  html.k-deck[data-panel="run-of-show"] .event-no { display: block; margin: 0 0 .4rem; white-space: nowrap; }
  html.k-deck[data-panel="run-of-show"] .event-name { display: block; margin: 0 0 .35rem; }
  html.k-deck[data-panel="run-of-show"] .event-actions { display: flex; gap: .5rem; }
  html.k-deck[data-panel="run-of-show"] .btn-quiet { flex: 1 1 0; min-width: 0; white-space: nowrap; font-size: .54rem; padding: .5rem .2rem; }
}

/* ═══════ 9.5 PUNCH-LIST — round 5 ═══════ */

/* #1 — dancer brighter (footer no longer reads dim) + uniform cyan everywhere */
#k-dancer-canvas.is-live,
html.k-deck[data-panel="interlude"] #k-dancer-canvas.is-live,
html.k-deck[data-panel="archive"]   #k-dancer-canvas.is-live,
html.k-deck[data-panel="rsvp"]       #k-dancer-canvas.is-live { opacity: .62; }

/* #2 — mobile RUN OF SHOW: set the date/venue VALUE lines in the sans display
   face (mono was orphan-wrapping "…in the / morning"); mono stays on the labels */
@media (max-width: 720px) {
  html.k-deck[data-panel="run-of-show"] .event-when,
  html.k-deck[data-panel="run-of-show"] .event-venue,
  html.k-deck[data-panel="run-of-show"] .event-dress { font-family: var(--font-display); letter-spacing: 0; text-wrap: pretty; }
  html.k-deck[data-panel="run-of-show"] .event-dress span { font-family: var(--font-mono); }
}

/* #3 — archive: the sealed frame drew unevenly (corner brackets made it read
   2-sided). Drop the brackets on the veil and close all four sides uniformly. */
html.k-deck .gframe--veiled::before,
html.k-deck .gframe--veiled::after { display: none; }
html.k-deck .gframe--veiled { border: 1px solid rgba(var(--accent-rgb), .22); }

/* ═══════ 9.5 PUNCH-LIST — round 6 (final) ═══════ */

/* #1 — dancer crisp + UNIFORM everywhere: lift it above the film-grain (was
   softening it) and give it a consistent dark backing on the right (behind the
   figure) so the backdrop never bleeds blue/haze between the wireframe. The
   backing is BELOW the figure, so it dims the backdrop, not the dancer. */
#k-dancer-canvas { z-index: 2; }                 /* above .k-grain (2, earlier in DOM) → crisp */
#k-dancer-canvas.is-live,
html.k-deck[data-panel="interlude"] #k-dancer-canvas.is-live,
html.k-deck[data-panel="archive"]   #k-dancer-canvas.is-live,
html.k-deck[data-panel="rsvp"]       #k-dancer-canvas.is-live { opacity: .66; }
#k-dancer-scrim {
  background: linear-gradient(90deg,
    var(--bg) 0%, var(--bg) 34%, rgba(11,12,15,0) 50%,
    rgba(11,12,15,0) 60%, rgba(6,7,10,.5) 80%, rgba(6,7,10,.5) 100%);
}
/* #3 — interlude: uniform anchor so the wide hand clears the right hairline */
html.k-deck[data-panel="interlude"] #k-dancer-canvas { transform: translateX(8%); }

/* #4 — gallery mask/overflow applies ONLY to the revealed photo grid; the single
   veil box must keep its full 4-side border (the mask was fading its top/bottom) */
@media (min-width: 900px) {
  html.k-deck .gallery-grid--veiled { max-height: none; overflow: visible;
    -webkit-mask-image: none; mask-image: none; }
}
@media (max-width: 899px) {
  html.k-deck .gallery-grid--veiled { max-height: none; overflow: visible;
    -webkit-mask-image: none; mask-image: none; }
}
html.k-deck .gframe--veiled { border: 1px solid rgba(var(--accent-rgb), .22); }

/* #2 — mobile archive: compact the sealed box + brighten the medallion so it
   doesn't collapse to a smudge or leave a hollow void */
@media (max-width: 899px) {
  html.k-deck .gframe--veiled { padding: 2rem 1.5rem; gap: 1rem; }
  html.k-deck .veil-seal { opacity: 1; color: var(--accent); }
}

/* ═══════ 9.5 PUNCH-LIST — round 7 ═══════ */
/* #1 — back the FULL dancer column with dark (not just the far edge) so the
   dense wireframe reads crisp + uniform on every panel incl. footer, where the
   backdrop haze was washing it soft. Middle stays transparent for atmosphere. */
#k-dancer-scrim {
  background: linear-gradient(90deg,
    var(--bg) 0%, var(--bg) 32%, rgba(11,12,15,0) 44%,
    rgba(11,12,15,0) 52%, rgba(6,7,10,.5) 60%, rgba(6,7,10,.62) 100%);
}

/* #1 (final) — near-opaque backing behind the dancer so the light-show glow can
   never bleed through and bloom the wireframe soft (the footer defect). Middle
   stays transparent for atmosphere behind the content. */
#k-dancer-scrim {
  background: linear-gradient(90deg,
    var(--bg) 0%, var(--bg) 30%, rgba(11,12,15,0) 44%, rgba(11,12,15,0) 50%,
    rgba(6,7,10,.6) 58%, rgba(6,7,10,.9) 70%, rgba(6,7,10,.92) 100%);
}

/* explicit belt-and-suspenders: the hero panel itself never scrolls/overflows */
html[data-variant="kinetic"] #hero { overflow: hidden; }

/* ═══════════════════════════════════════════════════════════════════
   MOBILE MAKEOVER (whole-UI review pass — Fable + Sonnet + Opus panel).
   Supersedes the punch-list rounds above for mobile: the dancer gets a
   RESERVED stage that content never enters (instead of sharing a band and
   occasionally crossing buttons/text); Back/Next + event actions grow to
   real touch targets; body copy clears the dark-UI legibility floor; the
   hidden side menu comes back as a tappable bottom sheet; panels regain
   internal scroll so short phones never clip content.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  /* ── Dancer stage RETIRED on phones ──
     The whole dancer scene (featured pair + ambient crowd) is now a single
     full-viewport canvas gated to tablet + desktop only (≥768px; #k-ambient-
     dancers is display:none below that and JS never inits it). So phones show a
     clean, dancer-free page: NO reserved bottom stage, NO stage padding — panel
     content only needs to clear the fixed bottom nav bar (4.85rem + safe-area).
     (The old #k-dancer-canvas bottom-stage + scrim rules here are removed; the
     element itself is globally display:none.) */
  html.k-deck .hero, html.k-deck .band, html.k-deck .footer {
    padding-bottom: calc(4.85rem + env(safe-area-inset-bottom, 0px));
  }

  /* ── Music FAB: out of the Next / iOS home-indicator collision zone ── */
  html.k-deck #right-dock {
    top: calc(env(safe-area-inset-top, 0px) + .75rem);
    bottom: auto; right: .75rem; left: auto;
  }
}

@media (max-width: 720px) {
  /* ── Back/Next + step readout: real 48px touch targets (WCAG 2.5.5) ── */
  #k-panel-controls {
    box-sizing: border-box;
    min-height: calc(4.85rem + env(safe-area-inset-bottom, 0px));
    padding: .7rem 1rem calc(.7rem + env(safe-area-inset-bottom, 0px));
    background: rgba(6, 7, 10, .92);
    -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
    border-top: 1px solid var(--hairline);
  }
  .k-wz-btn {
    min-height: 48px; min-width: 44px;
    font-size: .72rem; padding: .7rem 1.15rem;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .k-wz-btn:disabled { opacity: .45; }   /* was .28 — stays legible against obsidian */

  /* the step counter is now a tappable jump control (opens the sheet below) */
  .k-step {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: .1rem; min-height: 48px; padding: 0 .6rem;
    background: none; border: 0; cursor: pointer;
  }
  .k-step-label {
    display: block; font-size: .66rem; letter-spacing: .1em;
    text-transform: uppercase; color: var(--accent);
    max-width: 30vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .k-step-count { font-size: .58rem; letter-spacing: .08em; }

  /* ── Jump sheet: the hidden side menu, repurposed as a bottom sheet ── */
  #k-panel-menu {
    display: flex; position: fixed; left: 0; right: 0; top: auto; bottom: 0;
    flex-direction: column; gap: 0; transform: translateY(100%); pointer-events: none;
    background: rgba(6, 7, 10, .97);
    border-top: 1px solid rgba(var(--accent-rgb), .28);
    border-radius: 16px 16px 0 0;
    padding: .5rem 0 calc(.5rem + env(safe-area-inset-bottom, 0px));
    max-height: 70svh; overflow-y: auto;
    z-index: 59;                                 /* above the sheet backdrop + dock */
    transition: transform .35s var(--k-ease);
  }
  #k-panel-menu.k-sheet-open { transform: translateY(0); pointer-events: auto; }
  .k-menu-item {
    padding: .95rem 1.4rem; font-size: .78rem; letter-spacing: .1em;
    border-bottom: 1px solid var(--hairline); min-height: 48px;
  }
  .k-menu-item:last-child { border-bottom: 0; }
  .k-menu-item::before { display: none; }        /* the active row's own colour reads fine here */
  #k-sheet-backdrop {
    position: fixed; inset: 0; z-index: 58; background: rgba(6, 7, 10, .6);
    opacity: 0; pointer-events: none; transition: opacity .35s var(--k-ease);
  }
  #k-sheet-backdrop.k-sheet-open { opacity: 1; pointer-events: auto; }

  /* ── Typography floor: Run of Show is the highest-value copy on the site ── */
  html.k-deck[data-panel="run-of-show"] .event-no { font-size: .68rem; font-weight: 700; }
  html.k-deck[data-panel="run-of-show"] .event-name { font-size: 1.2rem; }
  html.k-deck[data-panel="run-of-show"] .event-when,
  html.k-deck[data-panel="run-of-show"] .event-venue,
  html.k-deck[data-panel="run-of-show"] .event-dress { font-size: .94rem; }
  html.k-deck[data-panel="run-of-show"] .event-dress span { font-size: .66rem; letter-spacing: .08em; }
  html.k-deck[data-panel="run-of-show"] .events .kicker { font-size: .72rem; }

  /* ── Event action buttons: real touch targets + a visible resting boundary
     (the hairline border only strengthened on :hover, meaningless on touch) ── */
  html.k-deck[data-panel="run-of-show"] .btn-quiet {
    min-height: 46px; font-size: .74rem; padding: .55rem .5rem; letter-spacing: .06em;
    border-color: rgba(var(--accent-rgb), .55);
    background: rgba(var(--accent-rgb), .05);
  }

  /* ── Restore internal scroll on tall panels (hero stays fixed/short by
     design — see the explicit #hero overflow:hidden rule below). Short phones
     and enlarged system fonts can now reach content instead of clipping it. ── */
  html.k-deck .band,
  html.k-deck .footer {
    overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
  }
  html.k-deck .band::-webkit-scrollbar,
  html.k-deck .footer::-webkit-scrollbar { width: 0; height: 0; }
}

@media (prefers-reduced-motion: reduce) {
  #k-panel-menu, #k-sheet-backdrop { transition: none; }
}

/* ═══════════════════════════════════════════════════════════════════
   THEME-8 (Taratata) FULLSCREEN VIDEO TAKEOVER
   Driven by js/app/kinetic-video.js at authored beats. When armed (.is-on),
   the opaque overlay sits ABOVE every other layer (custom cursor z9998, HUD/
   dock z56–59, gate z60) so its black fill occludes the ENTIRE UI by pure
   occlusion — no per-element hiding needed. The muted, audio-stripped video
   inside then fades in on top (opacity driven inline, frame-synced to the
   track). Removing .is-on drops the overlay and the UI returns underneath,
   untouched. A short overlay fade softens the cut to/from black without
   materially shifting the authored hide/return instants.
   ═══════════════════════════════════════════════════════════════════ */
#k-video-takeover {
  position: fixed;
  inset: 0;
  z-index: 10000;            /* above cursor (9998), HUD/dock, gate — occludes all */
  background: #000;
  display: none;             /* not painted until armed */
  opacity: 0;
  pointer-events: none;
  transition: opacity .22s linear;
}
#k-video-takeover.is-on {
  display: block;
  opacity: 1;
  pointer-events: auto;      /* swallow taps on the now-hidden UI beneath */
}
/* Belt-and-braces: while the takeover is armed, HARD-HIDE every top-level layer
   except the overlay itself — milkdrop, lightshow, vignette, grain, both dancer
   canvases, the HUD, dock, cursor, all page content (#smooth-wrapper) and any
   JS-injected kinetic chrome (panel menu / controls / step / sheet backdrop).
   The opaque overlay already occludes them by z-order; this ALSO stops the
   milkdrop/flash bleeding through the overlay's brief fade and is future-proof
   against anything that later grabs a z-index above the overlay. Driven by the
   html.k-takeover class that kinetic-video.js toggles in lockstep with .is-on. */
html.k-takeover body > *:not(#k-video-takeover) {
  visibility: hidden !important;
}
/* #hero rides ABOVE the video during the takeover (couple's name over the
   visualizer). #hero is trapped in #smooth-wrapper's stacking context (z2), so
   rather than fight to lift it past z10000, drop the overlay BELOW that layer
   while armed — every other layer is already hidden by visibility above, so the
   overlay's black still occludes them at any z. Then re-show #hero (a
   visibility:visible child overrides its hidden ancestor) and force it opaque
   (GSAP may have parked it at autoAlpha:0 on another panel). Gated to k-video-vis
   so the name appears only once the video is actually painting, not during the
   initial 2s cut-to-black. */
html.k-takeover #k-video-takeover { z-index: 1; }
html.k-video-vis #hero {
  visibility: visible !important;
  opacity: 1 !important;
}
/* ...but not the hero's own backdrop (grid/light-shaft) or its scrim (which
   fades to solid obsidian at the bottom) — those would paint over the video.
   Keep only the text so the name floats over the full visualizer. */
html.k-video-vis #hero .hero-media,
html.k-video-vis #hero .hero-veil {
  visibility: hidden !important;
}
#k-video-takeover video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;         /* fill the viewport like the full-screen scene */
  opacity: 0;                /* ramped inline by kinetic-video.js */
  background: #000;
  will-change: opacity;
}
@media (prefers-reduced-motion: reduce) {
  /* kinetic-video.js already bails under reduced-motion; belt-and-braces. */
  #k-video-takeover { display: none !important; }
}

/* ═══ THEME-9 VIDEO TAKEOVER (own-audio, plays INSTEAD of the scene) ══════
   Unlike theme-8 (a muted visualizer synced to the music at two brief windows),
   theme-9's video IS the track: it carries its own audio and plays fullscreen
   for the whole song, sitting ABOVE the scene (z40) but BELOW the HUD/dock/cursor
   (z55–9998) so the instrument chrome stays legible on top. The opaque video
   occludes every scene layer (milkdrop, lightshow, grain, both dancers, all page
   content — all ≤ z2) by z-order. The HUD only vanishes for one authored window
   (3:00→3:35), via k-video9-solo. kinetic-video9.js toggles both classes. */
#k-video9-takeover {
  position: fixed;
  inset: 0;
  z-index: 1;                /* behind #smooth-wrapper (z2) so the scene rides over the video */
  background: #000;
  display: none;             /* not painted until armed */
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s linear;
}
#k-video9-takeover.is-on {
  display: block;
  opacity: 1;
  pointer-events: auto;      /* swallow taps on the hidden scene beneath */
}
#k-video9-takeover video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;         /* fill the viewport like the full-screen scene */
  opacity: 0;                /* ramped inline by kinetic-video9.js */
  background: #000;
  will-change: opacity;
}
/* The one authored solo window (3:00→3:35): hide the HUD chrome AND the hero
   names — HUD frame, both kinetic panels, dock, cursor. */
html.k-video9-solo #hero,
html.k-video9-solo #k-hud,
html.k-video9-solo #k-panel-menu,
html.k-video9-solo #k-panel-controls,
html.k-video9-solo #right-dock,
html.k-video9-solo #k-cursor {
  opacity: 0 !important;
  pointer-events: none !important;
  transition: opacity .3s linear;
}
@media (prefers-reduced-motion: reduce) {
  /* kinetic-video9.js already bails under reduced-motion; belt-and-braces. */
  #k-video9-takeover { display: none !important; }
}

/* ═══════════════════════════════════════════════════════════════════
   HOLD-TO-DECRYPT (kinetic date reveal) — replaces the foil scratch-off
   Markup: .scratch.kdc > .scratch-frame (cipher plate) + button.kdc-key.
   Driven by js/app/kinetic-decrypt.js, which sets --kdc-charge (0→1) on
   .kdc and toggles .is-decrypting / .is-decrypted / .is-nudge.
   ═══════════════════════════════════════════════════════════════════ */

/* The real date, for assistive tech, present from first paint — AT users never
   perform the gesture. The animating glyphs above are aria-hidden. */
html[data-variant="kinetic"] .kdc-sr {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Cipher plate. Mono at ALL times (not only mid-decode like the headings): the
   glyph churn swaps letters for digits, so a proportional face would reflow the
   plate every frame and could wrap it inside the fixed 16/9 frame. */
html[data-variant="kinetic"] .kdc .scratch-date,
html[data-variant="kinetic"] .kdc .scratch-sub {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
html[data-variant="kinetic"] .kdc .scratch-date { font-weight: 700; font-size: clamp(1.5rem, 6.2vw, 2.3rem); letter-spacing: .04em; }

/* Un-resolved text sits back; resolving brings it forward. Low contrast while
   churning keeps the cipher from reading as noise (and lowers the reading load). */
html[data-variant="kinetic"] .kdc .scratch-under { transition: opacity .25s linear; }
html[data-variant="kinetic"] .kdc.is-decrypting .scratch-under { opacity: calc(.46 + var(--kdc-charge, 0) * .54); }
html[data-variant="kinetic"] .kdc:not(.is-decrypted) .scratch-under { opacity: .5; }
html[data-variant="kinetic"] .kdc.is-decrypted .scratch-under { opacity: 1; }

/* Charge wash inside the frame: a cyan floor that rises with the hold, so the
   plate itself reports progress and the eye never has to leave the date. */
html[data-variant="kinetic"] .kdc .scratch-frame::before,
html[data-variant="kinetic"] .kdc .scratch-frame::after { z-index: 2; }
html[data-variant="kinetic"] .kdc .scratch-frame {
  background:
    linear-gradient(180deg, transparent calc(100% - var(--kdc-charge, 0) * 100%), rgba(var(--accent-rgb), .10) 100%),
    var(--surface);
}

/* The key. touch-action:none is what prevents the browser claiming the gesture
   and firing pointercancel mid-hold; -webkit-touch-callout/user-select kill the
   iOS long-press callout (which since iOS 13.1 can appear without a contextmenu
   event, so JS cannot suppress it). Scoped to the button — never document-wide. */
html[data-variant="kinetic"] .kdc-key {
  display: block; width: 100%; margin: 1rem auto 0; padding: .8rem 1rem .7rem;
  position: relative; overflow: hidden;
  background: rgba(var(--accent-rgb), .05);
  border: 1px solid var(--hairline);
  color: var(--accent);
  font-family: var(--font-mono); font-size: .66rem; letter-spacing: .18em; text-transform: uppercase;
  cursor: pointer;
  touch-action: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none; user-select: none;
  transition: border-color .2s linear, background-color .2s linear;
}
html[data-variant="kinetic"] .kdc-key:hover,
html[data-variant="kinetic"] .kdc-key:focus-visible { border-color: rgba(var(--accent-rgb), .55); background: rgba(var(--accent-rgb), .09); }
html[data-variant="kinetic"] .kdc-key-label { position: relative; z-index: 1; }

/* Charge bar: a hairline at the key's foot, width tracking the hold. */
html[data-variant="kinetic"] .kdc-bar {
  position: absolute; left: 0; bottom: 0; height: 2px;
  width: calc(var(--kdc-charge, 0) * 100%);
  background: var(--accent);
  box-shadow: 0 0 10px rgba(var(--accent-rgb), .7);
}
html[data-variant="kinetic"] .kdc.is-decrypting .kdc-key { background: rgba(var(--accent-rgb), .12); border-color: rgba(var(--accent-rgb), .6); }

/* Tap instead of hold → the affordance is restated, not obeyed. */
@keyframes kdc-nudge { 0%, 100% { transform: none; } 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }
html[data-variant="kinetic"] .kdc.is-nudge .kdc-key { animation: kdc-nudge .26s steps(2, end) 2; border-color: rgba(var(--accent-rgb), .6); }

@media (prefers-reduced-motion: reduce) {
  /* The module removes the key and shows the plain date; belt-and-braces. */
  html[data-variant="kinetic"] .kdc-key { display: none !important; }
  html[data-variant="kinetic"] .kdc .scratch-under { opacity: 1 !important; }
}

/* ── Reveal payoff: one cyan scanline sweep + bloom across the plate ──────
   Replaces the shared confetti burst (canvas-confetti is not loaded in kinetic).
   A single non-repeating flash, local to the ~16/9 plate — well inside the
   general-flash area threshold, and nothing here strobes. */
html[data-variant="kinetic"] .kdc-flare {
  position: absolute; inset: 0; z-index: 3; pointer-events: none; opacity: 0;
  mix-blend-mode: screen;
  background:
    linear-gradient(180deg, transparent 0 46%, rgba(var(--accent-rgb), .9) 49% 51%, transparent 54% 100%),
    radial-gradient(120% 120% at 50% 50%, rgba(var(--accent-rgb), .3), transparent 62%);
  background-repeat: no-repeat;
  background-size: 100% 220%, 100% 100%;
  background-position: 50% -120%, 50% 50%;
}
@keyframes kdc-flare {
  0%   { opacity: 0; background-position: 50% -120%, 50% 50%; }
  10%  { opacity: 1; }
  100% { opacity: 0; background-position: 50% 220%, 50% 50%; }
}
@keyframes kdc-frame-flash {
  0%   { border-color: rgba(var(--accent-rgb), .85); box-shadow: 0 0 26px rgba(var(--accent-rgb), .35); }
  100% { border-color: var(--hairline); box-shadow: var(--shadow-lift); }
}
html[data-variant="kinetic"] .kdc.is-decrypted .kdc-flare { animation: kdc-flare .62s cubic-bezier(.16, 1, .3, 1) forwards; }
html[data-variant="kinetic"] .kdc.is-decrypted .scratch-frame { animation: kdc-frame-flash .5s ease-out; }

/* ── The countdown is the decrypt's payoff, so it does not exist until the
   date does. Holding it back keeps the answer from sitting above the puzzle:
   crack the date, and the clock to it materialises. js/app/kinetic-decrypt.js
   adds .kdc-done to #countdown on latch (and immediately under reduced motion).
   In the deck path the countdown panel is display:block with the cells in a
   flex row above the plate, so hiding them leaves no empty column.

   EVERY rule below is scoped with :has(.kdc:not([hidden])) — it applies ONLY
   WHEN A LIVE DECRYPT PLATE IS ON SCREEN. {{SECRET_ONLY}} resolves to the
   `hidden` ATTRIBUTE, not to removal (build.js: SECRET_ONLY = reveal ?
   "hidden" : ""), so with revealDate=true the plate is still in the DOM — a
   bare :has(.kdc) matches it and keeps hiding the counter. Without the
   :not([hidden]) the countdown stays display:none forever on desktop once the
   dates publish (mobile escaped only via the max-width:1099.98px override
   further down). Do not drop either half of the scoping. Failure direction is
   safe: if :has() is unsupported the digits simply show. ── */
html[data-variant="kinetic"] .countdown:has(.kdc:not([hidden])) .count-grid { display: none; }
html[data-variant="kinetic"] .countdown.kdc-done .count-grid { display: grid; }
@media (min-width: 900px) {
  /* deck path re-flows the released cells into the HUD row (matches the
     html.k-deck[data-panel="countdown"] .count-grid rule above) */
  html.k-deck[data-panel="countdown"] .countdown.kdc-done .count-grid { display: flex; }
  /* non-deck fallback (no GSAP): the techno 2-column split would leave an empty
     counter column, so the plate takes the full width until it is cracked */
  html[data-variant="kinetic"]:not(.k-deck) .countdown:has(.kdc:not([hidden])):not(.kdc-done) .wrap { grid-template-columns: minmax(0, 32rem); }
  html[data-variant="kinetic"]:not(.k-deck) .countdown:has(.kdc:not([hidden])):not(.kdc-done) .scratch { grid-column: 1; }
}
@media (prefers-reduced-motion: reduce) {
  html[data-variant="kinetic"] .kdc-flare { display: none !important; }
}

/* ── Hero centre scrim (kinetic) ──────────────────────────────────────
   The black hole is anchored off-axis (HOLE_OFS in js/app/lightshow.js) so it no
   longer sits behind the name lockup, but it is a LIVE scene: the disk drifts,
   brightens with the music envelope, and the debris cluster orbits. Relying on
   "the bright thing is somewhere else right now" is not legibility. This gives the
   centre text stack its own darker ground so the type holds at any frame of the
   animation, per the type-over-imagery convention (a local scrim, not a
   full-viewport wash that would flatten the render).
   Stronger + tighter than the shared techno .hero-veil, and deliberately biased
   toward the upper half, which is where the disc actually is. */
html[data-variant="kinetic"] .hero-veil {
  background:
    radial-gradient(ellipse 58% 42% at 50% 46%,
      rgba(4, 8, 12, .74) 0%, rgba(4, 8, 12, .42) 46%, rgba(4, 8, 12, .10) 70%, transparent 84%),
    linear-gradient(0deg, var(--bg) 0%, rgba(9, 10, 13, .3) 30%, transparent 58%);
}

/* ══════════════════════════════════════════════════════════════════
   HERO — rebuilt after a three-seat design review scored it 4.5/10
   ("a WebGL demo that someone typed names onto"). Four findings drive
   everything below:

   1. BALANCE. The old hero centred a symmetric type stack under an
      asymmetric art object and left the bottom 40% dead. Centred type
      beneath off-centre art reads as a mistake, not as negative space.
      Fixed with a counterweight instead of symmetry: a 12-column grid,
      art upper-right, copy bottom-LEFT. The diagonal is the balance,
      and bottom-anchoring the copy is what fills the void.
   2. HIERARCHY. On a dark page, hierarchy is LUMINANCE, not size — the
      brightest thing reads as nearest, which is why the artwork was
      out-shouting the couple on their own invitation. The names are now
      the brightest mass in frame and the only white one.
   3. TYPE VOICES. Four (condensed grotesque / mono / rounded humanist /
      Devanagari) cut to three: Archivo for display, Space Mono for
      labels, Noto Serif Devanagari for the shloka. Space Grotesk is
      dropped from the hero entirely.
   4. RHYTHM. Six near-identical 40-60px gaps between elements of wildly
      different weight destroyed all grouping. Replaced with a scale
      whose inter-group gap is >3x the intra-group one, so the hero
      reads as three groups, not seven orphans.
   ══════════════════════════════════════════════════════════════════ */
html[data-variant="kinetic"] {
  /* fluid type scale, perfect fourth (1.333) from a 15px base */
  --t-shloka: clamp(1.02rem, 0.88rem + 0.60vw, 1.40rem);
  --t-gloss:  clamp(0.72rem, 0.68rem + 0.18vw, 0.84rem);
  --t-kicker: clamp(0.64rem, 0.60rem + 0.19vw, 0.76rem);
  --t-names:  clamp(2.85rem, 0.55rem + 9.9vw, 8.9rem);
  --t-amp:    clamp(1.45rem, 1.00rem + 1.85vw, 2.60rem);   /* the missing rung */
  --t-quote:  clamp(0.98rem, 0.86rem + 0.52vw, 1.30rem);
  --t-await:  clamp(0.70rem, 0.66rem + 0.19vw, 0.82rem);

  /* spacing: intra-group vs inter-group must differ by >3x or grouping dies */
  --sp-hair: 0.5rem;
  --sp-tight: 1rem;
  --sp-group: 3.25rem;
}

/* ── grid: art upper-right, copy lower-left ───────────────────────── */
html[data-variant="kinetic"] .hero-inner {
  position: absolute; inset: 0; z-index: 2;
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  grid-template-rows: auto 1fr auto;
  align-items: initial; justify-items: initial;
  padding: clamp(1.5rem, 4vw, 4rem);
  padding-bottom: clamp(4.5rem, 9vh, 7rem);   /* room for the scroll cue */
  column-gap: clamp(1rem, 2vw, 2rem);
  text-align: left;
}
html[data-variant="kinetic"] .hero-copy {
  grid-column: 1 / 9;
  grid-row: 3;
  align-self: end;
  text-align: left;
  /* an invisible scrim: separation from the live backdrop with no visible box */
  filter: drop-shadow(0 0 42px rgba(11, 12, 15, .92));
}
@media (max-width: 900px) {
  html[data-variant="kinetic"] .hero-copy { grid-column: 1 / 13; }
}

/* ── invocation ───────────────────────────────────────────────────── */
html[data-variant="kinetic"] .hero-invocation {
  text-align: left;
  margin: 0 0 var(--sp-group);
  display: grid; justify-items: start; gap: var(--sp-hair);
}
html[data-variant="kinetic"] .hero-invocation .invocation-motif {
  width: clamp(26px, 4vw, 34px);   /* was a 62px crosshair that read as a HUD reticle */
  opacity: .5;
  /* the base rule is `margin: 0 auto`, which kept the motif optically centred in
     the column while the rest of the invocation moved left — it read as a stray
     ring floating in the middle of the frame */
  margin: 0 0 .15rem;
  justify-self: start;
}
html[data-variant="kinetic"] .hero-invocation .shloka {
  font-size: var(--t-shloka);
  /* Devanagari has no x-height, so matching the Latin point size renders it
     optically smaller; ~1.3x against all-caps Latin is the correction. */
  line-height: 1.85;              /* matras collide between lines below this */
  letter-spacing: 0;              /* tracking breaks the shirorekha that binds a word */
  color: #8fe9ff;
  opacity: .62;                   /* must sit visibly BEHIND the names in depth */
}
html[data-variant="kinetic"] .hero-invocation .shloka-roman {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--t-gloss);
  letter-spacing: .12em;
  color: var(--label);
  opacity: .75;
}
html[data-variant="kinetic"] .shloka-gloss {
  display: block;
  font-family: "Archivo", var(--font-display);
  font-variation-settings: 'wght' 300, 'wdth' 100;
  font-size: var(--t-gloss);
  font-style: italic;
  line-height: 1.5;
  color: var(--muted);
  max-width: 30ch;
}

/* ── kicker: a label ON the names, so it is bonded to them ────────── */
html[data-variant="kinetic"] .hero .kicker {
  margin: 0 0 var(--sp-hair);
  max-width: none; margin-inline: 0;
  font-size: var(--t-kicker);
  letter-spacing: .34em;
  color: var(--label);
  text-align: left;
}

/* ── the names: the brightest, widest, only-white mass in frame ───── */
html[data-variant="kinetic"] .hero-names {
  display: grid; justify-items: start;
  gap: 0;
  margin: 0;
  font-family: "Archivo", var(--font-display);
  /* a REAL width axis, not synthetic stretching. Wide reads confident and
     luxury and fills the measure; condensed would narrow the column and make
     the empty band worse. */
  font-variation-settings: 'wght' 640, 'wdth' 125;   /* 125 = the axis maximum: genuinely extended */
  font-weight: 600;
  font-size: var(--t-names);
  line-height: .86;               /* wide faces need tight leading */
  letter-spacing: -.02em;         /* and negative tracking at display size */
  text-transform: uppercase;
  color: #f2f6f8;                 /* not #fff: harsh, and it clips */
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;   /* light-on-dark optically bolds without it */
}
html[data-variant="kinetic"] .hero-name { padding-inline: 0; color: inherit; }

/* the ampersand: smaller and contrasting, per invitation convention, and
   subordinated by weight + luminance rather than shouted in flat accent.
   A full-strength cyan "&" at name size puts the accent hue on the least
   important glyph in the lockup. */
html[data-variant="kinetic"] .hero-amp {
  display: block;
  font-family: "Archivo", var(--font-display);
  font-variation-settings: 'wght' 300, 'wdth' 100;
  font-size: var(--t-amp);
  line-height: 1.15;
  color: #8fe9ff;
  opacity: .55;
  margin: -.06em 0 -.04em .06em;   /* tighten the stack: the lockup is one object */
}
html[data-variant="kinetic"] .hero-amp::before,
html[data-variant="kinetic"] .hero-amp::after { display: none; }

/* ── quote + the dates-are-coming line ────────────────────────────── */
html[data-variant="kinetic"] .hero-date {
  margin: var(--sp-tight) 0 0;
  font-family: "Archivo", var(--font-display);
  font-variation-settings: 'wght' 300, 'wdth' 104;
  font-size: var(--t-quote);
  line-height: 1.5;
  letter-spacing: .01em;
  color: var(--body);
  text-align: left;
}
html[data-variant="kinetic"] .hero-await {
  margin: var(--sp-tight) 0 0;
  font-family: var(--font-mono);
  font-size: var(--t-await);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--label);
}

/* ── hashtag: a corner mark, not a second headline ────────────────── */
html[data-variant="kinetic"] .hero-tag {
  grid-column: 1 / 6; grid-row: 1;
  align-self: start; justify-self: start;
  margin: 0;
  font-size: .68rem; letter-spacing: .2em;
  color: var(--muted); opacity: .6;
}

/* ── scroll cue: kinetic used to hide this outright, which left the page
      with no affordance at all. It is now the CTA as well as the cue. ── */
html[data-variant="kinetic"] .scroll-cue,
html.k-deck[data-variant="kinetic"] .scroll-cue {
  display: flex; align-items: center; gap: .55rem;
  position: absolute; left: auto; right: clamp(1.5rem, 4vw, 4rem);
  bottom: clamp(1.5rem, 4vh, 2.5rem);
  z-index: 3;
  text-decoration: none;
  opacity: .8; transition: opacity .3s var(--ease-luxe);
}
html[data-variant="kinetic"] .scroll-cue:hover { opacity: 1; }
html[data-variant="kinetic"] .scroll-cue .cue-label {
  font-size: .58rem; letter-spacing: .28em; color: var(--muted);
}
html[data-variant="kinetic"] .scroll-cue .cue-mouse { display: none; }
html[data-variant="kinetic"] .scroll-cue .cue-chevrons { display: grid; width: 16px; height: 16px; }

/* the wireframe polyhedron and the ambient mecha rendered at ~10% opacity,
   which a reviewer could not distinguish from JPEG compression artefacts.
   An element read as noise is worse than no element: it makes the frame look
   dirty rather than deep. They are dimmed further into pure atmosphere here so
   they never compete with the type. */
html[data-variant="kinetic"] #k-ambient-dancers { opacity: .3; }

/* ── Hero, revision 2 — after a re-review scored the above 7.2/10 ─────
   Two classes of fix. First, three COLLISIONS that only appear once the
   pointer moves (the kinetic deck chrome auto-hides while idle, so no
   static screenshot ever showed them): the deck's section-index rail
   printed straight through the invocation, the music dock sat on the
   hashtag, and the scroll cue landed on the deck's Back/Next. Second,
   the reviewer's alignment and rhythm defects.                        */

html[data-variant="kinetic"] {
  --gutter: clamp(1.5rem, 4vw, 5rem);
  /* column 1 is reserved for the deck's own chrome rail; the copy starts
     after it so the section index can never overprint the invocation */
  --rail: clamp(0rem, 7vw, 7.5rem);
}

html[data-variant="kinetic"] .hero-inner {
  padding: var(--gutter);
  padding-left: calc(var(--gutter) + var(--rail));
  padding-bottom: var(--gutter);   /* was 66px vs an 80px left margin: uneven frame */
}

/* the invocation becomes a masthead mark at the TOP edge rather than a
   preamble sitting in the reading path, so the hero reads as three groups:
   [mark] · void · [kicker + names + quote + reveal] */
html[data-variant="kinetic"] .hero-copy {
  grid-row: 1 / 4;
  display: grid;
  grid-template-rows: auto 1fr auto;
  align-self: stretch;
}
html[data-variant="kinetic"] .hero-invocation { grid-row: 1; align-self: start; margin: 0; }
html[data-variant="kinetic"] .hero-lockup { grid-row: 3; align-self: end; }

/* 1. THE top defect: the H1 was indented ~90px from the column's left edge,
      breaking the one line the whole layout is built on. Cause: the base rule
      sets `justify-content: center`, which centres the grid tracks. */
html[data-variant="kinetic"] .hero-names {
  justify-content: start;
  justify-items: start;
  margin-left: -.055em;            /* optical: round S / R stem flush, not mathematically flush */
  line-height: .82;
  /* wght 560 not 640: at 640 the widened S/C/R bowls read as transport signage.
     The extreme width is fine; the weight was the corporate tell. */
  font-variation-settings: 'wght' 560, 'wdth' 125;
  font-optical-sizing: auto;
}
/* RIYA is half the length of SANCHIT; a touch more size closes a 2:1 rag */
/* (removed: a per-name size override would break the equal-size guarantee) */

/* 4. the ampersand as a true rung: aligned to the name stems, upright, tighter */
html[data-variant="kinetic"] .hero-amp {
  font-size: .42em;
  font-variation-settings: 'wght' 400, 'wdth' 118;
  font-variant-alternates: normal;
  font-feature-settings: normal;
  line-height: .8;
  margin: -.02em 0 .02em;
  padding-left: .04em;
}

/* 5. rhythm inside the bottom group was inverted — the quote was glued to RIYA
      and divorced from the line it belongs with */
html[data-variant="kinetic"] .hero-date { margin-top: 2.5rem; }
html[data-variant="kinetic"] .hero-await { margin-top: 1.25rem; }

/* 8/9. gloss: balanced wrap, no stranded article */
html[data-variant="kinetic"] .shloka-gloss { max-width: 34ch; text-wrap: balance; }
/* 13. the Devanagari was out-saturating the corona; the eclipse owns the cyan */
html[data-variant="kinetic"] .hero-invocation .shloka { color: #cfe8ef; opacity: .70; }

/* hashtag: now the last line of the copy column, clear of the music dock */
html[data-variant="kinetic"] .hero-tag {
  grid-column: auto; grid-row: auto;
  margin: 1.75rem 0 0;
  font-size: .64rem; letter-spacing: .22em;
  color: var(--muted); opacity: .5;
}

/* the deck navigates by buttons and a menu sheet — the wheel does nothing — so a
   "scroll" cue would be an instruction that cannot be obeyed. Restore the skin's
   original decision to hide it; the deck's NEXT button is the affordance. */
html.k-deck[data-variant="kinetic"] .scroll-cue { display: none; }

/* 2. the ambient mecha rendered THROUGH the RIYA counters. Texture inside the
      H1's letterforms is a hygiene failure, and at 30% it was already too faint
      to be a design element — so it leaves the hero entirely. */
html[data-variant="kinetic"] #hero ~ #k-ambient-dancers,
html[data-variant="kinetic"] #k-ambient-dancers { opacity: 0; transition: opacity .6s var(--ease-luxe); }
html[data-variant="kinetic"]:not([data-panel="invocation"]) #k-ambient-dancers { opacity: .34; }

/* the invocation mark shares the top-left corner with the music dock; drop it
   clear so the two never crowd (the dock is ~120px tall at its largest) */
@media (min-width: 900px) {
  html[data-variant="kinetic"] .hero-invocation { margin-top: 3.25rem; }
}

/* ── Hero, revision 3 — against an 8.4 / 8.1 pair of re-reviews ───────
   The theme of this pass is that the deck CHROME has to belong to the
   same grid as the copy, instead of sitting on top of it.            */

/* 1. rail and copy were two unrelated left edges (45px and 220px). One
      modular rhythm: rail at 64, a hairline column boundary at 192, copy
      starting at 256. */
html[data-variant="kinetic"] { --rail: 12rem; }
html[data-variant="kinetic"] .hero-inner { padding-left: calc(var(--gutter) + var(--rail)); }
html.k-deck[data-variant="kinetic"] .k-menu {
  left: 64px;
  border-right: 1px solid rgba(255, 255, 255, .06);
  padding-right: 1.5rem;
}

/* 2. the crosshair hairlines ran full-bleed at constant alpha, cutting clean
      through the H1 and the corona. Fade them out from the pointer so they
      read as instrumentation rather than as two rules drawn over the art. */
html[data-variant="kinetic"] .k-cursor-h {
  background: linear-gradient(90deg, transparent, rgba(120, 220, 240, .10) 22%, rgba(120, 220, 240, .10) 78%, transparent);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 26%, #000 74%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 26%, #000 74%, transparent);
}
html[data-variant="kinetic"] .k-cursor-v {
  background: linear-gradient(180deg, transparent, rgba(120, 220, 240, .10) 22%, rgba(120, 220, 240, .10) 78%, transparent);
  -webkit-mask-image: linear-gradient(180deg, transparent, #000 26%, #000 74%, transparent);
          mask-image: linear-gradient(180deg, transparent, #000 26%, #000 74%, transparent);
}

/* 7. the bottom-left "00 — INVOCATION" label states what the rail's active item
      and the 01/06 counter already say. Three statements of one fact. */
html[data-variant="kinetic"] .k-hud-sec { display: none; }

/* 9. NEXT was the only bordered box on the page and read as a browser default */
html.k-deck[data-variant="kinetic"] #k-next {
  border: 0; background: none; padding-inline: .2rem;
  position: relative;
}
html.k-deck[data-variant="kinetic"] #k-next::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -3px; height: 1px;
  background: currentColor; transform: scaleX(0); transform-origin: left;
  transition: transform .35s var(--ease-luxe);
}
html.k-deck[data-variant="kinetic"] #k-next:hover::after { transform: scaleX(1); }

/* the hero is the one panel where the guest has not yet learned the interaction,
   so NEXT must not vanish with the rest of the idle chrome. */
html[data-panel="invocation"] #k-next { opacity: 1 !important; pointer-events: auto !important; }

/* 5. ampersand was too small to hold its counter at 0.42em */
html[data-variant="kinetic"] .hero-amp {
  font-size: .52em;
  font-variation-settings: 'wght' 450, 'wdth' 118;
}

/* 8. Devanagari overshot to near-white and started reading as body copy */
html[data-variant="kinetic"] .hero-invocation .shloka {
  color: #9fd4e2; opacity: .85; letter-spacing: .01em;
}
/* the gloss read as a default italic sans; bring it into the verse's voice */
html[data-variant="kinetic"] .shloka-gloss {
  font-style: normal;
  font-variation-settings: 'wght' 320, 'wdth' 100;
  letter-spacing: .06em;
  opacity: .58;
}

/* invocation gets its own vertical slot so the top-left corner is not three
   elements deep (brand, dock, mark) */
@media (min-width: 900px) {
  html[data-variant="kinetic"] .hero-invocation { margin-top: 6.5rem; }
}

/* hashtag: masthead row, opposite the brand, and legible enough to earn its place */
html[data-variant="kinetic"] .hero-tag {
  grid-column: 1 / 13; grid-row: 1;
  justify-self: end; align-self: start;
  margin: 0; opacity: .75; color: var(--label);
}

/* ── the bottom group's hierarchy: five elements, five distinct weights ── */
html[data-variant="kinetic"] .hero-voice {
  margin: .5rem 0 0;
  font-family: "Archivo", var(--font-display);
  font-variation-settings: 'wght' 300, 'wdth' 104;
  font-size: var(--t-quote);
  color: var(--muted);
  opacity: .8;
}
html[data-variant="kinetic"] .hero-await {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .1rem 1rem;
  margin-top: 1.6rem;
}
html[data-variant="kinetic"] .hero-await-promise { color: var(--label); }
/* the single most useful sentence in the hero was also the faintest. It is now
   the one cyan call to action, and it is a real control: it jumps the deck to
   the countdown act, so the hero has a door that does not auto-hide. */
html[data-variant="kinetic"] .hero-await-cta {
  font-family: var(--font-mono);
  font-size: var(--t-await);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--accent);
  cursor: pointer;
  position: relative;
}
html[data-variant="kinetic"] .hero-await-cta::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -4px; height: 1px;
  background: currentColor; opacity: .45;
  transform: scaleX(1); transform-origin: left;
  transition: opacity .3s var(--ease-luxe);
}
html[data-variant="kinetic"] .hero-await-cta:hover::after { opacity: 1; }

/* 3. RSVP is the one act where the guest owes an action; it should not look
      identical to the acts where they only read. */
html.k-deck[data-variant="kinetic"] .k-menu-item:last-child .k-menu-label { color: #cfe8ef; }

/* the short name sits flush RIGHT so the two lines bracket a rectangle. Both are
   set at one shared point size (js/app/name-fit.js) so neither partner's name is
   optically larger — the block is squared by alignment, not by scaling. */
html[data-variant="kinetic"] .hero-names { justify-items: stretch; }
html[data-variant="kinetic"] .hero-name { display: block; width: 100%; }
html[data-variant="kinetic"] .hero-name:first-of-type { text-align: left; }
html[data-variant="kinetic"] .hero-name:last-of-type { text-align: right; font-size: inherit; }
html[data-variant="kinetic"] .hero-amp { text-align: center; }

/* ── Hero, revision 4 — against a 9.0. Everything here is alignment ───
   discipline: at this point the composition is right and what is left is
   edges that land on nothing.                                          */
html[data-variant="kinetic"] {
  --rail-x: 4rem;      /* 64px  — the rail's own left inset          */
  --copy-x: 16rem;     /* 256px — 4x the rail unit, a real grid step */
  --gutter-r: 5rem;    /* 80px  — shared right inset for ALL corner marks */
}

@media (min-width: 1100px) {
  html[data-variant="kinetic"] .hero-inner {
    padding-left: var(--copy-x);
    padding-right: var(--gutter-r);
  }
  /* The name rectangle's two vertical edges previously landed on nothing, which
     is what kept it reading as "two names that happen to end at the same x"
     rather than as a block. The copy column now carries a visible hairline on
     its right edge, so both names are flush to something the eye can see. */
  html[data-variant="kinetic"] .hero-copy {
    grid-column: 1 / 9;
    border-right: 1px solid rgba(255, 255, 255, .055);
    padding-right: clamp(1.5rem, 3vw, 3rem);
  }
}

/* the single most visible alignment error in the frame was a 10px near-miss:
   the motif's artwork is inset ~4% inside its own viewBox, so a flush element
   box still renders visually indented. Pull it back by that inset. */
html[data-variant="kinetic"] .hero-invocation .invocation-motif { margin-left: -1.5px; }

/* the middle band was reading as an unfilled row rather than as air. Dropping
   the invocation group lets the void sit ABOVE it, where the polyhedron already
   holds the eye, and the band becomes framing for the eclipse. */
@media (min-width: 900px) {
  html[data-variant="kinetic"] .hero-invocation { margin-top: clamp(6.5rem, 17vh, 13rem); }
}

/* two sentences in the same face at the same size read as a duplicate, not a
   pair: the voice line becomes a subordinate aside tucked under the quote */
html[data-variant="kinetic"] .hero-voice {
  margin-top: .625rem;
  font-size: .95rem;
  color: rgba(255, 255, 255, .55);
  opacity: 1;
}

/* the ampersand sat optically low between the two cap heights */
html[data-variant="kinetic"] .hero-amp { transform: translateY(-6px); }

/* every corner mark shares one right inset so the masthead reads as a rule */
html[data-variant="kinetic"] .hero-tag { padding-right: 0; }
html.k-deck[data-variant="kinetic"] #k-panel-controls { right: var(--gutter-r); }
html[data-variant="kinetic"] .k-hud-time { right: var(--gutter-r); }
/* ...and one left inset, so the music dock stops disagreeing with the rail */
html[data-variant="kinetic"] #right-dock,
html.k-deck[data-variant="kinetic"] .k-menu { left: var(--rail-x); }
html[data-variant="kinetic"] .k-hud-brand { left: var(--rail-x); }

/* ── Hero, revision 5 — against a 9.0 / 8.9 pair ──────────────────── */

/* The invocation moves out of the copy column and into the mid-band, sitting
   under the eclipse and right-aligned. Two problems fall at once: the verse
   about the moon and the stars was as far from the celestial image as the frame
   allowed and read as a caption on nothing, and the centre of the composition
   was an unfilled row on the diagonal between the names and the eclipse. */
@media (min-width: 1100px) {
  html[data-variant="kinetic"] .hero-invocation {
    grid-column: 8 / 13;
    grid-row: 2;
    align-self: center;
    justify-self: end;
    justify-items: end;
    text-align: right;
    margin: 0;
    max-width: 26rem;
  }
  html[data-variant="kinetic"] .hero-invocation .invocation-motif { margin-left: 0; margin-right: -1.5px; justify-self: end; }
  html[data-variant="kinetic"] .shloka-gloss { max-width: none; }
}

/* the three copy lines were evenly spaced, so nothing grouped. The quote and the
   voice line are ONE thought and now sit tight; the blessing closes that block;
   a full line follows before the dates/CTA row. */
html[data-variant="kinetic"] .hero-voice { margin-top: .4em; }
html[data-variant="kinetic"] .hero-blessing {
  margin: .4em 0 0;
  font-family: "Archivo", var(--font-display);
  font-variation-settings: 'wght' 300, 'wdth' 104;
  font-size: .95rem;
  color: rgba(255, 255, 255, .45);
}
html[data-variant="kinetic"] .hero-await { margin-top: 2.4rem; }

/* the human line was styled identically to the line above it, so the intended
   change of voice was invisible. It borrows the diamond bead's ~3200K warmth —
   the only warm token on the page, and the one the eclipse already established.
   This is a highlight, not a second hue: the obsidian + cyan lock still holds. */
html[data-variant="kinetic"] .hero-voice { color: #f0e4d6; opacity: .92; }

/* the ampersand read as a superscript connector rather than the hinge of the
   union the rectangle is built around */
html[data-variant="kinetic"] .hero-amp { font-size: .65em; }

/* ── Hero, revision 6 — against a 9.2 ─────────────────────────────── */

/* the verse had drifted into the corona's luminance gradient, where delicate
   type loses its contrast. Drop it clear into true black. */
@media (min-width: 1100px) {
  html[data-variant="kinetic"] .hero-invocation { align-self: end; margin-bottom: 12vh; }
}

/* ragged-left over two near-equal lines reads as an accident; three lines of
   descending length make the rag a deliberate stair */
html[data-variant="kinetic"] .shloka-gloss { line-height: 1.65; }

/* the warm colour was carrying the change of voice while the smaller size was
   undoing it. Match the quote's size and let colour alone do the work. */
html[data-variant="kinetic"] .hero-voice { font-size: var(--t-quote); }

/* the column hairline was too faint to register, and a stub behind the names
   only would read as a glitch: full height, and warm-tinted so it belongs to
   the same light as the bead and the voice line */
@media (min-width: 1100px) {
  html[data-variant="kinetic"] .hero-copy { border-right-color: rgba(240, 228, 214, .10); }
}

/* the ampersand's own left margin is computed in js/app/name-fit.js; keep the
   CSS from fighting it */
html[data-variant="kinetic"] .hero-amp { text-align: left; }

/* pull the stack up so the lower-left void reads as margin rather than omission */
html[data-variant="kinetic"] .hero-copy { padding-bottom: 2.5rem; }

/* the frame claims symmetry, so the corner brackets must match */
html[data-variant="kinetic"] .k-hud-corner { border-width: 1px; }

/* ── Hero, revision 7 — the last set, against a 9.2 / 9.2 ─────────── */

/* the CTA row was landing ~50px off the deck pager, so the logistics line and
   "01 / 06" collided optically; the whole stack also sat too low */
html[data-variant="kinetic"] .hero-copy { padding-bottom: 7rem; }
html[data-variant="kinetic"] .hero-await { margin-top: 2rem; }

/* the salutation and the logistics footnote were typographic twins — same face,
   size, tracking and colour. They are not the same kind of sentence: one opens
   the invitation, the other is a note. Warm the salutation to the page's single
   warm token and leave the logistics grey. */
html[data-variant="kinetic"] .hero .kicker { color: #f0e4d6; opacity: .72; }

/* the blessing is a DIFFERENT SPEAKER — the families, not the couple — so it
   needs signature spacing, not another clause gap */
html[data-variant="kinetic"] .hero-blessing { margin-top: .9em; }

/* the source verse must lead its own translation */
html[data-variant="kinetic"] .hero-invocation .shloka { font-size: calc(var(--t-shloka) * 1.15); }
/* and sit as image-plus-caption, not as a third stacked object */
html[data-variant="kinetic"] .hero-invocation { gap: .35rem; }
@media (min-width: 1100px) {
  html[data-variant="kinetic"] .hero-invocation { margin-bottom: 14vh; }
}

/* ── Hero, revision 8 — the closing set ───────────────────────────── */

/* Every gap in the stanza was the same, so the speaker changes carried no
   rhythm: quote and invitation are ONE utterance and now bind; the families'
   blessing keeps its signature gap. */
html[data-variant="kinetic"] .hero-voice { margin-top: .55em; }
html[data-variant="kinetic"] .hero-blessing { margin-top: .9em; }

/* elders should never be the lowest-contrast element on a wedding invitation,
   techno skin or not: the blessing was fainter than the logistics footnote */
html[data-variant="kinetic"] .hero-blessing { color: var(--label); }

/* commit to the hang. A 5px offset between the ampersand and the short name's
   left edge is the one value that reads as an alignment error rather than a
   decision, so the ampersand now overhangs by a full stem. */
html[data-variant="kinetic"] .hero-amp { margin-left: 0; transform: translate(-.42em, -6px); }

/* the verse rides the corona's lower arc instead of sitting level with the
   stanza opposite it */
@media (min-width: 1100px) {
  html[data-variant="kinetic"] .hero-invocation { margin-bottom: 22vh; }
}

/* the hairline was not resolving against near-black */
@media (min-width: 1100px) {
  html[data-variant="kinetic"] .hero-copy { border-right-color: rgba(240, 228, 214, .16); }
}

/* the mark was small enough to read as a UI affordance rather than a motif */
html[data-variant="kinetic"] .hero-invocation .invocation-motif { width: clamp(34px, 5vw, 44px); }

/* 7rem overshot: the block was top-heavy over a void */
html[data-variant="kinetic"] .hero-copy { padding-bottom: 4.5rem; }

/* the pager was occluding the bottom-right corner bracket */
html.k-deck[data-variant="kinetic"] #k-panel-controls { right: calc(var(--gutter-r) + 2.5rem); }

/* ── Hero, revision 9 — closing pass (seat scores 9.4 / 9.6) ──────── */

/* the mass had settled below the geometric centre; optical centre wants to sit
   marginally above it */
html[data-variant="kinetic"] .hero-copy { padding-bottom: 6rem; }

/* the verse had drifted down until it shared a band with RIYA's baseline across
   the gutter, so the two read as one line of type. Lock it to the corona's arc. */
@media (min-width: 1100px) {
  html[data-variant="kinetic"] .hero-invocation { margin-bottom: 30vh; }
}

/* the three name lines were breathing apart rather than interlocking */
html[data-variant="kinetic"] .hero-names { line-height: .76; }

/* the blessing is a third beat, not a footnote to the invitation */
html[data-variant="kinetic"] .hero-blessing {
  font-size: var(--t-quote);
  color: rgba(240, 228, 214, .52);   /* one more degree of the page's warm token */
}

/* the hairline entered at the top, disappeared behind the lockup's scrim and
   resumed below, which read as a rendering fault. Draw it as one full-bleed rule
   behind the content instead of as a border on the text column. */
@media (min-width: 1100px) {
  html[data-variant="kinetic"] .hero-copy { border-right: 0; }
  html[data-variant="kinetic"] .hero-inner::after {
    content: "";
    position: absolute;
    top: 0; bottom: 0;
    left: calc(var(--copy-x) + ((100% - var(--copy-x) - var(--gutter-r)) * 8 / 12));
    width: 1px;
    background: linear-gradient(180deg, transparent, rgba(240, 228, 214, .16) 12%, rgba(240, 228, 214, .16) 88%, transparent);
    pointer-events: none;
    z-index: 0;
  }
}

/* ── Hero, revision 10 — final ────────────────────────────────────── */

/* At .76 leading the ampersand's strokes started overlapping SANCHIT's baseline
   zone and RIYA's cap line: it read as a glyph wedged between them rather than
   the pivot of the lockup. Smaller, with its own line box, so it NESTS in the
   counter space instead of colliding. */
html[data-variant="kinetic"] .hero-amp {
  font-size: .62em;
  line-height: 1;
  letter-spacing: -.01em;
}

/* The hairline is back on the copy column itself, which is the only way it can
   be guaranteed to sit exactly on the column's right edge — the free-standing
   rule was computed from the grid and drifted, so RIYA overran it and the one
   violation read as an accident. The reason it previously vanished mid-run was
   the lockup's scrim, so the scrim moves to the lockup alone. */
@media (min-width: 1100px) {
  html[data-variant="kinetic"] .hero-inner::after { content: none; }
  html[data-variant="kinetic"] .hero-copy {
    border-right: 1px solid rgba(240, 228, 214, .16);
    filter: none;
  }
  html[data-variant="kinetic"] .hero-lockup { filter: drop-shadow(0 0 42px rgba(11, 12, 15, .92)); }
}

/* the lower stack should breathe, not the page floor */
html[data-variant="kinetic"] .hero-copy { padding-bottom: 4.5rem; }
html[data-variant="kinetic"] .hero-blessing { margin-top: 1.4em; }

/* ── Hero, revision 11 — final polish ─────────────────────────────── */

/* Giving the ampersand its own line box fixed the stroke collision but added
   leading on both sides, so the lockup spread back out into three stacked lines.
   The collision came from OVERLAP, not from proximity, so the density can be
   recovered with negative block margin while keeping lh:1. */
html[data-variant="kinetic"] .hero-amp {
  margin-block: -.10em;
  transform: none;   /* horizontal anchor is computed in js/app/name-fit.js */
}

/* The border could not be faded, so its lower end stopped in a bare stub just
   above the deck chrome. As a pseudo-element pinned to the column's right edge
   it keeps the exact alignment a border gave AND can resolve at both ends. */
@media (min-width: 1100px) {
  html[data-variant="kinetic"] .hero-copy { border-right: 0; position: relative; }
  html[data-variant="kinetic"] .hero-copy::after {
    content: "";
    position: absolute; top: 0; bottom: 0; right: -1px; width: 1px;
    background: linear-gradient(180deg,
      transparent, rgba(240, 228, 214, .16) 10%, rgba(240, 228, 214, .16) 82%, transparent);
    pointer-events: none;
  }
}

/* The families' line now PRECEDES the names rather than trailing the CTA stack.
   On a real Indian invitation the families issue the invitation before the couple
   are named, and a reviewer identified this position (not its styling) as the last
   thing keeping the panel reading as an announcement rather than an invitation. */
html[data-variant="kinetic"] .hero-blessing {
  margin: 0 0 .5rem;
  font-size: var(--t-quote);
  color: rgba(240, 228, 214, .58);
}

/* ══════════════════════════════════════════════════════════════════
   DECK-WIDE, after a panel reviewed acts 01-05 (scores 3.4-7.9 against
   the hero's 9.8). The per-act notes are worth little until the global
   faults are gone, so these come first.
   ══════════════════════════════════════════════════════════════════ */

/* G1. ONE display face across the deck. The hero was rebuilt in Archivo while
   every act heading still resolved --font-display to Space Grotesk, so the deck
   changed typeface between panels. Overriding the variable flips all 18 call
   sites at once rather than chasing them individually. */
html[data-variant="kinetic"] {
  --font-display: "Archivo", "Space Grotesk", ui-sans-serif, system-ui, sans-serif;
  --font-body: "Archivo", "Space Grotesk", ui-sans-serif, system-ui, sans-serif;
}
/* act headings pick up the hero's extended cut so the display voice is one voice */
html[data-variant="kinetic"] .script-head {
  font-variation-settings: 'wght' 560, 'wdth' 118;
  letter-spacing: -.022em;
  line-height: .96;
}

/* G5 was a MISDIAGNOSIS and is withdrawn. A reviewer flagged drifting red and
   blue blooms sitting on the copy and I attributed them to the ambient dancer
   canvas. They were frames of the THEME-9 VIDEO TAKEOVER (js/app/kinetic-video9.js),
   which replaces the whole scene for the duration of that track — the capture
   happened to be on TRK 09/09. Nothing about the dancers was at fault, so their
   opacity is left as it was. */

/* ── DECK ACTS: the global faults, in the panel's own priority order ──
   The per-act notes are worth little until these land, because every act
   inherited the same four problems: a different type face from the hero,
   no shared grid, top-anchored content over a dead lower half, and no
   spacing or type scale.                                              */
@media (min-width: 1100px) {
  /* G2 + G3. One grid with the hero, and content anchored to the BOTTOM.
     The hero earned its score on a bottom-anchored block; the acts dumped
     everything into the top 60% and left 400-700px of dead black beneath,
     which is the loudest "unfinished" signal in the deck. */
  html.k-deck[data-variant="kinetic"] .band {
    display: grid;
    align-content: end;
    padding-left: calc(var(--gutter) + var(--rail));
    padding-right: var(--gutter-r);
    padding-bottom: 9.5rem;      /* clears the Back/Next chrome */
    padding-top: var(--gutter);
  }
  html.k-deck[data-variant="kinetic"] .band > .wrap,
  html.k-deck[data-variant="kinetic"] .band > .wrap--narrow {
    max-width: none;
    margin-inline: 0;
    padding-inline: 0;
    width: 100%;
  }
}

/* G7. One type scale with the hero. Act headings were ~64px against a 170px
   hero display, with no rung between them and 11px mono labels. */
html[data-variant="kinetic"] .script-head {
  font-size: clamp(2.6rem, 5.4vw, 4.75rem);
}

/* G6. A spacing scale with a >3x inter-group ratio, so the eyebrow binds to its
   heading and the content block separates from both. Every gap in every act was
   previously 30-50px, which groups nothing. */
html[data-variant="kinetic"] .band .kicker { margin-bottom: var(--sp-hair); }
html[data-variant="kinetic"] .band .script-head { margin-bottom: var(--sp-group); }

/* G9. Uncontrolled measure: body copy ran to ~660px on one act and ~260px on
   another. */
html[data-variant="kinetic"] .band p:not(.kicker):not(.k-index) { max-inline-size: 46ch; }

/* G8/G11. The section rail floated vertically centred while all content was
   top-anchored; with the acts now bottom-anchored it reads against the copy. */
html.k-deck[data-variant="kinetic"] .k-menu { align-self: end; }

/* ── DECK ACTS: the global faults, in the panel's priority order ──────
   A clean re-review (theme-9's video takeover excluded) put the acts at
   8.0-9.3 against the hero's 9.8. Its top finding: the acts abandoned the
   three moves the hero earned its score with — bottom-anchored copy, a
   shared grid, and art cropping an edge.                              */
@media (min-width: 1100px) {
  /* G1. Bottom-anchor, like the hero. The acts were top-anchored with
     400-700px of dead black beneath, which is the loudest "unfinished"
     signal in the deck and the single biggest per-act lift available. */
  html.k-deck[data-variant="kinetic"] .band {
    display: grid;
    align-content: end;
    padding-left: calc(var(--gutter) + var(--rail));
    padding-right: var(--gutter-r);
    padding-top: var(--gutter);
    padding-bottom: 9.5rem;          /* clears the Back/Next chrome */
  }
  /* G5. …and honour the hero's 256px content column instead of inventing a
     different inset per act. */
  html.k-deck[data-variant="kinetic"] .band > .wrap,
  html.k-deck[data-variant="kinetic"] .band > .wrap--narrow {
    max-width: none; margin-inline: 0; padding-inline: 0; width: 100%;
  }
}

/* G4. The repeating wireframe figure. It rendered at an identical position and
   scale on four acts, unlabelled, competing with the accent colour and reading
   as debug geometry left in the build. (An earlier attempt to cut this was
   withdrawn because I had misattributed the reviewer's complaint to the
   theme-9 video; the figure itself is a real and separate finding.) The hero
   already suppressed it; now the whole deck does. */
html[data-variant="kinetic"] #k-ambient-dancers { opacity: 0; }

/* G7. One type scale with the hero: act headings were ~64px against a 170px
   hero display with no rung between them and the 11px mono labels. */
html[data-variant="kinetic"] .script-head { font-size: clamp(2.6rem, 5.4vw, 4.75rem); }

/* G6/G9. A spacing scale with a >3x inter-group ratio, and a controlled measure
   (body copy ran to ~660px on one act and ~260px on another). */
html[data-variant="kinetic"] .band .kicker { margin-bottom: var(--sp-hair); }
html[data-variant="kinetic"] .band .script-head { margin-bottom: var(--sp-group); }
html[data-variant="kinetic"] .band p:not(.kicker):not(.k-index) { max-inline-size: 46ch; }

/* G9. Micro-copy opacity was drifting across four values; two tiers only. */
html.k-deck[data-variant="kinetic"] .k-menu-item { opacity: .6; }
html.k-deck[data-variant="kinetic"] .k-menu-item.is-current { opacity: 1; }

/* Two corrections, both found by reading computed styles rather than the diff.

   1. `.band` became a grid with no explicit column, so its single implicit
      column sized to max-content and the child's `width:100%` resolved against
      that content width — circular, leaving the copy stuck mid-frame at ~354px
      instead of filling the 256px column. An explicit 1fr column fixes it. */
@media (min-width: 1100px) {
  html.k-deck[data-variant="kinetic"] .band { grid-template-columns: minmax(0, 1fr); }
}
/* 2. The ambient-dancer suppression was silently losing to an earlier
      `:not([data-panel="invocation"])` rule, which carries higher specificity —
      which is why the wireframe figure kept rendering on every act after I
      "removed" it. Match that specificity so the cut actually applies. */
html[data-variant="kinetic"]:not([data-panel="invocation"]) #k-ambient-dancers { opacity: 0; }
/* ...but that cut also outranked the PRESENTER TAKEOVER rule (`html.hud-hidden
   #k-ambient-dancers.is-live`, same (1,2,1) specificity, earlier in the file),
   so a WELCOME/DROP takeover on any act faded the chrome and then showed an
   empty stage: the giant performed at opacity 0. G4 retires the RESTING
   figure, not the performance. One step up in specificity ends the arms race. */
html.hud-hidden[data-variant="kinetic"] #k-ambient-dancers.is-live { opacity: 1; }

/* ── Run of show, after the events module made the list variable-length ──
   Four alignment faults visible once five events render. */
@media (min-width: 1100px) {
  /* the rows were indented off the heading's left edge, giving the act two
     left margins; and the rules ran ~470px past the last content, pointing at
     nothing. Both come from the grid keeping the old three-card proportions. */
  html.k-deck[data-variant="kinetic"] .events .event-grid {
    margin-left: 0;
    max-width: 52rem;
  }
  /* the number column was sized for the words "TRACK 01"; it now holds two
     digits, so the gutter before the event name was ~160px of nothing */
  html.k-deck[data-variant="kinetic"] .events .event-card { grid-template-columns: 4rem minmax(0, 1fr); }
}
/* the single gated statement was breaking mid-phrase ("here / first.") because
   it inherited the body measure */
html[data-variant="kinetic"] .event-gate-note {
  max-inline-size: 40ch;
  text-wrap: balance;
  color: var(--label);
  margin-bottom: var(--sp-group);
}

/* ── MOBILE REPAIR ────────────────────────────────────────────────────
   The hero rebuild introduced desktop insets (--rail 12rem, --copy-x 16rem,
   a 12-column grid placement, a right-hand hairline) as UNSCOPED rules. They
   exist to clear the deck's section-index rail, which only renders on wide
   viewports — so on a 390px phone they were pure damage: padding-left resolved
   to 216px and squeezed the copy column to 176px, with the name lockup
   overhanging it. Desktop is deliberately untouched; everything below only
   applies under the 1100px breakpoint where the rail does not exist. */
@media (max-width: 1099.98px) {
  html[data-variant="kinetic"] {
    --rail: 0rem;                 /* no section-index rail to clear */
    --copy-x: 0rem;
    --gutter-r: var(--gutter);    /* symmetric gutters on a phone */
  }
  html[data-variant="kinetic"] .hero-inner {
    display: block;               /* the 12-col placement is desktop-only */
    padding: var(--gutter);
    padding-bottom: calc(var(--gutter) * 1.5);
  }
  html[data-variant="kinetic"] .hero-copy {
    display: block;
    grid-column: auto; grid-row: auto;
    border-right: 0;              /* the column hairline is a desktop device */
    padding-right: 0;
    filter: none;                 /* drop-shadow scrim costs paint on mobile */
  }
  html[data-variant="kinetic"] .hero-copy::after { content: none; }

  /* the invocation returns to the top of the flow rather than being parked in a
     right-hand column that does not exist at this width */
  html[data-variant="kinetic"] .hero-invocation {
    grid-column: auto; grid-row: auto;
    justify-self: start; justify-items: start;
    text-align: left;
    max-width: none;
    margin: 0 0 var(--sp-group);
  }
  html[data-variant="kinetic"] .hero-invocation .invocation-motif {
    justify-self: start; margin: 0 0 .15rem -1.5px;
  }
  html[data-variant="kinetic"] .shloka-gloss { max-width: 32ch; }

  /* hashtag rejoins the flow instead of being pinned to a masthead row */
  html[data-variant="kinetic"] .hero-tag {
    grid-column: auto; grid-row: auto;
    justify-self: start; margin-top: var(--sp-tight);
  }

  /* the name lockup: still one shared size and a flush-right short name, but
     sized to the phone's measure. js/app/name-fit.js re-fits on resize, so it
     picks this up without a reload. */
  html[data-variant="kinetic"] .hero-names {
    font-size: clamp(2.5rem, 15vw, 4.5rem);
    font-variation-settings: 'wght' 600, 'wdth' 112;   /* 125 is too wide to fit a phone measure */
  }
  html[data-variant="kinetic"] .hero-amp { font-size: .5em; }

  /* the acts: bottom-anchoring and the rail inset are equally desktop-only */
  html.k-deck[data-variant="kinetic"] .band {
    display: block;
    padding-left: var(--gutter);
    padding-right: var(--gutter);
    padding-bottom: 7rem;         /* still clears the Back/Next chrome */
  }
  html.k-deck[data-variant="kinetic"] .events .event-grid { max-width: none; }
}

@media (max-width: 1099.98px) {
  /* The deck's Back/Next chrome is revealed by pointermove, which a touch screen
     never sends: verified sitting at opacity 0 until the guest happens to tap.
     So on a phone the deck's only navigation was invisible, and the round trip
     from the hero to the countdown and back had no visible return control.
     Touch gets it permanently. */
  html.k-deck[data-variant="kinetic"] #k-panel-controls {
    opacity: 1 !important;
    pointer-events: auto !important;
  }
  /* and it lands in the thumb arc, clear of the home indicator */
  html.k-deck[data-variant="kinetic"] #k-panel-controls {
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }

  /* Panels were top-anchored, leaving 24-36% dead at the bottom of a FIXED
     panel — no scroll exists to justify a hanging bottom, so it read as
     unfinished and pushed the content up out of thumb reach. */
  html.k-deck[data-variant="kinetic"] .band,
  html[data-variant="kinetic"] .hero {
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding-block: 8svh 13svh;      /* slight top bias = optical centring */
  }
  /* the run of show is the one act that can exceed a panel; let it scroll
     inside itself and ANNOUNCE the overflow rather than implying a cut row */
  html.k-deck[data-variant="kinetic"] .events .event-grid {
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 40px), transparent);
            mask-image: linear-gradient(to bottom, #000 calc(100% - 40px), transparent);
  }
  html.k-deck[data-variant="kinetic"] .events .event-card:last-child { border-bottom: 0; }

  /* mono kickers kept tracking tuned for a 1200px measure and wrapped to a
     single orphan word */
  html[data-variant="kinetic"] .kicker { letter-spacing: .16em; text-wrap: balance; }
}

@media (max-width: 1099.98px) {
  /* The deck controls were being given a desktop right-offset
     (`right: calc(var(--gutter-r) + 2.5rem)`) that I left unscoped, so on a
     phone the bar floated inset from the right edge with the page showing
     through beside it. On mobile it is not a floating cluster, it is a bar:
     full-bleed, pinned to the bottom, with its own ground so the content
     scrolling under it stays legible. */
  html.k-deck[data-variant="kinetic"] #k-panel-controls {
    left: 0; right: 0; bottom: 0;
    width: 100%;
    justify-content: space-between;
    gap: 0;
    padding: .75rem clamp(1rem, 4vw, 1.5rem);
    padding-bottom: calc(.75rem + env(safe-area-inset-bottom, 0px));
    background: rgba(6, 8, 12, .82);
    -webkit-backdrop-filter: blur(12px);
            backdrop-filter: blur(12px);
    border-top: 1px solid rgba(240, 228, 214, .10);
    border-radius: 0;
    box-shadow: none;
  }
  /* the corner brackets are a desktop frame device; at this width they collide
     with the bar's own top border */
  html[data-variant="kinetic"] .k-hud-bl,
  html[data-variant="kinetic"] .k-hud-br { display: none; }
}

@media (max-width: 1099.98px) {
  /* BACK kept the desktop button box while NEXT had already had its border
     stripped, so the two halves of one bar disagreed. Match them. */
  html.k-deck[data-variant="kinetic"] #k-panel-controls .k-wz-btn {
    border: 0;
    background: none;
    box-shadow: none;
    padding-inline: .25rem;
    min-height: 48px;
  }
}

@media (max-width: 1099.98px) {
  /* Hold-to-decrypt sat at exactly 44px — the accessibility FLOOR, not a target,
     and this is the deck's only sustained-press interaction, which is far more
     error-prone one-handed than a tap. */
  html[data-variant="kinetic"] .kdc-hold,
  html[data-variant="kinetic"] [data-kdc-hold] { min-height: 56px; }

  /* the end-of-deck NEXT was disabled by opacity alone, so it still looked
     tappable and the dead tap was unexplained */
  html.k-deck[data-variant="kinetic"] #k-next[disabled],
  html.k-deck[data-variant="kinetic"] #k-next[aria-disabled="true"] {
    opacity: .28;
    pointer-events: none;
  }

  /* the ampersand was optically stranded in the three-line stack at this
     measure: desktop's wider column earns the gap, 390px does not */
  html[data-variant="kinetic"] .hero-amp { margin-block: -.18em; }

  /* the hero column is taller than the others, so centring left ~300px under
     the CTA before the bar */
  html[data-variant="kinetic"] .hero { padding-block: 6svh 11svh; }
}

@media (max-width: 1099.98px) {
  /* Corrected selector: the hold control is .kdc-key (my earlier rule guessed
     .kdc-hold and silently did nothing — measured still 44px). 44 is the
     accessibility FLOOR and this is a sustained press, the least forgiving
     gesture one-handed. */
  html[data-variant="kinetic"] .kdc-key { min-height: 56px; }

  /* The hero's primary call to action measured 297x19 — a text link with no
     touch padding, less than half the 44px minimum, and it is the one control
     the whole hero funnels into. Give it a real target without turning it into
     a button: the underline stays, the hit area grows. */
  html[data-variant="kinetic"] .hero-await-cta {
    display: inline-flex;
    align-items: center;
    min-height: 48px;
    padding-block: .5rem;
  }
  html[data-variant="kinetic"] .hero-await-cta::after { bottom: 10px; }
}

@media (max-width: 1099.98px) {
  /* CONTRAST GUARD. #milkdrop screen-blends the music visualiser over the whole
     page and floods body copy to roughly 1.2:1 on a loud passage — measured, and
     confirmed on acts 01/03/05. On desktop the wash is diluted by a much larger
     dark field; a 390px screen has nowhere to hide it.

     The LEVEL is bounded in js/app/milkdrop.js (MAX_OPACITY), not here. An
     `opacity: N !important` rule cannot cap an inline value that is rewritten
     every frame — it PINS it, so the layer sits at N even through the quiet
     passages where the visualiser should be at zero, and the guard becomes a
     permanent wash instead of a ceiling. That is exactly what a previous
     revision of this block did. Bound it where it is computed; only the colour
     treatment belongs here. */
  html[data-variant="kinetic"] #milkdrop {
    filter: saturate(0.30) hue-rotate(158deg) brightness(0.22) contrast(1.02) blur(18px);
  }
}

@media (max-width: 1099.98px) {
  /* The 4-up row existed only above 900px, so mobile fell back to a stacked
     ladder: four units down the page, ~350px of an 844px screen, and the
     countdown lost the horizontal DD:HH:MM:SS silhouette that is the whole
     reason a guest recognises it as a clock. That silhouette matters MORE when
     the values are masked, because the shape is carrying the meaning the digits
     cannot. */
  html.k-deck[data-panel="countdown"] .count-grid,
  html[data-variant="kinetic"] .countdown .count-grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: .5rem;
    margin: 1.5rem 0 2rem;
  }
  html[data-variant="kinetic"] .countdown .count-cell {
    display: grid;
    justify-items: center;
    gap: .35rem;
    padding: .6rem .1rem 0;
    background: none;
    /* the per-cell cyan rule produced one hairline above DAYS attached to
       nothing, none under SECONDS, and spent the deck's single accent on
       separators. One quiet hairline under the whole row instead. */
    border: 0;
    border-top: 0;
  }
  html[data-variant="kinetic"] .countdown .count-grid {
    border-bottom: 1px solid rgba(255, 255, 255, .08);
    padding-bottom: 1rem;
  }
  /* the mask must sit in the digits' own footprint */
  html[data-variant="kinetic"] .countdown .count-num {
    font-family: var(--font-mono);
    font-size: clamp(1.6rem, 8.5vw, 2.4rem);
    line-height: 1;
    font-variant-numeric: tabular-nums;
    letter-spacing: .02em;
    text-shadow: none;
    transition: opacity .6s var(--ease-luxe);
  }
  html[data-variant="kinetic"] .countdown .count-label {
    font-size: .58rem;
    letter-spacing: .18em;
    opacity: .55;
  }
}

@media (max-width: 1099.98px) {
  /* Panels were centred in the FULL viewport while a ~95px nav bar occupies the
     bottom, so every act's optical centre sat above the true centre of the
     visible area and left a 200-300px dead band just above the bar. Centre in
     the viewport MINUS the bar instead. */
  html.k-deck[data-variant="kinetic"] .band,
  html[data-variant="kinetic"] .hero {
    padding-block: 5svh calc(95px + 3svh);
  }
  /* the eclipse must never be able to paint over a kicker or headline */
  html[data-variant="kinetic"] #lightshow { z-index: 0; }
  html[data-variant="kinetic"] .hero-inner,
  html.k-deck[data-variant="kinetic"] .band > .wrap { position: relative; z-index: 2; }
}

/* [hidden] must win. The build gates this line with the `hidden` attribute when
   revealDate flips, but `display: flex` above silently overrode it, so the
   revealed build showed the real dates AND "DATES AND VENUES TO BE REVEALED"
   on the same panel. Any element given a display value must restore this, or
   the attribute is decorative. */
html[data-variant="kinetic"] [hidden] { display: none !important; }

/* Run of show, REVEALED state. Adding a date, venue and two action buttons to
   every row roughly doubles the block, and this is a fixed-panel deck with no
   scroll — a reviewer found the headline clipped off the top edge and the last
   event's venue and buttons cut by the nav bar, i.e. genuinely unreachable
   content. The list becomes its own scroll region, with a gradient mask so the
   overflow is announced rather than looking severed. */
html.k-deck[data-variant="kinetic"] .events .event-grid {
  max-height: calc(100svh - 22rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 44px), transparent);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 44px), transparent);
  padding-bottom: 1.5rem;
}
/* compact the revealed row so it needs the scroll less often */
html.k-deck[data-variant="kinetic"] .events .event-when,
html.k-deck[data-variant="kinetic"] .events .event-venue { line-height: 1.35; }
html.k-deck[data-variant="kinetic"] .events .event-actions { gap: .75rem; margin-top: .5rem; }

/* ═══════ RUN OF SHOW: up to six rows (prelude + five) ═══════
   The list is a flex column inside a height-capped panel, so with six events
   every row SHRANK and the card's overflow:hidden cut it to motif + number.
   Rows now keep their natural height and the list scrolls inside the panel
   (mobile already did; desktop gets the same fade-out cue). */
html.k-deck[data-panel="run-of-show"] .event-card { flex-shrink: 0; overflow: visible; }
/* the motif moves into the number gutter so it stops costing a row of height */
html.k-deck[data-panel="run-of-show"] .event-no { grid-row: 1; }
html.k-deck[data-panel="run-of-show"] .event-motif {
  grid-column: 1; grid-row: 2 / span 7; align-self: start;
  width: 30px; height: 30px; margin: .5rem 0 0; opacity: .85;
}
/* prelude: "Evening of devotion" is too long for the 4rem number gutter, so it
   sits above the name as an eyebrow and the motif takes the gutter alone */
html.k-deck[data-panel="run-of-show"] .event-card--prelude .event-no {
  grid-column: 2; grid-row: auto; margin: 0 0 .2rem; white-space: nowrap;
}
html.k-deck[data-panel="run-of-show"] .event-card--prelude .event-motif { grid-row: 1 / span 8; margin-top: .1rem; }
html.k-deck[data-panel="run-of-show"] .event-jai,
html.k-deck[data-panel="run-of-show"] .event-note,
html.k-deck[data-panel="run-of-show"] .event-addr { grid-column: 2; }
html.k-deck[data-panel="run-of-show"] .event-addr { margin: 0 0 .12rem; }
/* the band-wide 46ch prose measure was wrapping the one-line date row */
html.k-deck[data-variant="kinetic"][data-panel="run-of-show"] .band p.event-when:not(.kicker) { max-inline-size: none; }
@media (min-width: 900px) {
  html.k-deck[data-variant="kinetic"][data-panel="run-of-show"] .events .event-grid {
    max-height: calc(100svh - 19rem);  /* header + deck chrome; the panel centres, so a taller list pushes the heading off-frame */
    overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin;
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 48px), transparent);
            mask-image: linear-gradient(to bottom, #000 calc(100% - 48px), transparent);
  }
  /* actions move to a right-hand column of equal-width buttons, top-aligned
     with the event name: ~50px less height per row, one clean right edge */
  html.k-deck[data-variant="kinetic"][data-panel="run-of-show"] .events .event-card {
    grid-template-columns: 4rem minmax(0, 1fr) 12.5rem; padding-block: .85rem;
  }
  html.k-deck[data-variant="kinetic"][data-panel="run-of-show"] .event-actions {
    grid-column: 3; grid-row: 1 / span 8; align-self: start; margin: .15rem 0 0;
    flex-direction: column; align-items: stretch; gap: .5rem;
  }
  html.k-deck[data-variant="kinetic"][data-panel="run-of-show"] .event-actions .btn-quiet { justify-content: center; }
  /* dress code reads as one labelled line (label inline), as on mobile, so the
     label no longer hugs the venue above it and each row loses a line */
  html.k-deck[data-panel="run-of-show"] .event-dress span { display: inline; margin-right: .7em; }
  /* the number and the motif stack tightly in the gutter in every row */
  html.k-deck[data-panel="run-of-show"] .event-motif { grid-row: 1 / span 8; margin-top: 1.5rem; }
  html.k-deck[data-panel="run-of-show"] .event-card--prelude .event-motif { margin-top: .1rem; }
}
@media (max-width: 720px) {
  /* cards are display:block here: the motif floats to the row's top-right */
  html.k-deck[data-panel="run-of-show"] .event-motif { float: right; width: 26px; height: 26px; margin: 0 0 .3rem .6rem; }
  html.k-deck[data-panel="run-of-show"] .event-card--prelude .event-no { white-space: normal; }
  html.k-deck[data-panel="run-of-show"] .event-jai { font-size: .9rem; }
  html.k-deck[data-panel="run-of-show"] .event-note,
  html.k-deck[data-panel="run-of-show"] .event-addr { font-size: .72rem; }
  /* two buttons share a 390px row: the icons pushed "ADD TO CALENDAR" past
     the border, and the label alone is unambiguous */
  html.k-deck[data-panel="run-of-show"] .btn-quiet { justify-content: center; }
  html.k-deck[data-panel="run-of-show"] .btn-quiet .ev-ic { display: none; }
  /* date / time on their own lines: "6 o'clock in / the evening" broke mid-phrase */
  html.k-deck[data-panel="run-of-show"] .event-when .ev-t { display: block; }
  html.k-deck[data-panel="run-of-show"] .event-when .ev-t::before { content: none; }
}
