/* ============================================================
   THE NEXT GEN — design tokens
   Clone target: tokyofilmfestival.ai (Framer + UnicornStudio.js)
   Fonts substituted for free equivalents — see BUILD_NOTES.md
   ============================================================ */
:root{
  --cream: #f2efe6;
  --cream-line: rgba(26,21,18,0.14);
  --cream-line-inv: rgba(242,239,230,0.09); /* grid line on dark sections */
  --grid: 48px;                              /* graph-paper cell, page-wide */
  --frame: 6.25%;                            /* cream side-frame, matches the gate inset */
  --veil: rgba(242,239,230,0.72);            /* cream backing behind copy sections */
  --ink: #1a1512;          /* warm near-black */
  --ink-soft: #4a4038;     /* warm grey */
  --navy: #060b12;
  --navy-2: #0c1622;
  /* Super San Francisco — International Orange primary, Golden Hour secondary */
  --green-deep: #7a1f14;   /* deep burnt orange */
  --green: #c0362c;        /* International Orange — primary accent */
  --green-light: #e7a33c;  /* Golden Hour — secondary accent */
  --teal: #c0362c;         /* kicker accent on the dark photos */
  --gold: #e0b84c;
  /* Logo mark — sampled from the source artwork, so deliberately not reusing --green/
     --green-light/--ink above. Those are the palette's interpretation of the same two
     hues and drift a few points off; the mark should match the supplied file exactly. */
  --mark-gold: #e3ae28;
  --mark-red: #da4433;
  --mark-ground: #0c0d0f;
  --blue: #5b7fd6;
  --radius: 18px;
  --ease: cubic-bezier(.16,.84,.32,1);
  /* Display: Clash Display — editorial grotesque with real character (max weight 700).
     Script: DM Serif Display italic — high-contrast editorial serif (single weight, 400).
     Body + Mono: General Sans and Geist Mono, the actual families the reference site uses. */
  --font-display: "Archivo", "Archivo Black", sans-serif;
  --font-serif: "STIX Two Text", "Times New Roman", serif;  /* hero lockup */
  /* Gate lockup only — the wordmark moment. Boska Black is the closest free face to
     the Gage EXO reference: same high-contrast display serif, dramatic thick/thin,
     flared terminals. Free from Fontshare, which the page already preconnects for
     General Sans, so it costs no extra provider.
     Falls back to DM Serif Display rather than a sans — a high-contrast serif
     degrading to a grotesque changes the lockup's character completely. */
  --font-gate: "Boska", "DM Serif Display", Georgia, serif;
  --font-script: "DM Serif Display", Georgia, serif;
  --font-mono: "Geist Mono", ui-monospace, monospace;
  --font-body: "General Sans", "Inter", sans-serif;
}

*{box-sizing:border-box; margin:0; padding:0;}
html{
  scroll-behavior:smooth;
  /* Warm "golden-hour paper" — not flat cream: a soft gold glow up top easing into
     a deeper warm sand, with a faint ember pool at the base.
     background-color first and separately: it is what paints if the gradient or the
     shader canvas is mid-repaint, and without it that gap falls through to the
     browser's white.
     No background-attachment:fixed — mobile engines re-rasterise a fixed background
     on every scroll frame and iOS Safari has never handled it properly. It was the
     main source of the white flashing while scrolling on a phone. */
  background-color:#f0e9db;
  background-image:
    radial-gradient(150% 70% at 50% -18%, rgba(231,163,60,0.16), rgba(231,163,60,0) 55%),
    radial-gradient(130% 80% at 50% 118%, rgba(122,31,20,0.07), rgba(122,31,20,0) 55%),
    linear-gradient(180deg, #f5f0e4 0%, #efe6d4 45%, #e8ddc9 100%);
}
/* NOT on body. #bgShader sits at z-index:-2, which paints above the root background
   but below every block background — so an opaque body covers the shader completely.
   The root background-color above is the right layer for this: it is behind the
   canvas, so it catches a mid-repaint gap without hiding the animation. */
body{
  color:var(--ink);
  font-family:var(--font-body);
  overflow-x:hidden;
  position:relative;
  /* Same cream side-frame as the gate/lander (6.25% L/R, flush top/bottom). */
  padding:0 var(--frame);
}
/* Margin rules where the frame meets the content — a red rope rather than a hairline,
   running the full height of the page on both sides to tie the sections together.
   assets/rope.png is a mirror tile: the source rope stacked against a vertically
   flipped copy of itself, so the seam matches by construction (the flipped copy's
   first row is the original's last). One row is dropped from each half so that shared
   row isn't drawn twice. The twist reverses at every join, which is what makes it
   read as one continuous length instead of a repeating stamp.
   Exported at 48px wide and drawn at 14 so it stays sharp on retina. The negative
   margins centre the rope on the frame line rather than hanging it off one side. */
body::before, body::after{
  content:""; position:absolute; top:0; bottom:0; width:14px;
  background:url("assets/rope.png") repeat-y center top / 14px auto;
  pointer-events:none; z-index:1;
}
body::before{ left:var(--frame); margin-left:-7px; }
body::after{ right:var(--frame); margin-right:-7px; }

/* Animated WebGL golden-hour background behind the whole page (assets/shader-bg.js).
   The gradient is repeated here, on the canvas itself, as the no-WebGL fallback —
   without it a failed context would leave the canvas transparent, which matters now
   that the gate leans on this element rather than painting its own background. */
#bgShader{
  position:fixed; inset:0; width:100%; z-index:-2; display:block; pointer-events:none;
  /* lvh = the viewport at its tallest, i.e. URL bar hidden. height:100% tracks the
     *current* viewport, so the canvas shrank and grew as the bar moved and left a
     bare strip at the bottom mid-scroll. lvh is a fixed target, so it never resizes. */
  height:100%; height:100lvh;
  background:
    radial-gradient(150% 70% at 50% -18%, rgba(231,163,60,0.16), rgba(231,163,60,0) 55%),
    radial-gradient(130% 80% at 50% 118%, rgba(122,31,20,0.07), rgba(122,31,20,0) 55%),
    linear-gradient(180deg, #f5f0e4 0%, #efe6d4 45%, #e8ddc9 100%);
}
/* While the gate is up it sits above the whole page at z-index 9997, so a shader
   parked at -2 would be invisible behind it. Lift it above the page content but
   below the gate, and the gate's side gutters show the same live background the
   rest of the site has instead of flat cream. script.js owns the .gated class.
   Dropping the class on enter is seamless: the canvas returns to -2, where the
   transparent body still lets it show through the same 6.25% gutters. */
html.gated #bgShader{ z-index:9996; }

/* ---------------- cream veil, per-section ----------------
   The veil used to be one fixed sheet across the whole viewport, which washed the
   shader out everywhere. It now backs only the copy-heavy sections, so the live
   shader reads through in three places:
     - the side gutters (these rules sit inside body's --frame padding, so they
       stop at the content column and never paint the frame)
     - the card sections — .hosts, .speakers, .videos — where the photos and
       embeds already carry their own contrast against the shader
     - the sections that own an opaque background already (.schedule, .footer) or
       a full-bleed photo (.hero, .where)
   --veil is the single alpha knob; raise it if copy ever reads thin over the
   brighter parts of the shader. */
.statement, .marquee-wrap, .about, .build{ background:var(--veil); }
@media (max-width:860px){ body{ --frame:4%; } }
/* Phones: the stacked gap is proportionally much larger on a short viewport,
   so close it further — 34+34 becomes 14+16. */
@media (max-width:640px){
  .marquee-wrap{ padding-bottom:14px; }
  .about{ padding-top:16px; }
  .marquee-label{ margin-bottom:14px; }
}
a{color:inherit; text-decoration:none;}
ul{list-style:none;}
button{font:inherit; background:none; border:none; color:inherit; cursor:pointer;}
em{font-style:italic; font-family:var(--font-script);}
img{max-width:100%; display:block;}
::selection{background:var(--green-light); color:var(--ink);}

/* ---------------- grain overlay ---------------- */
.grain{
  position:fixed; inset:-10%; width:120%; height:120%; z-index:9998;
  pointer-events:none; mix-blend-mode:multiply; opacity:.05;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  animation:grain-shift 8s steps(8) infinite;
}
/* Off on touch devices: a fixed full-viewport layer with mix-blend-mode has to be
   recomposited on every scroll frame, and at 5% opacity the grain is barely
   perceptible on a phone screen anyway. */
@media (hover: none), (max-width:640px){ .grain{ display:none; } }
@keyframes grain-shift{
  0%,100%{transform:translate(0,0);} 10%{transform:translate(-2%,-3%);}
  20%{transform:translate(-4%,2%);} 30%{transform:translate(2%,-4%);}
  40%{transform:translate(-2%,4%);} 50%{transform:translate(4%,1%);}
  60%{transform:translate(-3%,-2%);} 70%{transform:translate(3%,3%);}
  80%{transform:translate(-1%,-4%);} 90%{transform:translate(1%,2%);}
}

/* ---------------- cursor spotlight ----------------
   Scoped to the hero. Absolute, not fixed, so .hero's own overflow:hidden clips it —
   the glow stops dead at the photo's edge with no extra masking.
   soft-light rather than screen: the photo runs from bright sky to near-black, and
   screen blows the light areas out while barely touching the dark.
   z-index 1 puts it over the photo but under the hero copy at 2. Coordinates are
   hero-relative, written by script.js.
   Fade out is quicker than fade in — leaving the photo should read as switching off,
   while arriving should read as the light rising. */
.spotlight{
  position:absolute; inset:0; z-index:1; pointer-events:none;
  mix-blend-mode:soft-light; opacity:0; transition:opacity .18s linear;
  background:radial-gradient(circle 280px at var(--sx,50%) var(--sy,50%),
    rgba(255,216,150,.95) 0%,
    rgba(255,188,108,.42) 40%,
    rgba(255,170,90,0) 72%);
}
html.pointer-live .spotlight{ opacity:1; transition:opacity .45s var(--ease); }
@media (prefers-reduced-motion: reduce){ .spotlight{ display:none; } }
@media (prefers-reduced-motion: reduce){ .spotlight{ display:none; } }


/* ---------------- ticket button (shared) ---------------- */
.ticket-btn{
  position:relative; display:inline-flex; align-items:stretch;
  background:var(--cream); border:1px solid var(--ink); border-radius:8px;
  font-family:var(--font-mono); font-size:.8rem; letter-spacing:.03em;
  text-transform:uppercase; overflow:hidden;
  transition:transform .35s var(--ease), background .3s, color .3s;
}
.ticket-btn > span:first-child{ padding:15px 26px; display:flex; align-items:center; }
.ticket-btn .arrow{
  padding:15px 20px; display:flex; align-items:center; font-size:1rem;
  border-left:1px dashed var(--ink); transition:transform .35s var(--ease);
}
/* ticket edge notches */
.ticket-btn::before, .ticket-btn::after{
  content:""; position:absolute; right:60px; width:12px; height:12px; border-radius:50%;
  background:var(--cream); border:1px solid var(--ink); z-index:2;
}
.ticket-btn::before{ top:-7px; }
.ticket-btn::after{ bottom:-7px; }
.ticket-btn:hover{ transform:translateY(-3px); background:var(--ink); color:var(--cream); }
.ticket-btn:hover .arrow{ transform:translateX(3px); }
.ticket-btn.light{ background:transparent; border-color:rgba(255,255,255,.6); color:#fff; }
.ticket-btn.light .arrow{ border-left-color:rgba(255,255,255,.6); }
.ticket-btn.light::before, .ticket-btn.light::after{ background:var(--ink); border-color:rgba(255,255,255,.6); }
.ticket-btn.light:hover{ background:#fff; color:var(--ink); }

/* ---------------- reveal-on-scroll ---------------- */
.reveal{ opacity:0; transform:translateY(28px); transition:opacity .9s var(--ease), transform .9s var(--ease); }
.reveal.in{ opacity:1; transform:translateY(0); }
/* Phones: 28px of travel is a far bigger share of a short viewport than of a desktop
   one, so the same animation reads as a lurch rather than a lift. Shorter distance
   and a slightly quicker settle — and will-change promotes each element before it
   moves, which is what stops the first frame of the transform from stuttering.
   The property list is restated rather than using transition-duration alone so the
   easing stays attached to both animated properties. */
@media (max-width:640px){
  .reveal{
    transform:translateY(14px);
    transition:opacity .62s var(--ease), transform .62s var(--ease);
    will-change:opacity, transform;
  }
  /* drop the hint once it has arrived — a permanent will-change keeps a layer alive
     for every revealed element on the page */
  .reveal.in{ will-change:auto; }
  .reveal-line span{ transition-duration:.8s; }
}

/* Safety nets — content must never be stranded at opacity:0.
   `.motion-ready` is set by script.js; without JS (or before it runs) nothing is hidden. */
html:not(.motion-ready) .reveal,
html:not(.motion-ready) .reveal-line span{ opacity:1; transform:none; }

@media (prefers-reduced-motion: reduce){
  .reveal, .reveal-line span{ opacity:1 !important; transform:none !important; transition:none !important; }
  /* .gate-img is already the sharp photo, so reduced motion just needs the drifting
     smoke gone and the settle scale held still. */
  .gate-smoke-field{ display:none !important; }
  .gate-img{ transform:none !important; animation:none !important; }
  .marquee-track, .grain, .scroll-hint span{ animation:none !important; }
  html{ scroll-behavior:auto !important; }
}
.reveal-line{ display:block; overflow:hidden; padding-bottom:.08em; margin-bottom:-.08em; }
.reveal-line span{ display:block; line-height:1; padding-top:.06em; transform:translateY(115%); transition:transform 1.1s var(--ease); }
.reveal-line.in span{ transform:translateY(0); }

/* ============================================================ GATE */
/* The gate is the cinematic moment: the bridge photo behind a sheet of smoke that
   the cursor carves through. Framed like the reference site — inset in the side
   gutters rather than full-bleed. */
.gate{
  /* transparent, not cream — the lifted #bgShader behind it supplies the same
     background the main page uses. Any opaque colour here would hide it. */
  position:fixed; inset:0; z-index:9997; background:transparent; color:#fff;
  display:flex; align-items:center; justify-content:center; flex-direction:column;
  transition:transform 1.1s var(--ease), opacity .8s var(--ease) .25s;
  gap:26px; text-align:center; overflow:hidden;
  padding:32px 20px;
}
.gate.closed{ transform:translateY(-100%); opacity:0; pointer-events:none; }

/* Inset left/right, flush to the top and bottom edges — no top band. Since the image
   spans the full height, centring the content in .gate also centres it in the image. */
.gate-bg{ position:absolute; top:0; bottom:0; left:6.25%; right:6.25%; z-index:0; overflow:hidden; }
@media (max-width:860px){
  .gate-bg{ left:4%; right:4%; }
}
/* The photo sits sharp from the start — the haze is a real smoke layer in front of
   it rather than a pre-blurred copy of it underneath. */
.gate-img{
  position:absolute; inset:0;
  background-color:#17110c;
  background-image:
    url("assets/gate-bg.jpg"),
    linear-gradient(180deg,#3a2113 0%,#1e130b 45%,#0c0703 100%);
  background-size:cover, cover;
  background-position:center, center;
  transform:scale(1.16);
  animation:fog-clear-img 4.6s var(--ease) forwards .3s;
}

/* ---- smoke ----
   Three drifting sheets of fractal noise, blended to lighten. script.js carves the
   pointer's wake out of them by writing mask layers onto the field below.

   The whole field is masked as one, and it is the field that blends with the photo.
   The blend has to live here rather than on each sheet: a mask creates a stacking
   context, so a screen blend on a child would blend against its siblings inside this
   box instead of against the bridge underneath.
   No mask by default — that is the resting state now. With the pointer still, the
   smoke is solid, which doubles as the no-JS fallback. */
.gate-smoke-field{
  position:absolute; inset:0; overflow:hidden; pointer-events:none;
  mix-blend-mode:screen; will-change:mask-image;
}
.gate-smoke{ position:absolute; inset:0; overflow:hidden; }
.gate-smoke-inner{
  position:absolute; inset:-30%;
  background-repeat:repeat; background-position:center;
}
/* Three scales and three speeds, so the sheets slide past each other instead of
   moving as one flat texture. The blur is what turns noise into smoke. */
.s1 .gate-smoke-inner{
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='600' height='600'><filter id='s'><feTurbulence type='fractalNoise' baseFrequency='0.01' numOctaves='2' seed='11'/><feColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 0.93 0 0 0 0 0.86 1.25 0 0 0 -0.46'/></filter><rect width='100%25' height='100%25' filter='url(%23s)'/></svg>");
  background-size:150% 150%; filter:blur(5px); opacity:.62;
  animation:smoke-a 46s ease-in-out infinite alternate;
}
.s2 .gate-smoke-inner{
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='520' height='520'><filter id='s'><feTurbulence type='fractalNoise' baseFrequency='0.016' numOctaves='2' seed='29'/><feColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 0.93 0 0 0 0 0.86 1.25 0 0 0 -0.46'/></filter><rect width='100%25' height='100%25' filter='url(%23s)'/></svg>");
  background-size:110% 110%; filter:blur(4px); opacity:.5;
  animation:smoke-b 34s ease-in-out infinite alternate;
}
.s3 .gate-smoke-inner{
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='460' height='460'><filter id='s'><feTurbulence type='fractalNoise' baseFrequency='0.024' numOctaves='2' seed='47'/><feColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 0.93 0 0 0 0 0.86 1.25 0 0 0 -0.46'/></filter><rect width='100%25' height='100%25' filter='url(%23s)'/></svg>");
  background-size:80% 80%; filter:blur(3px); opacity:.36;
  animation:smoke-c 27s ease-in-out infinite alternate;
}
@keyframes smoke-a{
  from{ transform:translate3d(-4%,2%,0) scale(1.05); }
  to  { transform:translate3d(5%,-3%,0) scale(1.18); }
}
@keyframes smoke-b{
  from{ transform:translate3d(3%,-2%,0) scale(1.12); }
  to  { transform:translate3d(-5%,3%,0) scale(1.02); }
}
@keyframes smoke-c{
  from{ transform:translate3d(-2%,-3%,0) scale(1.0); }
  to  { transform:translate3d(4%,4%,0) scale(1.14); }
}

/* Edge vignette only — deliberately NOT a central pool.
   This layer sits above the reveal, so anything it darkens in the middle
   compresses the fogged and revealed states toward the same value and kills the
   effect. Type legibility is handled by text-shadow on .gate-content instead. */
.gate-vignette{
  position:absolute; inset:0;
  background:
    radial-gradient(ellipse 70% 58% at 50% 50%, rgba(20,11,5,.42) 0%, rgba(20,11,5,.12) 55%, rgba(20,11,5,0) 82%),
    linear-gradient(180deg,
      rgba(20,11,5,.34) 0%, rgba(20,11,5,.06) 26%, rgba(20,11,5,.18) 60%, rgba(20,11,5,.72) 100%);
}

/* Legibility lives here rather than in a background scrim, so the fog wipe
   behind it keeps its full contrast. The wide soft shadow reads as the type
   pushing the fog away from itself. */
/* Kept tight — wide soft shadows on heavy display type merge into a visible
   dark rectangle behind the words. The base plate is dark enough to carry it. */
/* Same rope as the page margins. The body's own pair sits at z-index 1, far below the
   gate at 9997, so it is invisible while the gate is up — the gate needs its own.
   Both use var(--frame), which body drops to 4% under 860px, so these stay lined up
   with .gate-bg's inset and with the ropes waiting on the page behind.
   z-index 2 puts them over the photo but under .gate-content at 3. */
.gate::before, .gate::after{
  content:""; position:absolute; top:0; bottom:0; width:14px; z-index:2;
  background:url("assets/rope.png") repeat-y center top / 14px auto;
  pointer-events:none;
}
.gate::before{ left:var(--frame); margin-left:-7px; }
.gate::after{ right:var(--frame); margin-right:-7px; }

.gate-content{
  position:relative; z-index:3; display:flex; flex-direction:column; align-items:center; gap:24px;
}
/* White, not --teal: that accent is International Orange, which sits on the dark
   photo at almost the same luminance as the smoke behind it and disappears. */
.gate-kicker{ font-family:var(--font-mono); font-size:.76rem; letter-spacing:.14em; color:#fff; }
/* One line now, so the size has to fall back further on narrow screens than the old
   two-line lockup needed — 6.4vw keeps "The NextGen" inside a 360px viewport. */
/* Set as a wordmark: 900 is Boska's heaviest cut, and the tight tracking locks the
   letters into one mark rather than a sentence. Only -.025em though, not the -.045em
   a grotesque wanted — a high-contrast serif has hairline strokes and serifs that
   collide and smudge together when tracked that tightly.
   No text-shadow: the mark carries itself against the photo. The stroke below is what
   keeps the hairlines from dropping out, so if the lockup ever reads thin over a
   bright patch of the image, raise that rather than reintroducing a shadow. */
.gate-word{
  font-family:var(--font-gate); font-weight:900;
  font-size:clamp(2.1rem,7.2vw,5.8rem); line-height:1.02; letter-spacing:-.025em;
  white-space:nowrap; text-shadow:none;
  /* Boska is a variable font topping out at 900, so this is already its heaviest
     cut — extra weight has to come from a stroke, not from font-weight. The stroke
     is centred on the outline, so it thickens stems from both sides.
     0.03em is deliberate: past roughly 0.05em the hairlines fatten up to meet the
     stems and the thick/thin contrast that makes this face worth using collapses. */
  -webkit-text-stroke:0.03em currentColor;
}
.gate-sub{ font-family:var(--font-mono); font-size:.8rem; letter-spacing:.08em; color:#fff; }
.gate-sub p{ margin:2px 0; }

/* the button sits on the photo, so it needs a little glass behind it and
   notches dark enough to read as punched-out rather than as white dots */
.gate .ticket-btn{ background:rgba(6,11,18,.55); backdrop-filter:blur(8px); text-shadow:none; }
.gate .ticket-btn::before, .gate .ticket-btn::after{
  background:rgba(6,11,18,.92); border-color:rgba(255,255,255,.55);
}
.gate .ticket-btn:hover{ background:#fff; }
.gate .ticket-btn:hover::before, .gate .ticket-btn:hover::after{ background:var(--cream); }

/* ============================================================ NAV */
.nav{ position:fixed; top:18px; left:0; right:0; z-index:900; display:flex; justify-content:center; padding:0 16px; }
/* Column, not row: the stub sits on top and the directory unfolds underneath it. */
.nav-ticket{
  display:flex; flex-direction:column; background:var(--cream); border:1px solid var(--ink);
  border-radius:16px; padding:10px 10px 10px 14px; position:relative;
  box-shadow:0 6px 20px rgba(0,0,0,.06);
  transition:box-shadow .35s var(--ease);
}
.nav-ticket.open{ box-shadow:0 14px 38px rgba(0,0,0,.16); }
.nav-row{ display:flex; align-items:center; gap:16px; }
.nav-brand{ display:flex; align-items:center; gap:10px; }
/* The badge is the logo's dark ground, not a container for it: 8/34 lands on the
   same 23.5% corner radius the mark is drawn with, so the two agree by construction. */
.nav-badge{
  width:34px; height:34px; border-radius:8px; background:var(--mark-ground);
  display:flex; align-items:center; justify-content:center;
}
/* width:0/height:0 rather than display:none on the sprite host: a display:none
   ancestor stops <use> resolving in some engines. */
.svg-sprite{ position:absolute; width:0; height:0; overflow:hidden; }
/* Full badge size, not inset: the mark's viewBox is the whole logo square and carries
   its own margins, so anything smaller double-counts the padding. */
.brand-mark{ width:34px; height:34px; display:block; overflow:visible; }
.nav-word{ display:flex; flex-direction:column; line-height:1; font-family:var(--font-display); font-weight:600; font-size:.72rem; letter-spacing:.02em; }
.nav-word .italic{ font-family:var(--font-script); font-style:italic; text-transform:none; font-size:1.05rem; font-weight:400; letter-spacing:0; }
.nav-divider{ width:1px; align-self:stretch; background:repeating-linear-gradient(var(--ink) 0 4px, transparent 4px 8px); opacity:.3; }
.nav-meta{ display:flex; gap:12px; font-family:var(--font-mono); font-size:.68rem; letter-spacing:.06em; color:var(--ink-soft); }
.nav-meta span:not(:last-child)::after{ content:"·"; margin-left:12px; color:var(--cream-line); }

/* hamburger, crossing into an X while the drawer is open */
.hamburger{ width:34px; height:34px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:5px; margin-left:auto; }
.hamburger span{ width:18px; height:2px; background:var(--ink); transition:transform .35s var(--ease), opacity .2s; }
.nav-ticket.open .hamburger span:nth-child(1){ transform:translateY(7px) rotate(45deg); }
.nav-ticket.open .hamburger span:nth-child(2){ opacity:0; }
.nav-ticket.open .hamburger span:nth-child(3){ transform:translateY(-7px) rotate(-45deg); }

/* ---- the drawer ----
   grid-template-rows 0fr -> 1fr animates to the content's natural height. max-height
   would need a magic number large enough for the content, which then makes the
   easing wrong: the transition spends its time closing empty space. The inner
   wrapper needs overflow:hidden and min-height:0 for the 0fr row to actually
   collapse — without min-height:0 a grid item refuses to shrink below its content. */
.nav-drawer{
  display:grid; grid-template-rows:0fr;
  transition:grid-template-rows .42s var(--ease), opacity .28s var(--ease);
  opacity:0;
}
.nav-ticket.open .nav-drawer{ grid-template-rows:1fr; opacity:1; }
.nav-drawer-inner{ overflow:hidden; min-height:0; }

/* tear-off line, with a punched hole at each end */
.nav-perf{
  position:relative; height:1px; margin:12px 0 4px;
  background:repeating-linear-gradient(90deg, var(--ink) 0 5px, transparent 5px 10px);
  opacity:.32;
}
.nav-perf::before, .nav-perf::after{
  content:""; position:absolute; top:-3px; width:7px; height:7px; border-radius:50%;
  background:var(--ink); opacity:.5;
}
.nav-perf::before{ left:-4px; }
.nav-perf::after{ right:-4px; }

.nav-links{ display:flex; flex-direction:column; padding:6px 0 2px; }
.nav-links a{
  display:flex; align-items:baseline; gap:14px;
  font-family:var(--font-display); font-weight:600; font-size:1rem;
  padding:9px 6px; border-radius:8px;
  transition:padding-left .3s var(--ease), background .25s, color .25s;
}
.nav-links a:hover{ padding-left:16px; background:rgba(26,21,18,.05); color:var(--green); }
.nav-links .num{ font-family:var(--font-mono); font-size:.7rem; color:var(--green); }
/* flex-end, not space-between: the "HOSTED BY TKS" line that used to sit opposite
   the toggle is gone, and space-between would strand the button on the left. */
.nav-drawer-foot{
  display:flex; justify-content:flex-end; align-items:center;
  margin-top:6px; padding:10px 6px 2px; border-top:1px solid var(--cream-line);
  font-family:var(--font-mono); font-size:.64rem; letter-spacing:.1em; color:var(--ink-soft);
}
/* live now that assets/theme.mp3 exists — was disabled and dimmed while it did not */
.music-toggle{
  display:flex; align-items:center; gap:8px; cursor:pointer; color:var(--ink-soft);
  transition:color .25s var(--ease);
}
.music-toggle:hover{ color:var(--ink); }
.music-toggle .dot{
  width:6px; height:6px; border-radius:50%; background:currentColor;
  transition:background .25s, box-shadow .25s;
}
/* the dot is the state read at a glance — lit and haloed while the track plays */
.music-toggle.playing{ color:var(--green); }
.music-toggle.playing .dot{ background:var(--green); box-shadow:0 0 0 3px rgba(192,54,44,.18); }

@media (max-width:640px){
  .nav-meta{ display:none; }
  .nav-divider{ display:none; }
}
@media (prefers-reduced-motion: reduce){
  .nav-drawer{ transition:none; }
}

/* ============================================================ HERO */
.hero{
  position:relative; min-height:100svh; display:flex; flex-direction:column;
  justify-content:flex-start; overflow:hidden; color:#fff; padding:118px 6vw 40px;
}
.hero-bg{ position:absolute; inset:0; z-index:0; }
/* The fog already cleared on the gate — the hero shows the settled image. */
.hero-img{
  position:absolute; inset:0;
  background-color:#17110c;
  background-image:
    url("assets/hero-bg.jpg"),
    linear-gradient(180deg,#3a2113 0%,#1e130b 45%,#0c0703 100%);
  background-size:cover, cover;
  background-position:center, center;
  transform:scale(1.04);
}
@keyframes fog-clear-img{ from{ transform:scale(1.22); } to{ transform:scale(1); } }


/* scrim: the fog in the photo is bright, so the white type needs a real gradient behind it */
/* Warm charcoal scrim (matches the golden-hour art) — darkens the top-left + bottom
   for the white lockup and the label, keeps the sky open on the right. */
.hero-vignette{
  position:absolute; inset:0;
  background:
    linear-gradient(180deg, rgba(20,11,5,.60) 0%, rgba(20,11,5,.10) 26%, rgba(20,11,5,.34) 62%, rgba(20,11,5,.90) 100%),
    linear-gradient(90deg, rgba(20,11,5,.74) 0%, rgba(20,11,5,.24) 42%, rgba(20,11,5,0) 70%);
}

.hero-top{ display:flex; justify-content:space-between; align-items:baseline; gap:20px; z-index:2; }
.hero-kicker{ font-family:var(--font-mono); font-size:.78rem; letter-spacing:.14em; color:var(--green); }
.hero-index{ font-family:var(--font-mono); font-size:.72rem; letter-spacing:.14em; color:rgba(255,255,255,.6); }
.hero-headline{ margin:auto 0 34px; z-index:2; }
/* Small mono lockup rather than the big serif display type this used to be — same
   treatment as the minimal hero it came from. No optical kern needed at this size;
   the old -.035em pull on line 2 existed to correct the round serif "G" against the
   "T" above it, and mono has uniform bearings by definition. */
.hero-title{
  display:flex; flex-direction:column; gap:3px;
  font-family:var(--font-mono); font-weight:500;
  font-size:clamp(.8rem,1.55vw,1.375rem); line-height:1.12;
  letter-spacing:-.03em; margin:0; z-index:2;
  color:#f2f4e8; text-shadow:0 1px 9px rgba(12,7,3,.6);
}

/* Editorial metadata bar — hairline top rule, columns split by dividers, and an
   International Orange apply cell (echoes the ticket motif). */
.hero-meta-bar{ display:flex; align-items:stretch; z-index:2; }
.hm-col{
  /* label + value sit against the right edge of each column; the 30px right padding
     is what keeps them off the divider rule. */
  flex:1; display:flex; flex-direction:column; gap:9px;
  align-items:flex-end; text-align:right;
  padding:20px 30px 2px 0;
  border-top:1px solid rgba(255,255,255,.32);   /* rule spans the info columns only */
  border-right:1px solid rgba(255,255,255,.15);
}
.hm-label{ font-family:var(--font-mono); font-size:.64rem; letter-spacing:.16em; text-transform:uppercase; color:rgba(255,255,255,.55); }
.hm-val{ font-family:var(--font-serif); font-weight:400; font-size:1.4rem; letter-spacing:.005em; color:#fff; }
.hm-apply{
  display:flex; align-items:center; gap:12px; align-self:stretch; margin-left:6px;
  padding:0 34px; background:var(--green); color:#fff;
  font-family:var(--font-mono); font-size:.82rem; letter-spacing:.1em; text-transform:uppercase;
  transition:background .2s var(--ease), gap .25s var(--ease);
}
.hm-apply .hm-arrow{ font-size:1.05rem; transition:transform .25s var(--ease); }
.hm-apply:hover{ background:#a52d22; gap:18px; }
.hm-apply:hover .hm-arrow{ transform:translateX(4px); }
@media (max-width:640px){
  .hero-index{ display:none; }
  .hero-meta-bar{ flex-wrap:wrap; }
  .hm-col{ flex:1 1 33%; padding:14px 10px 2px 0; }
  .hm-col .hm-val{ font-size:1.05rem; }
  .hm-col:nth-child(3){ border-right:none; }
  .hm-apply{ flex:1 1 100%; justify-content:space-between; padding:15px 18px; margin:12px 0 0; }
}

/* ============================================================ STATEMENT */
/* Vertical padding was min(18vw,220px), set when this held a two-line sentence. The
   copy is now a single short line, so that much air read as an empty band rather
   than as a pull-quote. */
.statement{ padding:min(8vw,96px) 8vw; text-align:center; }
.statement p{ font-family:var(--font-script); font-style:italic; font-weight:400; font-size:clamp(1.7rem,4.8vw,3.5rem); line-height:1.2; max-width:1000px; margin:0 auto; }
.statement em{ color:var(--green); font-family:inherit; }

/* ============================================================ MARQUEE */
/* Bottom padding stacks with .about's top padding, since the two sections meet.
   They were 60 + 60 = 120px of empty cream between the carousel and the copy. */
.marquee-wrap{ padding:0 0 34px; }
.marquee-label{ text-align:center; font-family:var(--font-mono); font-size:.72rem; letter-spacing:.14em; color:var(--ink-soft); margin-bottom:22px; }
.marquee{ overflow:hidden; border-top:1px solid var(--cream-line); border-bottom:1px solid var(--cream-line); padding:18px 0; display:flex; }
.marquee-track{
  display:flex; align-items:center; gap:34px; white-space:nowrap;
  animation:marquee 22s linear infinite; padding-right:34px; flex-shrink:0;
}
/* Real logos now, so the old nth-child(even) rule that tinted every second *text*
   node is gone — separators carry their own class instead of relying on position. */
.wm{ display:inline-flex; align-items:center; gap:10px; }
/* One height for all five. They were trimmed to their alpha bounds and exported at
   slightly different pixel heights first, because the two crest lockups (Berkeley,
   CSAIL) carry more vertical mass than a plain wordmark and read oversized when
   matched box-for-box. */
.wm img{ height:34px; width:auto; display:block; }
.wm-word{ font-family:var(--font-display); font-weight:600; font-size:1.05rem; letter-spacing:.02em; color:var(--ink); }
.wm-dot{ color:var(--green-light); font-family:var(--font-mono); font-size:.9rem; }
@keyframes marquee{ from{ transform:translateX(0); } to{ transform:translateX(-100%); } }
.placeholder-note{ text-align:left; font-family:var(--font-mono); font-size:.68rem; color:#b3894a; margin-top:12px; padding:0 6vw; }
.placeholder-note.center{ text-align:center; }

/* ============================================================ ABOUT */
.about{ padding:34px 6vw 120px; }   /* top pairs with .marquee-wrap above */
.about-grid{ display:grid; grid-template-columns:1.5fr 1fr; gap:70px; max-width:1200px; margin:0 auto; }
.about-copy p{ font-size:clamp(1.05rem,1.7vw,1.3rem); line-height:1.6; color:var(--ink); margin-bottom:22px; max-width:640px; }
.text-btn{ display:inline-flex; align-items:center; gap:10px; font-family:var(--font-mono); font-size:.78rem; letter-spacing:.08em; border-bottom:1px solid var(--ink); padding-bottom:4px; margin-top:8px; }
.text-btn .arrow{ transition:transform .3s var(--ease); }
.text-btn:hover .arrow{ transform:rotate(90deg); }
.know-more-panel{ display:grid; grid-template-columns:1fr 1fr; gap:36px; max-height:0; overflow:hidden; transition:max-height .5s var(--ease), margin .5s var(--ease); margin-top:0; }
.know-more-panel.open{ max-height:600px; margin-top:28px; }
.km-col h4{ font-family:var(--font-script); font-style:italic; font-size:1.35rem; color:var(--green-deep); margin-bottom:12px; }
.km-col ul{ display:flex; flex-direction:column; gap:8px; font-size:.92rem; }
.km-col li::before{ content:"— "; color:var(--green); }
.km-col p{ font-size:.92rem; line-height:1.6; color:var(--ink-soft); }

.about-side{ display:flex; flex-direction:column; gap:28px; border-left:1px dashed var(--cream-line); padding-left:36px; }
.about-stat{ display:flex; flex-direction:column; }
.about-stat .num{ font-family:var(--font-display); font-weight:700; font-size:2.2rem; color:var(--green); }
.about-stat .label{ font-family:var(--font-mono); font-size:.72rem; letter-spacing:.04em; color:var(--ink-soft); margin-top:4px; }

@media (max-width:860px){
  .about-grid{ grid-template-columns:1fr; gap:40px; }
  .about-side{ border-left:none; border-top:1px dashed var(--cream-line); padding-left:0; padding-top:28px; flex-direction:row; flex-wrap:wrap; }
  .know-more-panel{ grid-template-columns:1fr; }
}

/* ============================================================ SECTION HEADINGS shared */
.section-kicker{ display:block; font-family:var(--font-mono); font-size:.72rem; letter-spacing:.14em; color:var(--green); margin-bottom:14px; }
.section-kicker.light{ color:var(--teal); }
/* Matches the carousel slides: deep-green serif italic, sentence case, with the
   emphasised word in the lighter green ("What's *Happening*"). */
.script-heading{
  font-family:var(--font-script); font-style:italic; font-weight:400;
  font-size:clamp(2.6rem,7vw,4.6rem); line-height:1.05; letter-spacing:-.005em;
  text-transform:none; color:var(--green-deep); margin-bottom:44px;
}
.script-heading em{ font-family:inherit; font-style:italic; color:var(--green); }
.schedule .script-heading{ color:var(--cream); }
.schedule .script-heading em{ color:var(--green-light); }

/* ---- headings that sit directly on the shader ----
   The sections without a --veil panel (.hosts, .speakers, .videos) put this type
   straight onto the live WebGL background. That background is built from the
   International Orange / Golden Hour palette — the same hues as --green and
   --green-deep — so the brand colours land red-on-orange and go nearly invisible
   at some points in the animation. Luminance is not the only problem; the hues
   match too, which is why simply darkening the red never fixed it.
   script.js samples the canvas under each [data-contrast] element and toggles
   .on-light / .on-dark. Both variants drop the red entirely and use ink or cream,
   which separate from every colour the shader produces. .on-light is also the
   no-JS default below, since the shader spends most of its range light. */
[data-contrast]{ transition:color .5s var(--ease); }
[data-contrast] em{ transition:color .5s var(--ease); }

.section-kicker[data-contrast],
.section-kicker[data-contrast].on-light{ color:var(--green-deep); }
.script-heading[data-contrast],
.script-heading[data-contrast].on-light{ color:var(--ink); }
.script-heading[data-contrast] em,
.script-heading[data-contrast].on-light em{ color:var(--green-deep); }

.section-kicker[data-contrast].on-dark{ color:var(--green-light); }
.script-heading[data-contrast].on-dark{ color:var(--cream); }
.script-heading[data-contrast].on-dark em{ color:var(--green-light); }

/* ============================================================ YOUR HOSTS */
/* Replaced the polaroid collage. Those were tilted, hard-edged white cards with a
   drop shadow — a physical-object metaphor that fought the translucent panels the
   rest of the page settled on. These use the same --veil card as the feed and the
   copy sections, so the shader reads through them. */
.hosts{ padding:100px 6vw; text-align:center; }
.host-grid{
  display:grid; grid-template-columns:repeat(3,1fr); gap:24px;
  max-width:1060px; margin:44px auto 0;
}
.host-card{
  min-width:0; display:flex; flex-direction:column; gap:16px;
  background:var(--veil); border:1px solid var(--cream-line);
  border-radius:var(--radius); padding:16px;
  /* opacity + transform restated because .reveal declares them in a transition
     shorthand and this rule would otherwise replace it and kill the fade-in. */
  transition:opacity .9s var(--ease), transform .9s var(--ease),
             border-color .3s, background .3s, box-shadow .35s var(--ease);
}
.host-card:hover{
  border-color:rgba(26,21,18,.28); background:rgba(242,239,230,.86);
  box-shadow:0 14px 34px rgba(26,21,18,.13);
}
/* All three portraits are now full-frame photos cropped to exactly 4/5, so this
   plate only shows while they load. It stays white rather than --cream because a
   warm plate tints the first paint of a photo that is about to be neutral. */
.host-photo{
  position:relative; overflow:hidden;
  aspect-ratio:4/5; border-radius:calc(var(--radius) - 6px); background:#fff;
}
.host-photo img{
  width:100%; height:100%; object-fit:cover; object-position:50% 22%;
  transition:transform .7s var(--ease);
}
.host-card:hover .host-photo img{ transform:scale(1.035); }
.host-card figcaption{ display:flex; flex-direction:column; gap:4px; text-align:left; }
.host-name{ font-family:var(--font-serif); font-weight:400; font-size:1.35rem; letter-spacing:.005em; color:var(--ink); }
/* No uppercase transform any more: these lines carry handles and a domain
   (@tastefully.co, @sequoia), and capitalising those reads as shouting rather than
   as a label. Tracking eased from .14em to .06em to buy the extra width — the
   longest line is 37 characters against the old four. */
.host-role{ font-family:var(--font-mono); font-size:.68rem; letter-spacing:.06em; line-height:1.45; color:var(--ink-soft); }
/* Icon-only here, unlike the speakers' labelled pill — two buttons per card would
   make a labelled row wider than the card at this column width. The aria-label on
   each anchor carries the name, so they don't read as identical "link" repeats. */
.host-links{ display:flex; gap:8px; margin-top:12px; }
.host-link{
  display:inline-flex; align-items:center; justify-content:center;
  width:32px; height:32px; border-radius:50%;
  border:1px solid var(--cream-line); color:var(--ink-soft);
  transition:color .25s var(--ease), border-color .25s var(--ease), background .25s var(--ease);
}
.host-link svg{ width:15px; height:15px; display:block; }
.host-link:hover{ color:var(--green); border-color:var(--green); background:rgba(242,239,230,.55); }
.host-link:focus-visible{ outline:2px solid var(--green); outline-offset:3px; }

@media (max-width:900px){
  /* three portraits get too narrow to read faces before they get too narrow to fit */
  .host-grid{ grid-template-columns:repeat(2,1fr); max-width:660px; }
}
@media (max-width:620px){
  .host-grid{ grid-template-columns:1fr; max-width:360px; gap:20px; }
}

/* ============================================================ SCHEDULE / timeline */
.schedule{ padding:100px 6vw; background:var(--ink); color:var(--cream); }
/* The 720px measure belongs to the text, not the photo — capping the wrapper boxed
   the banner into a 720px column regardless of its own ratio. */
.schedule-inner{ max-width:none; margin:0; }
.schedule .script-heading{ max-width:720px; margin-left:auto; margin-right:auto; text-align:center; }
.schedule-inner .timeline-wrap{ max-width:720px; margin:0 auto; }
/* The entire photo, nothing cropped — so no aspect-ratio and no object-fit here.
   Those two were the source of the blank bars: aspect-ratio sized the *box*
   independently of the image, and any leftover space inside it was padding the
   layout (and painting the old navy background). height:auto instead means the
   element is exactly as tall as the photo, with nothing left over to close.
   The width/height attributes on the <img> still reserve the right space during
   load, so dropping aspect-ratio costs no layout stability.
   Centred inside the section's own 6vw padding, equal space either side. */
.schedule-photo{
  width:100%; height:auto; margin:0 auto 56px;
  border-radius:var(--radius); display:block;
}
.timeline-wrap{ position:relative; }
.timeline{ position:relative; padding-left:34px; }
.timeline::before{ content:""; position:absolute; left:5px; top:6px; bottom:6px; width:1px; background:rgba(242,239,230,.2); }
.timeline li{ position:relative; padding:20px 0; display:flex; align-items:baseline; gap:22px; border-bottom:1px solid rgba(242,239,230,.1); transition:opacity .3s var(--ease); }
.timeline li:last-child{ border-bottom:none; }
.t-dot{ position:absolute; left:-34px; top:26px; width:11px; height:11px; border-radius:50%; background:var(--cream); border:2px solid var(--ink); box-shadow:0 0 0 2px rgba(242,239,230,.5); }

/* ---- cursor-tracking lamp ----
   One light for the whole list, slid to whichever row the pointer is over rather
   than a glow toggled per row — a single travelling light reads as one object
   moving, where per-row toggling reads as five lights blinking.
   It rests on the accent row (the finale) whenever nothing is hovered, so the
   default state still points at the main event. script.js writes --lamp-y. */
.t-lamp{
  position:absolute; left:0; top:var(--lamp-y,32px);
  width:11px; height:11px; border-radius:50%; z-index:3; pointer-events:none;
  background:var(--green-light);
  box-shadow:0 0 0 2px var(--green-light), 0 0 18px 3px rgba(231,163,60,.7);
  transform:translateY(-50%) scale(1);
  transition:top .45s var(--ease), transform .35s var(--ease), box-shadow .35s var(--ease);
}
.timeline-wrap.is-hovered .t-lamp{
  transform:translateY(-50%) scale(1.5);
  box-shadow:0 0 0 2px var(--green-light), 0 0 32px 9px rgba(231,163,60,.85);
}
/* Pull the unhovered rows back so the lit one is unmistakably the focus. */
.timeline-wrap.is-hovered .timeline li:not(:hover){ opacity:.5; }

/* The static accent glow is the no-JS fallback. Once the lamp is live it owns the
   light, so the dot underneath drops back to a plain one — otherwise the resting
   row shows two overlapping glows. */
.t-dot--accent{ background:var(--green-light); box-shadow:0 0 0 2px var(--green-light), 0 0 16px 2px var(--green-light); }
.timeline.has-lamp .t-dot--accent{ background:var(--cream); box-shadow:0 0 0 2px rgba(242,239,230,.5); }

.t-time{ font-family:var(--font-mono); font-size:.85rem; color:var(--green-light); min-width:110px; transition:color .3s var(--ease); }
.t-desc{ font-family:var(--font-display); font-weight:600; font-size:1.15rem; letter-spacing:.01em; text-transform:uppercase; transition:color .3s var(--ease); }
.timeline li:hover .t-desc{ color:#fff; }

@media (prefers-reduced-motion: reduce){
  .t-lamp{ transition:none; }
}
.timeline-progress{ display:none; }

/* ============================================================ THE BUILD */
/* Spans the full content column so its veil matches .about's width, but keeps the
   copy measure at 900px via padding rather than max-width — a centred 900px box
   would paint a narrow cream panel and read as a card. */
.build{ padding:110px max(6vw, calc((100% - 900px) / 2)); }
.build .script-heading{ margin-bottom:20px; }
.build-copy{ font-size:clamp(1.1rem,2vw,1.4rem); max-width:600px; margin-bottom:44px; }
.build-copy strong{ color:var(--green); }
.prize-card{
  background:#fdfcf8; border:1px solid var(--ink); border-radius:var(--radius); padding:36px;
  display:flex; justify-content:space-between; align-items:center; gap:24px; flex-wrap:wrap; margin-bottom:50px;
}
.prize-kicker{ font-family:var(--font-mono); font-size:.7rem; letter-spacing:.12em; color:var(--green); display:block; margin-bottom:10px; }
/* line-height carries the highlight bands: at the old default they nearly touched
   between wrapped lines, which is what made the marks look cramped. */
.prize-card p{ font-size:1.2rem; max-width:340px; line-height:1.75; }
.prize-card strong{
  color:var(--green-light); background:var(--ink);
  /* em-based, so the breathing room scales with the type instead of being a flat
     4px that shrinks visually as the font grows. Was `0 4px` — no vertical room at
     all, and the box started hard against the first and last letter. */
  padding:.16em .48em; border-radius:5px;
  /* the gift-card mark now wraps in this column; without clone the second fragment
     loses its padding and radius and reads as a torn edge */
  -webkit-box-decoration-break:clone; box-decoration-break:clone;
}
.prize-photo{ width:min(320px,100%); border-radius:12px; display:block; object-fit:cover; }
.build-list{ font-family:var(--font-mono); }
.build-list ul{ margin-top:16px; display:flex; flex-direction:column; gap:8px; font-size:.9rem; color:var(--ink-soft); }
.build-list li::before{ content:"— "; color:var(--green); }

@media (max-width:640px){ .prize-card{ flex-direction:column; align-items:flex-start; } }

/* ============================================================ SPEAKERS */
/* Short bottom padding: this meets .videos, whose own top padding stacks with it.
   At 100px each the two sections sat ~200px apart. */
.speakers{ padding:100px 6vw 48px; text-align:center; }
/* Two up rather than four: the bios run 25-45 words, and a four-across row squeezes
   them into a column narrow enough to hyphenate badly. */
/* grid-auto-rows:1fr forces every row to the tallest card's height, so all four
   cards match and the names sit on one line across the grid. Without it each row
   sizes to its own content and the second row's names land at a different height
   from the first — the bios run 25 to 45 words, so the rows never match by chance. */
.speaker-grid{
  display:grid; grid-template-columns:repeat(2,1fr); grid-auto-rows:1fr; gap:22px;
  max-width:1000px; margin:44px auto 0; text-align:left;
}
.speaker-card{
  min-width:0; display:flex; align-items:flex-start; gap:18px;
  background:var(--veil); border:1px solid var(--cream-line);
  border-radius:var(--radius); padding:20px;
  /* opacity + transform restated because .reveal declares them in a transition
     shorthand this rule would otherwise replace wholesale, killing the fade-in. */
  transition:opacity .9s var(--ease), transform .9s var(--ease),
             border-color .3s, background .3s, box-shadow .35s var(--ease);
}
.speaker-card:hover{
  border-color:rgba(26,21,18,.28); background:rgba(242,239,230,.86);
  box-shadow:0 14px 34px rgba(26,21,18,.13);
}
/* align-self on the photo alone, not align-items on the card: centring the whole
   row would drag the text block down with it and undo the shared baselines the
   name/bio heights are there to hold. The card fills its grid row, so this centres
   against the full card height. */
.speaker-photo{
  width:88px; height:88px; flex-shrink:0; border-radius:50%; align-self:center;
  object-fit:cover; display:block; background:#e6e2d8;
  border:1px solid var(--cream-line);
}
.speaker-body{ min-width:0; }
/* Fixed one-line slot. Left to wrap, "Alexandru Turcanu" could take two lines where
   "Skye Lam" takes one, which alone would push their bios onto different lines. */
.speaker-name{
  font-family:var(--font-serif); font-weight:500; font-size:1.18rem;
  line-height:1.3; height:1.3em; margin-bottom:7px;
  letter-spacing:.005em; color:var(--ink);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
/* Reserve the tallest bio's height on all four — 5 lines at this measure, which is
   what Ethan's and Roxana's run to. The shorter two (Skye 4, Alexandru 3) simply
   leave the remainder blank, which is the point: every card's first bio line sits on
   the same baseline instead of floating to its own. min-height, not height, so a
   narrower viewport can still push them longer without clipping. */
.speaker-bio{
  font-size:.92rem; line-height:1.55; color:var(--ink-soft);
  min-height:calc(5 * 1.55em);
}
/* Sits below the bio's reserved height, so it lands on the same line across all four
   cards. Ink rather than LinkedIn blue — the brand colour is the one hue that fights
   both the cream panel and the orange accent. */
.speaker-link{
  display:inline-flex; align-items:center; gap:7px; margin-top:13px;
  font-family:var(--font-mono); font-size:.66rem; letter-spacing:.1em; text-transform:uppercase;
  color:var(--ink-soft); padding:6px 12px 6px 10px;
  border:1px solid var(--cream-line); border-radius:999px;
  transition:color .25s var(--ease), border-color .25s var(--ease), background .25s var(--ease);
}
.speaker-link svg{ width:13px; height:13px; flex-shrink:0; display:block; }
.speaker-link:hover{ color:var(--green); border-color:var(--green); background:rgba(242,239,230,.55); }
.speaker-link:focus-visible{ outline:2px solid var(--green); outline-offset:3px; }

@media (max-width:820px){
  .speaker-grid{ grid-template-columns:1fr; max-width:520px; }
}
@media (max-width:440px){
  /* stack rather than shrink the portrait — an 88px circle beside a 4-line bio
     leaves the text about 30 characters wide, which reads as a ransom note */
  .speaker-card{ flex-direction:column; align-items:center; text-align:center; gap:14px; }
  /* One card per screen here, so there is nothing to line up against — drop the
     reserved height rather than pad every card with dead space, and let the longest
     name wrap instead of risking an ellipsis at this width. */
  .speaker-name{ white-space:normal; height:auto; }
  .speaker-bio{ min-height:0; }
}

/* ============================================================ VIDEOS */
/* Matching short top padding — see the note on .speakers above. */
.videos{ padding:48px 6vw 100px; }
/* Three posts, three equal columns — every card now has the same 16/9 media, so
   they line up as a set instead of needing per-embed heights. */
.video-grid{
  display:grid; grid-template-columns:repeat(3,1fr); gap:22px;
  max-width:1180px; margin:0 auto; align-items:start;
}

/* ---- rebuilt post cards ----
   The Reels and the LinkedIn post used to be live embeds. Both platforms serve
   cross-origin iframes: unstyleable from out here, painting their own opaque white
   chrome, and liable to show a login wall to logged-out viewers. So the posts are
   reconstructed in our own markup instead — avatar, name, handle, text, media,
   platform mark — and the whole card links out to the original.
   Everything is rehosted under assets/social/. The platform CDN URLs those came
   from are signed and expire, so they cannot be hotlinked; to refresh, re-pull
   og:image from each permalink. */
.post-card{
  min-width:0;                                   /* grid children default to auto and refuse to shrink */
  display:flex; flex-direction:column; gap:13px;
  background:var(--veil); border:1px solid var(--cream-line);
  border-radius:var(--radius); padding:16px;
  /* opacity + transform are restated because .reveal (further up) declares them in a
     transition shorthand, and this rule would otherwise replace it wholesale and kill
     the scroll fade-in. For the same reason the hover lift is done with shadow rather
     than translateY: transform belongs to the reveal here, and animating it from two
     places fights. */
  transition:opacity .9s var(--ease), transform .9s var(--ease),
             border-color .3s, background .3s, box-shadow .35s var(--ease);
}
.post-card:hover{
  border-color:rgba(26,21,18,.28);
  background:rgba(242,239,230,.86); box-shadow:0 14px 34px rgba(26,21,18,.13);
}
.post-card:focus-visible{ outline:2px solid var(--green); outline-offset:4px; }

.post-head{ display:flex; align-items:center; gap:11px; }
.post-avatar{
  width:40px; height:40px; flex-shrink:0; border-radius:50%;
  object-fit:cover; border:1px solid var(--cream-line);
}
.post-id{ display:flex; flex-direction:column; gap:1px; min-width:0; }
.post-name{ font-weight:600; font-size:.94rem; line-height:1.2; color:var(--ink); }
.post-handle{
  font-family:var(--font-mono); font-size:.72rem; line-height:1.2; color:var(--ink-soft);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.post-logo{ width:16px; height:16px; margin-left:auto; flex-shrink:0; color:var(--ink-soft); transition:color .3s; }
.post-card:hover .post-logo{ color:var(--green); }

/* Clamped so an over-long caption can't stretch one card taller than its row. */
.post-text{
  font-size:.94rem; line-height:1.5; color:var(--ink);
  display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;
}

.post-media{
  position:relative; overflow:hidden;
  aspect-ratio:16/9; border-radius:calc(var(--radius) - 6px); background:var(--ink);
}
.post-media img{
  width:100%; height:100%; object-fit:cover;
  transition:transform .7s var(--ease), filter .5s;
  filter:saturate(.94);
}
.post-card:hover .post-media img{ transform:scale(1.04); filter:saturate(1.04); }
/* Instagram bakes a faint play glyph into the centre of its og:image — roughly 8%
   of the frame, ~28px at this card width. 54px is chosen to sit over it rather than
   beside it; shrink this and a second, fainter ring appears around ours. */
.post-play{
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  width:54px; height:54px; border-radius:50%; background:var(--cream);
  box-shadow:0 6px 20px rgba(26,21,18,.38);
  display:grid; place-items:center;
  transition:transform .3s var(--ease), background .3s;
}
.post-play::after{
  content:""; width:0; height:0; margin-left:4px;
  border-left:16px solid var(--ink);
  border-top:10px solid transparent; border-bottom:10px solid transparent;
}
.post-card:hover .post-play{ transform:translate(-50%,-50%) scale(1.08); background:#fff; }

.post-cta{
  margin-top:auto; font-family:var(--font-mono); font-size:.7rem; letter-spacing:.12em;
  text-transform:uppercase; color:var(--ink-soft); transition:color .25s;
}
.post-cta::after{ content:" \2192"; }
.post-card:hover .post-cta{ color:var(--green); }

@media (max-width:1020px){
  .video-grid{ grid-template-columns:repeat(2,1fr); max-width:760px; }
}
/* Below this two columns leave the media too small to read, so drop to one. */
@media (max-width:660px){
  .video-grid{ grid-template-columns:1fr; max-width:420px; gap:20px; }
}

/* ============================================================ WHERE / APPLY */
.where{ position:relative; padding:140px 6vw; color:#fff; overflow:hidden; text-align:center; }
.where-bg{
  position:absolute; inset:0;
  background:url("assets/venueSF.jpg") center/cover no-repeat, var(--ink);
  transform:scale(1.05);
}
.where-bg::after{ content:""; position:absolute; inset:0; background:rgba(20,11,5,.60); }
.where-content{ position:relative; z-index:1; display:flex; flex-direction:column; align-items:center; gap:22px; max-width:620px; margin:0 auto; }
.where h2{ font-family:var(--font-display); font-weight:700; font-size:clamp(1.8rem,5vw,3rem); text-transform:uppercase; line-height:1.15; }
.sponsored{ font-family:var(--font-mono); font-size:.72rem; letter-spacing:.1em; color:rgba(255,255,255,.6); margin-top:10px; }
.sponsored strong{ color:#fff; }

/* ============================================================ FOOTER */
.footer{ background:var(--ink); color:var(--cream); padding:70px 6vw 30px; }
.footer-top{ display:flex; justify-content:space-between; flex-wrap:wrap; gap:50px; padding-bottom:50px; border-bottom:1px solid rgba(242,239,230,.15); max-width:1200px; margin:0 auto; }
.footer-brand{ display:flex; align-items:center; gap:10px; }
/* No longer inverted — the mark is two-tone, so flipping the ground would just break
   the logo. The footer is ink and the badge is very nearly ink too, so a hairline is
   what separates them; without it the square dissolves into the background. */
.footer-brand .nav-badge{ box-shadow:inset 0 0 0 1px rgba(242,239,230,.22); }
.footer-cols{ display:flex; gap:70px; flex-wrap:wrap; }
.footer-cols h5{ font-family:var(--font-mono); font-size:.68rem; letter-spacing:.12em; color:var(--green-light); margin-bottom:14px; }
.footer-cols a{ display:block; padding:4px 0; font-size:.92rem; opacity:.85; transition:opacity .2s, transform .2s; }
.footer-cols a:hover{ opacity:1; transform:translateX(4px); }
.footer-bottom{ display:flex; justify-content:space-between; flex-wrap:wrap; gap:10px; padding-top:24px; font-family:var(--font-mono); font-size:.68rem; letter-spacing:.05em; color:rgba(242,239,230,.5); max-width:1200px; margin:0 auto; }

/* ============================================================ misc */
@media (max-width:640px){
  .about, .hosts, .schedule, .build, .speakers, .videos, .where{ padding-left:6vw; padding-right:6vw; }
}
