/* ============================================================================
   studyTorium
   ----------------------------------------------------------------------------
   Japanese tea-house language: rice paper, sumi ink, matcha, kaki persimmon.
   Light-first, because a tea room is. Surfaces are shoji (frosted paper) not
   glass: high opacity, low blur, near-neutral saturation, no specular gloss.

   Four rules hold it together:
     1. Ma. Negative space is a material here, not leftover room.
     2. Text sits on a shoji panel or a photo scrim, never directly on the
        wash, so contrast never depends on which gradient is behind it.
     3. Every backdrop-filter has an opaque fallback (see @supports at the
        end). The panels are opaque enough to survive without blur.
     4. One accent, one radius scale, one theme per page.
   ============================================================================ */

/* ------------------------------------------------------------------- type
   Self-hosted so there is no render-blocking third-party request. */
@font-face {
  font-family: "Geist";
  src: url("/site/fonts/geist-400.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "Geist";
  src: url("/site/fonts/geist-500.woff2") format("woff2");
  font-weight: 500;
  font-display: swap;
}
@font-face {
  font-family: "Geist";
  src: url("/site/fonts/geist-600.woff2") format("woff2");
  font-weight: 600;
  font-display: swap;
}
@font-face {
  font-family: "Geist";
  src: url("/site/fonts/geist-700.woff2") format("woff2");
  font-weight: 700;
  font-display: swap;
}

/* ----------------------------------------------------------------------------
   Light-first, because a tea house is. The previous dark-glass build inverted
   that, and no amount of recolouring makes a dark glossy pane feel like a
   room full of paper and daylight.

   Surfaces are shoji, not glass: high-opacity, low-blur, low-saturation, no
   specular highlight. Frosted paper rather than a wet windowpane.
   ---------------------------------------------------------------------------- */
:root {
  color-scheme: light dark;

  --font: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;

  /* Rice paper with a green-grey cast, deliberately not the yellow cream
     family (#f5f1ea, #faf7f1, #e8dfcb) that every warm-craft site defaults to.
     The wash is matcha and clay, kept far below the accent's saturation. */
  --bg-base: #eef0e8;
  --orb-1: rgba(140, 163, 106, 0.30);  /* matcha  */
  --orb-2: rgba(176, 190, 150, 0.26);  /* sencha  */
  --orb-3: rgba(198, 160, 124, 0.14);  /* clay    */

  --glass: rgba(252, 252, 248, 0.78);
  --glass-strong: rgba(252, 252, 248, 0.90);
  --glass-hover: rgba(255, 255, 255, 0.94);
  --glass-border: rgba(35, 38, 31, 0.10);
  --glass-border-lit: rgba(35, 38, 31, 0.22);
  --blur: 10px;
  --sat: 105%;

  --text: #23261f;      /* sumi ink, never pure black */
  --muted: #545a4d;
  --faint: #7c8375;

  --accent: #5c6f3a;    /* matcha, deep enough to carry white */
  --accent-ink: #ffffff;
  --accent-soft: rgba(92, 111, 58, 0.10);
  /* Kaki persimmon. Earthy enough to belong, far enough from matcha to still
     read as a warning. */
  --warn: #9c4a21;

  /* The sticky nav's height. Declared on :root, not on .nav: the hero pulls
     itself up under the nav and is not a descendant of it, so a variable set
     on .nav would never reach it and would silently fall back.

     Measured, not derived: 14px padding twice plus the line box plus the 1px
     bottom border comes to 79 at every breakpoint. A stale 65 here left the
     hero 14px taller than the viewport. */
  --nav-h: 79px;

  --radius: 14px;
  --radius-sm: 10px;
  /* Chunkier, for the cards under the hero. Clay is pressed, not cut, and at
     14px those cards read as software chrome parked under a photograph of a
     model. See "keeping the room" at the end of this file. */
  --radius-clay: 20px;
  --maxw: 1120px;

  /* The pendant lamps in the panorama. The page wash is matcha and clay and
     carries no warm note of its own, which is most of why everything below
     the hero read as a different site. */
  --lamp: rgba(255, 176, 96, 0.22);

  /* One warm highlight along the top edge. A modelled lump of clay is lit from
     above; a pane of glass has a specular streak instead, and that difference
     is nearly all of what separates the two materials at card size. */
  --lit-edge: inset 0 1px 0 rgba(255, 244, 222, 0.16);

  /* Barely-there. A tea room has ambient light, not drop shadows; hairline
     borders carry the separation instead. */
  --shadow: 0 1px 2px rgba(35, 38, 31, 0.04);
  --shadow-lg: 0 10px 34px rgba(35, 38, 31, 0.08);

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg-base: #16180f;
    --orb-1: rgba(140, 163, 106, 0.20);
    --orb-2: rgba(120, 140, 96, 0.16);
    --orb-3: rgba(170, 130, 96, 0.10);

    --glass: rgba(238, 240, 232, 0.045);
    --glass-strong: rgba(238, 240, 232, 0.075);
    --glass-hover: rgba(238, 240, 232, 0.095);
    --glass-border: rgba(236, 238, 229, 0.11);
    --glass-border-lit: rgba(183, 204, 138, 0.30);

    --text: #eceee5;
    --muted: #b9bfae;
    --faint: #8d9484;

    /* Deep matcha goes muddy on sumi, so the dark theme lifts to a young-leaf
       green and flips the ink. */
    --accent: #b7cc8a;
    --accent-ink: #1a1e16;
    --accent-soft: rgba(183, 204, 138, 0.12);
    --warn: #e0a06a;

    /* Additive over near-black, so it carries further at a lower alpha. */
    --lamp: rgba(255, 168, 88, 0.13);

    --shadow: 0 1px 2px rgba(0, 0, 0, 0.30);
    --shadow-lg: 0 10px 34px rgba(0, 0, 0, 0.42);
  }
}

* { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--bg-base);
  color: var(--text);
  font-family: var(--font);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Fixed so it never repaints on scroll; pointer-events none so it cannot
   swallow a click. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background:
    radial-gradient(60vmax 50vmax at 12% -8%, var(--orb-1), transparent 60%),
    radial-gradient(58vmax 52vmax at 92% 6%,  var(--orb-2), transparent 62%),
    radial-gradient(52vmax 46vmax at 72% 78%, var(--orb-3), transparent 58%),
    radial-gradient(46vmax 40vmax at 4% 92%,  var(--orb-2), transparent 60%);
}

/* Washi tooth over the whole page.
   z-index must be ABOVE the content, not below it. Underneath, every
   backdrop-filter samples the noise and blurs it into a flat grey slab, which
   is invisible on paper and very obvious on sumi. Fixed and pointer-events
   none so it never repaints on scroll and never eats a click. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
  opacity: 0.55;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.032'/%3E%3C/svg%3E");
}

a { color: inherit; }

img {
  display: block;
  max-width: 100%;
  height: auto;
}

.container {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 24px;
}

/* ------------------------------------------------------------------ glass */
.nav,
.card,
.panel,
.place-row,
.empty,
.input,
.badge,
.waitlist-done,
.place-header,
.bento-cell,
.versus-side,
.rail-card {
  background: var(--glass);
  backdrop-filter: blur(var(--blur)) saturate(var(--sat));
  -webkit-backdrop-filter: blur(var(--blur)) saturate(var(--sat));
  border: 1px solid var(--glass-border);
}

/* -------------------------------------------------------------------- nav */
.nav {
  position: sticky;
  top: 0;
  z-index: 30;
  border-width: 0 0 1px;
  border-radius: 0;
}

.nav-inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 14px 24px;      /* 79px tall with the border; see --nav-h */
  display: flex;
  align-items: center;
  gap: 24px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-weight: 600;
  font-size: 19px;
  letter-spacing: -0.02em;
  text-decoration: none;
}

/* The mark is the alt="" partner of the wordmark beside it, so it is decorative
   and the link still reads as "studyTorium" to a screen reader.
   `.brand picture` is the dark-scheme swap wrapper; left inline it becomes a
   flex item with no width of its own and the mark collapses. */
.brand picture { display: flex; flex: none; }
.brand-mark {
  width: 30px;
  height: 30px;
  display: block;
  flex: none;
}

/* Scoped to .brand-word, not .brand: the wordmark is now wrapped, so a bare
   `.brand span` would paint the whole word in the accent instead of the half
   that is meant to carry it. */
.brand-word span { color: var(--accent); }

.nav-links {
  margin-left: auto;
  display: flex;
  gap: 24px;
  align-items: center;
}
/* :not(.btn) matters: `.nav-links a` outranks `.btn-primary` on specificity,
   so without it the nav CTA loses its high-contrast ink colour. */
.nav-links a:not(.btn) {
  color: var(--muted);
  text-decoration: none;
  font-size: 14.5px;
  transition: color 0.16s var(--ease);
}
.nav-links a:not(.btn):hover { color: var(--text); }

/* ---------------------------------------------------------------- buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 12px 26px;
  font: inherit;
  font-weight: 600;
  font-size: 15px;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;      /* CTA labels never wrap */
  transition: transform 0.16s var(--ease), box-shadow 0.16s var(--ease),
              background 0.16s var(--ease);
}

.btn-primary {
  background: var(--accent);
  color: var(--accent-ink);
  }

.btn-primary:hover { transform: translateY(-1px); box-shadow: var(--shadow); }
.btn-primary:active { transform: translateY(0) scale(0.985); }

.btn-ghost {
  background: var(--glass-strong);
  border-color: var(--glass-border);
  color: var(--text);
}
.btn-ghost:hover { background: var(--glass-hover); }

.btn:disabled { opacity: 0.55; cursor: not-allowed; transform: none; }

.btn:focus-visible,
a:focus-visible,
button:focus-visible,
.input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ------------------------------------------------------------------ type */
/* Scale is planned around the hero image width: at 54px this headline wrapped
   to three lines, which breaks the two-line cap. */
h1 {
  font-size: clamp(31px, 3.9vw, 46px);
  line-height: 1.08;
  letter-spacing: -0.038em;
  font-weight: 600;
  margin: 0 0 20px;
  text-wrap: balance;
}
/* Italic emphasis in the same family, never a second typeface. leading and
   padding reserve keep the descender on "you" from clipping. */
h1 em {
  font-style: italic;
  color: var(--accent);
  line-height: 1.1;
  padding-bottom: 0.06em;
  display: inline-block;
}

h2 {
  font-size: clamp(25px, 3.2vw, 34px);
  line-height: 1.14;          /* body's 1.6 is far too loose for display type */
  letter-spacing: -0.028em;
  font-weight: 600;
  margin: 0 0 12px;
  text-wrap: balance;
}

.eyebrow {
  display: inline-block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 22px;
  font-weight: 500;
}

.lede {
  font-size: 17.5px;
  color: var(--muted);
  max-width: 46ch;
  margin: 0 0 30px;
}

.section-sub {
  color: var(--muted);
  margin: 0 0 34px;
  max-width: 60ch;
  font-size: 16px;
}
.center .section-sub { margin-inline: auto; }

section { padding: 104px 0; }

/* ------------------------------------------------------- hero (split) */
.hero-split { padding: 88px 0 80px; }

.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  gap: 72px;
  align-items: center;
}

.hero-figure {
  position: relative;
  margin: 0;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
}
.hero-figure img {
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
}

/* Floating score chip: the product's core output, shown on a real photo. */
.hero-chip {
  position: absolute;
  left: 16px;
  bottom: 16px;
  right: 16px;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 13px 16px;
  border-radius: var(--radius-sm);
  background: rgba(22, 24, 15, 0.66);
  backdrop-filter: blur(12px) saturate(var(--sat));
  -webkit-backdrop-filter: blur(12px) saturate(var(--sat));
  border: 1px solid rgba(236, 238, 229, 0.20);
  color: #eceee5;   /* fixed light ink: this sits on a photo scrim in both themes */
}
.hero-chip-score {
  font-size: 27px;
  font-weight: 700;
  letter-spacing: -0.03em;
  color: #b7cc8a;
  font-variant-numeric: tabular-nums;
}
.hero-chip-text {
  display: flex;
  flex-direction: column;
  font-size: 13.5px;
  line-height: 1.35;
  color: rgba(236, 238, 229, 0.74);
}
.hero-chip-text strong { color: #eceee5; font-weight: 600; }

/* ---------------------------------------------- mechanic (full-width) */
.mechanic { padding: 100px 0 108px; }
.mechanic-title { max-width: 24ch; }

.versus {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 20px;
  margin-top: 8px;
}

/* A shelf each, so the question is two rooms rather than two labels. The
   whole point of the section is that these are places you have been, and a
   pair of empty rectangles asks you to choose between two strings. */
.versus-side {
  display: block;
  padding: 9px 9px 0;
  overflow: hidden;
  border-radius: var(--radius-clay);
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  box-shadow: var(--shadow), var(--lit-edge);
  transition: transform 0.28s var(--ease), opacity 0.28s var(--ease),
              border-color 0.28s var(--ease), box-shadow 0.28s var(--ease);
}
.versus-side:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg), var(--lit-edge); }

.versus-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 14px 10px 16px;
}
.versus-name { font-weight: 600; font-size: 16.5px; letter-spacing: -0.015em; }
.versus-meta { color: var(--muted); font-size: 14px; }

.versus-or {
  font-size: 13px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
}

.versus-side.is-picked {
  border-color: var(--accent);
  box-shadow: var(--shadow-lg), 0 0 0 1px var(--accent);
}
.versus-side.is-dropped { opacity: 0.42; transform: scale(0.97); }

/* Picking one lifts the model out of its shelf, as though it had been reached
   for. `steps()` deliberately: a stop-motion rig moves in whole frames, and
   five of them over a fifth of a second is the difference between a clay
   figure being handled and a div easing. Hover stays smooth -- a hover is a
   hint, not a take. */
.shelf-art { transition: transform 0.3s var(--ease); }
.versus-side:hover .shelf-art { transform: translateY(-4px) scale(1.02); }
.versus-side.is-picked .shelf-art {
  transform: translateY(-8px) scale(1.05);
  transition-timing-function: steps(5, end);
  transition-duration: 0.2s;
}

.versus-prompt {
  margin: 26px 0 0;
  font-size: 16px;
  color: var(--muted);
  text-align: center;
}
.versus-prompt.is-answered { color: var(--accent); }

/* ------------------------------------------------------------- bento
   Four cells for four pieces of content, tiled with no holes: row one is
   [wide 2 + live 1], row two is [text 1 + photo 2]. A row-span here left an
   empty cell in the corner, which reads as a planning mistake.

   The two wide cells each carry a diorama on a shelf, on opposite sides, so
   the eye crosses the grid rather than running down one column. */
.bento {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-auto-rows: minmax(252px, auto);
  gap: 16px;
  margin-top: 30px;
}

.bento-cell {
  position: relative;
  display: flex;
  flex-direction: column;
  border-radius: var(--radius-clay);
  padding: 24px;
  box-shadow: var(--shadow), var(--lit-edge);
  overflow: hidden;
  transition: transform 0.24s var(--ease), border-color 0.24s var(--ease),
              box-shadow 0.24s var(--ease);
}
.bento-cell h3 {
  margin: 0 0 9px;
  font-size: 17.5px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.25;
}
.bento-cell p { margin: 0; color: var(--muted); font-size: 15px; }

/* Shelf beside the copy rather than a full-bleed image behind it. These are
   objects photographed on a set: a cutout stretched edge to edge stops being
   a model and becomes wallpaper. */
.bento-wide,
.bento-photo {
  grid-column: span 2;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.02fr);
  align-items: stretch;
  padding: 0;
}
.bento-photo .shelf { order: -1; }

.bento-wide .shelf,
.bento-photo .shelf {
  height: 100%;
  aspect-ratio: auto;    /* the cell's row height decides, not the artwork */
  border-radius: 0;
}

.bento-text { align-self: center; padding: 24px 26px; }
.bento-live .badges { margin: 0 0 12px; }

/* ------------------------------------------------------------ campus rail
   Horizontal scroll-snap, so this section does not repeat the bento family. */
.rail-section { padding: 84px 0 108px; }
.rail-head { margin-bottom: 26px; }

.rail {
  display: flex;
  gap: 16px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding: 4px 24px 18px;
  max-width: calc(var(--maxw) + 48px);
  margin: 0 auto;
  scrollbar-width: thin;
  scrollbar-color: var(--glass-border-lit) transparent;
}
.rail::-webkit-scrollbar { height: 6px; }
.rail::-webkit-scrollbar-thumb {
  background: var(--glass-border-lit);
  border-radius: 999px;
}

.rail-card {
  flex: 0 0 264px;
  scroll-snap-align: start;
  border-radius: var(--radius-clay);
  overflow: hidden;
  text-decoration: none;
  box-shadow: var(--shadow), var(--lit-edge);
  transition: transform 0.24s var(--ease), border-color 0.24s var(--ease),
              box-shadow 0.24s var(--ease);
}
.rail-card:hover {
  transform: translateY(-2px);
  border-color: var(--glass-border-lit);
  box-shadow: var(--shadow-lg), var(--lit-edge);
}
.rail-card img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}
.rail-body { display: flex; flex-direction: column; gap: 2px; padding: 15px 18px 17px; }
.rail-name { font-weight: 600; font-size: 16px; letter-spacing: -0.015em; }
.rail-meta { color: var(--muted); font-size: 13.5px; }

/* ------------------------------------------------------------- closer
   The page ends where it began, back in the room. Same file as the hero and
   the same `?v=` query, so the browser serves it from cache and the bookend
   costs no bytes; a bare url() in this stylesheet would have been a second
   200KB request for the identical image. */
/* This is the #waitlist anchor target, and the nav is sticky: without the
   margin the jump puts "Start your list" underneath it. */
.closer {
  padding: 112px 0 124px;
  position: relative;
  overflow: hidden;
  scroll-margin-top: calc(var(--nav-h) + 12px);
}

.closer-room {
  position: absolute;
  inset: 0;
  z-index: -1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.15;
  /* Blurred hard enough that no face is legible. At 3px you read the room and
     start looking at the people in it, and the call to action is then the
     second thing on the screen. */
  filter: blur(6px) saturate(0.85);
  /* Faded at both ends: a hard edge would read as a second hero starting. */
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 38%, #000 64%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 38%, #000 64%, transparent);
}
@media (prefers-color-scheme: dark) { .closer-room { opacity: 0.22; } }

/* -------------------------------------------------------- place lists */
.place-list { display: flex; flex-direction: column; gap: 11px; }

.place-row {
  display: flex;
  align-items: center;
  gap: 18px;
  border-radius: var(--radius);
  padding: 17px 20px;
  text-decoration: none;
  box-shadow: var(--shadow);
  transition: transform 0.22s var(--ease), box-shadow 0.22s var(--ease),
              background 0.22s var(--ease), border-color 0.22s var(--ease);
}
.place-row:hover {
  background: var(--glass-hover);
  border-color: var(--glass-border-lit);
  transform: translateY(-2px);
  box-shadow: var(--shadow-lg);
}

.rank-num {
  font-size: 19px;
  font-weight: 700;
  color: var(--faint);
  min-width: 32px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.place-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.place-name { font-weight: 600; font-size: 16.5px; letter-spacing: -0.015em; }
.place-meta { color: var(--muted); font-size: 14px; }

.place-score { text-align: right; flex: 0 0 auto; }
.place-score .score,
.place-score .score-sub { display: block; }
.place-score .score-sub { margin-top: 1px; white-space: nowrap; }

.score {
  font-size: 21px;
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.025em;
}
.score.small { font-size: 16px; }
.score-sub { font-size: 12.5px; color: var(--faint); }

/* ------------------------------------------------------------- badges */
.badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }

.badge {
  font-size: 13px;
  color: var(--muted);
  border-radius: 999px;
  padding: 5px 13px;
  white-space: nowrap;
}
.badge-live { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
.badge-warn { color: var(--warn); border-color: var(--warn); }

/* --------------------------------------------------------- place page */
/* Full-bleed banner, faded into the page so the header text below it keeps
   its glass surface rather than sitting on photography. */
.place-hero {
  margin: 0;
  position: relative;
  height: clamp(180px, 26vw, 300px);
  overflow: hidden;
}
.place-hero img { width: 100%; height: 100%; object-fit: cover; }
.place-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgba(22, 24, 15, 0.10), var(--bg-base));
}

.place-header {
  border-width: 0 0 1px;
  border-radius: 0;
  padding: 30px 0;
  margin-top: -1px;
}

.breadcrumb { font-size: 14px; color: var(--muted); margin: 0 0 14px; }
.breadcrumb a { color: var(--muted); text-decoration: none; }
.breadcrumb a:hover { color: var(--accent); }

.page-title {
  font-size: clamp(28px, 4vw, 38px);
  letter-spacing: -0.04em;
  font-weight: 700;
  margin: 0 0 10px;
}
.place-title {
  font-size: clamp(30px, 4.4vw, 40px);
  letter-spacing: -0.04em;
  font-weight: 700;
  margin: 0 0 6px;
}

.score-block { display: flex; align-items: center; gap: 16px; margin: 20px 0 0; }
.score-block .place-meta { display: block; }

.score-big {
  font-size: 46px;
  font-weight: 700;
  color: var(--accent);
  letter-spacing: -0.045em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.provisional { font-size: 13.5px; color: var(--warn); margin: 14px 0 0; }

.sub-heading {
  font-size: 21px;
  font-weight: 600;
  letter-spacing: -0.025em;
  margin: 0 0 12px;
}
.sub-heading.spaced { margin-top: 36px; }

.panel {
  border-radius: var(--radius);
  padding: 28px 30px;
  box-shadow: var(--shadow);
}
.panel > :first-child { margin-top: 0; }

.card {
  border-radius: var(--radius);
  padding: 24px;
  box-shadow: var(--shadow);
}
.card h3 { margin: 0 0 9px; font-size: 17px; font-weight: 600; letter-spacing: -0.02em; }
.card p { margin: 0; color: var(--muted); font-size: 15px; }

a.card { text-decoration: none; display: block; }
a.card:hover { border-color: var(--glass-border-lit); }

/* ------------------------------------------------------------ reviews */
.review { padding: 18px 0; border-bottom: 1px solid var(--glass-border); }
.review:last-child { border-bottom: none; }
.review-head { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; }
.review-author { font-weight: 600; font-size: 15px; }
.review-note { margin: 7px 0 0; color: var(--muted); }

.two-col {
  display: grid;
  grid-template-columns: minmax(0, 1.9fr) minmax(0, 1fr);
  gap: 44px;
  align-items: start;
}

.hours { margin: 0; padding-left: 18px; color: var(--muted); font-size: 14.5px; }
.hours li { margin: 3px 0; }

/* -------------------------------------------------------------- forms */
.form-row { display: flex; gap: 10px; max-width: 470px; }
.center .form-row { margin-inline: auto; }

.input {
  flex: 1 1 auto;
  min-width: 0;
  border-radius: 999px;
  padding: 13px 22px;
  font: inherit;
  color: var(--text);
  box-shadow: var(--shadow);
  transition: border-color 0.16s var(--ease), background 0.16s var(--ease);
}
.input::placeholder { color: var(--faint); }
.input:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--glass-strong);
}

.waitlist-form { margin: 0; }
.form-note { font-size: 14px; margin: 14px 0 0; color: var(--muted); }
.form-note.error { color: var(--warn); }
.form-note[hidden] { display: none; }

.waitlist-done {
  max-width: 470px;
  border-radius: var(--radius);
  padding: 26px 22px;
  box-shadow: var(--shadow);
}
.center .waitlist-done { margin-inline: auto; text-align: center; }
.waitlist-done p { margin: 0 0 6px; color: var(--muted); }
.waitlist-done strong { color: var(--text); }
.waitlist-done code {
  display: inline-block;
  margin-top: 10px;
  padding: 7px 13px;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 13.5px;
  word-break: break-all;
}

/* --------------------------------------------------------------- misc */
.empty {
  text-align: center;
  padding: 44px 24px;
  color: var(--muted);
  border-radius: var(--radius);
  border-style: dashed;
  box-shadow: var(--shadow);
}
.empty a { color: var(--accent); }

.footer {
  margin-top: 40px;
  padding: 30px 24px;
  text-align: center;
  color: var(--faint);
  font-size: 14px;
  border-top: 1px solid var(--glass-border);
}

.muted { color: var(--muted); }
.faint { color: var(--faint); }
.small { font-size: 13px; }
.center { text-align: center; }
.pad { padding: 64px 24px; }
.spaced { margin-top: 18px; }
.spaced-sm { margin-top: 12px; }

/* ------------------------------------------------------------- reveal
   Gated behind .js, which an inline head script sets before first paint.
   Without JavaScript the hidden state is never applied, so content can
   never be stranded invisible behind an animation that will not run. */
.js .reveal {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 1s var(--ease), transform 1s var(--ease);
}
.js .reveal.is-in { opacity: 1; transform: none; }
.js .reveal-delay { transition-delay: 0.16s; }

/* ------------------------------------------------------- simple grid
   Used by the campus index, which is a flat list of equivalent items and
   genuinely wants a uniform grid. */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(248px, 1fr));
  gap: 16px;
  margin-top: 26px;
}

/* --------------------------------------------------------- responsive */
@media (max-width: 960px) {
  .hero-grid { grid-template-columns: 1fr; gap: 40px; }
  .hero-figure { max-width: 460px; }
  .hero-figure img { aspect-ratio: 3 / 2; }
  .two-col { grid-template-columns: 1fr; gap: 30px; }
  .bento { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .bento-wide, .bento-photo { grid-column: span 2; }
  .nav-links a:not(.btn) { display: none; }
}

@media (max-width: 640px) {
  .versus { grid-template-columns: 1fr; }
  .versus-or { justify-self: center; }
  .bento { grid-template-columns: 1fr; }
  .bento-wide, .bento-photo { grid-column: auto; }

  /* One column inside the cell too: a shelf beside the copy at this width
     leaves each about 140px, which is too small to read a room in and too
     narrow to set a sentence in. Shelf on top, copy under it, and the shelf
     goes back to deciding its own height. */
  .bento-wide, .bento-photo { grid-template-columns: 1fr; }
  .bento-photo .shelf { order: 0; }
  .bento-wide .shelf, .bento-photo .shelf {
    height: auto;
    aspect-ratio: 16 / 9;
  }
  .bento-wide .shelf { order: -1; }
  .bento-text { padding: 20px 22px 24px; }
}

@media (max-width: 560px) {
  section, .hero-split, .mechanic, .rail-section, .closer { padding: 48px 0; }
  .panel { padding: 22px 20px; }
  .form-row { flex-direction: column; }
  .input, .btn { border-radius: var(--radius-sm); }
  .rail { padding-inline: 20px; }
  .rail-card { flex-basis: 78vw; }

  .place-row { gap: 12px; padding: 15px 16px; }
  .rank-num { min-width: 20px; font-size: 16px; color: var(--muted); }
  /* A three-line wrapped address turns every row into a paragraph. */
  .place-meta {
    font-size: 13.5px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .place-name { font-size: 15.5px; }
  .score { font-size: 19px; }
  .score-sub { font-size: 11.5px; }

  /* Blur is expensive to composite. */
  :root { --blur: 8px; }
}

/* ------------------------------------------------------- accessibility */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
  .reveal { opacity: 1; transform: none; }
  .place-row:hover, .rail-card:hover, .versus-side:hover,
  .btn-primary:hover { transform: none; }
  /* A hover is a hint and can go. The pick keeps its lift: it is the state,
     not the animation, and the transition is already down to nothing. */
  .versus-side:hover .shelf-art { transform: none; }
}

/* Without blur a 5.5%-opacity panel is invisible, not subtle. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav, .card, .panel, .place-row, .empty, .input, .badge,
  .waitlist-done, .place-header, .bento-cell, .versus-side,
  .rail-card, .btn-ghost {
    background: #fcfcf8;
    border-color: rgba(35, 38, 31, 0.14);
  }
  @media (prefers-color-scheme: dark) {
    .nav, .card, .panel, .place-row, .empty, .input, .badge,
    .waitlist-done, .place-header, .bento-cell, .versus-side,
    .rail-card, .btn-ghost {
      background: #21241b;
      border-color: rgba(236, 238, 229, 0.16);
    }
  }
}

/* Transparency-sensitive users get opaque surfaces. */
@media (prefers-reduced-transparency: reduce) {
  .nav, .card, .panel, .place-row, .empty, .input,
  .waitlist-done, .place-header, .bento-cell, .versus-side, .rail-card {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: #fcfcf8;
  }
  @media (prefers-color-scheme: dark) {
    .nav, .card, .panel, .place-row, .empty, .input,
    .waitlist-done, .place-header, .bento-cell, .versus-side, .rail-card {
      background: #21241b;
    }
  }
}

@media (prefers-contrast: more) {
  :root {
    --glass: rgba(255, 255, 255, 0.97);
    --glass-border: rgba(35, 38, 31, 0.34);
    --muted: #3d4238;
  }
  @media (prefers-color-scheme: dark) {
    :root {
      --glass: rgba(20, 22, 15, 0.96);
      --glass-border: rgba(236, 238, 229, 0.36);
      --muted: #d3d8c8;
    }
  }
}


/* ---------------------------------------------------------------- console
   Internal only, so it borrows the public surfaces rather than introducing a
   second visual system. */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 14px;
  margin-top: 26px;
}
.stat {
  background: var(--glass);
  backdrop-filter: blur(var(--blur)) saturate(var(--sat));
  -webkit-backdrop-filter: blur(var(--blur)) saturate(var(--sat));
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  padding: 20px;
  box-shadow: var(--shadow);
}
.stat b {
  display: block;
  font-size: 30px;
  font-weight: 600;
  letter-spacing: -0.035em;
  line-height: 1.1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.stat span { display: block; font-size: 14px; color: var(--muted); margin-top: 4px; }
.stat em { display: block; font-style: normal; font-size: 12.5px; color: var(--faint); margin-top: 6px; }

.spark { height: 108px; }
.spark svg { width: 100%; height: 100%; display: block; }
.spark rect { fill: var(--accent); opacity: 0.85; }

.admin-cols {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: 24px;
}
@media (max-width: 860px) { .admin-cols { grid-template-columns: 1fr; } }

.admin-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  padding: 9px 0;
  border-bottom: 1px solid var(--glass-border);
  font-size: 14.5px;
}
.admin-row:last-child { border-bottom: none; }

/* Fixed-width tabular numeral so the names on the leaderboard line up whether
   the row is 1st or 10th. */
.admin-rank {
  display: inline-block;
  min-width: 1.8em;
  color: var(--faint);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.admin-gate-title { margin: 0 0 6px; color: var(--text); font-weight: 600; }

/* ------------------------------------------------------------ nav auth
   Populated by auth.js. Empty until the SDK resolves, so the nav must not
   reserve space or jump when it fills. */
.nav-auth { display: inline-flex; align-items: center; }

.nav-auth-btn {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 14.5px;
  color: var(--muted);
  cursor: pointer;
  white-space: nowrap;     /* "Sign up" on two lines doubles the nav's height */
  transition: color 0.16s var(--ease);
}
.nav-auth-btn:hover { color: var(--text); }
.nav-auth-btn:disabled { opacity: 0.55; cursor: default; }

.nav-user { display: inline-flex; align-items: center; gap: 9px; }

.nav-avatar {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid var(--glass-border);
  object-fit: cover;
}

/* The CTA is the one thing that must survive on a narrow screen. With the
   signed-out Sign up beside it, brand + two controls need every one of these
   savings to share 375px on one line. The compact CTA no longer props the bar
   open, so min-height holds the 79px that --nav-h promises (minus the .nav
   border that completes it). */
@media (max-width: 560px) {
  .nav-avatar { display: none; }
  .nav-auth-btn { font-size: 13.5px; }
  .nav-inner { padding: 14px 12px; gap: 12px; min-height: calc(var(--nav-h) - 1px); }
  .nav-links { gap: 12px; }
  .nav-links .btn { padding: 9px 14px; font-size: 13.5px; }
  .brand { font-size: 17px; gap: 7px; }
  .brand-mark { width: 26px; height: 26px; }
}


/* ------------------------------------------------------------ workspace */
.stack { display: flex; flex-direction: column; gap: 14px; }

.field { display: flex; flex-direction: column; gap: 6px; }
.field > span { font-size: 13.5px; color: var(--muted); }

/* Square inputs here: the pill shape belongs to the marketing CTA, and a form
   full of pills reads as a landing page rather than a tool. */
.input.sq { border-radius: var(--radius-sm); padding: 11px 14px; box-shadow: none; }

.field-row { display: flex; gap: 12px; flex-wrap: wrap; }
.field-row > .field { flex: 1 1 150px; }
.field-row.tight { align-items: center; }

.versus-lite { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 6px; }
.versus-lite .btn { flex: 1 1 200px; }

#findResults .place-row,
#myList .place-row { margin-bottom: 10px; }

@media (max-width: 560px) {
  .field-row { flex-direction: column; }
  .field-row > .field { flex: 1 1 auto; }
}

/* ============================================================================
   Keeping the room: everything below the hero
   ----------------------------------------------------------------------------
   The hero is a lit claymation room. Under it the page was type on a flat
   wash and a set of empty glass rectangles, which read as a second product
   bolted to the bottom of the first one. Three things carry the room down:

   1. **Lamplight.** A warm pool behind each section, in the amber of the
      pendant lamps in the panorama. The wash is matcha and clay and has no
      warm note at all on its own, so the join was a colour-temperature cut.

   2. **Shelves.** The four dioramas in static/img are transparent cutouts of
      this same world, already shipped for /app. Each sits in a lit recess
      rather than floating on the page: they are photographed objects, and an
      object wants something to stand on and a shadow to cast.

   3. **Clay edges.** A chunkier radius and one warm highlight along the top
      of every card. A pressed surface is lit from above; a glass one has a
      specular streak. At card size that is most of the material read.

   A shelf is a dark, warm, lit room in BOTH themes, exactly as the hero is,
   so its values are fixed rather than tokens. A cream recess was the first
   attempt and it turned every card on the dark theme into a bright rectangle
   with a diorama pasted onto it -- the models are lit warm from above and
   need a room around them, not a swatch behind them.
   ============================================================================ */

.lit { position: relative; }

/* Behind the content but above body::before, which sits at -2. */
.lit::before {
  content: "";
  position: absolute;
  inset: -6% 0 auto;
  height: 78%;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(58% 100% at 50% 0%, var(--lamp), transparent 70%);
}

.shelf {
  display: block;
  position: relative;
  overflow: hidden;
  border-radius: calc(var(--radius-clay) - 8px);
  aspect-ratio: 16 / 11;
  /* Wall, then a horizon, then a floor -- and one lamp above it. The stop at
     56% is a hair soft so it reads as where two surfaces meet rather than as
     a seam in a gradient. */
  background:
    radial-gradient(72% 64% at 50% 2%, rgba(255, 198, 124, 0.42), transparent 72%),
    linear-gradient(to bottom,
      #2c2418 0%, #272016 54%,
      #1d1710 56%, #15100a 100%);
}

/* The model's contact shadow, under the model: ::before paints before the
   positioned <img>, so the figure lands on top of its own shadow. */
.shelf::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 12%;
  width: 52%;
  height: 9%;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(6, 4, 2, 0.66), transparent);
}

/* The floor, drawn over the model rather than behind it, so the bottom of the
   figure is in shadow the way anything standing on a dark floor is.
   ::after paints last, which is what puts it in front. */
.shelf::after {
  content: "";
  position: absolute;
  inset: auto 0 0;
  height: 30%;
  pointer-events: none;
  background: linear-gradient(to top,
    #15100a 30%, rgba(21, 16, 10, 0.58) 62%, transparent);
}

.shelf-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* The renders keep a faint pale residue of the contact shadow they were made
     with -- scripts/prepare_scenes.py takes most of it out on chroma and some
     survives, and on a dark surface what survives is a visible crescent. The
     floor above covers it; this fade only softens the cut edge so the two
     meet without a line. */
  -webkit-mask-image: linear-gradient(to bottom, #000 82%, transparent 97%);
  mask-image: linear-gradient(to bottom, #000 82%, transparent 97%);
}

/* ============================================================================
   The cafe: the hero
   ----------------------------------------------------------------------------
   One claymation panorama of a room with four tables in it. The camera opens
   wide with the headline, then pans and pushes in on each table in turn.

   One image, not four. The argument of this section is that these people are
   in the *same room* and still need different things, and cutting between
   separate pictures would throw that away.

   The pan and the push-in sit on different elements. A single transform
   composes scale with translate, so sharing one would make the pan distance
   change with the zoom and the tables would stop landing centred.

   Nothing listens to scroll. The reference this follows called preventDefault
   on wheel events, which takes the scrollbar away from the whole page; a
   view-timeline gets the same camera and leaves scrolling alone.

   This section is deliberately dark in both themes. It is a lit room at dusk,
   and a light treatment would mean a second panorama.
   ============================================================================ */

.cafe { padding: 0 0 24px; }

/* ------------------------------------------------------------- base layout
   The panorama at rest, with the four tables listed under it. This is what
   ships under reduced motion and without scroll-driven animation. */

.cafe-track { max-width: var(--maxw); margin: 0 auto; padding: 0 24px; }

.cafe-stage {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  background: #17140d;
}

.cafe-lens { display: block; }

.cafe-pano {
  display: block;
  width: 100%;
  height: auto;
}

.cafe-vignette,
.cafe-grain { position: absolute; inset: 0; pointer-events: none; }

/* Warm scrim: the copy sits on the lower left of a photographic image, so it
   needs a guaranteed floor rather than whatever pixels happen to be there. */
.cafe-vignette {
  background:
    /* Both sides, not just the left: table one's caption sits on the right to
       stay off the character it describes, and needs the same floor. */
    linear-gradient(90deg, rgba(18, 16, 10, 0.74), transparent 42%,
                           transparent 58%, rgba(18, 16, 10, 0.68)),
    linear-gradient(0deg, rgba(13, 10, 6, 0.82), transparent 54%);
}

/* Above the vignette, not below it: underneath, the gradient smooths out the
   very texture the grain is there to add. */
.cafe-grain {
  opacity: 0.15;
  mix-blend-mode: soft-light;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 140 140' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.9'/%3E%3C/svg%3E");
}

/* Fixed light values, not tokens: this copy always sits on a dark lit room,
   in both themes, so it must not follow the page from light to dark. */
.cafe-intro { position: relative; padding: 26px 24px 8px; }

.cafe-eyebrow {
  font-size: 13px;
  font-style: italic;
  letter-spacing: 0.01em;
  color: #cfd8b8;
  margin: 0 0 10px;
}

.cafe-intro h1 {
  font-size: clamp(34px, 5.6vw, 68px);
  line-height: 0.98;
  letter-spacing: -0.04em;
  color: #f6f4e6;
  margin: 0 0 16px;
  text-wrap: balance;
}

.cafe-lede {
  font-size: clamp(15px, 1.6vw, 18px);
  line-height: 1.55;
  color: #cdd3bd;
  margin: 0 0 24px;
  max-width: 46ch;
}

.cafe-table { padding: 22px 24px 0; }

.cafe-seat {
  font-size: 10.5px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: #a8b393;
  margin: 0 0 8px;
}

.cafe-who {
  font-size: clamp(21px, 3vw, 38px);
  line-height: 1.02;
  letter-spacing: -0.035em;
  color: #f6f4e6;
  margin: 0 0 8px;
  text-wrap: balance;
}

.cafe-needs {
  font-size: clamp(14.5px, 1.5vw, 17px);
  line-height: 1.55;
  color: #cdd3bd;
  margin: 0;
  max-width: 42ch;
  text-wrap: pretty;
}

.cafe-progress { display: none; }

.cafe-outro { padding-block: 44px 0; max-width: 720px; }

.cafe-punch {
  font-size: clamp(18px, 2.2vw, 24px);
  line-height: 1.45;
  letter-spacing: -0.015em;
  color: var(--text);
  margin: 0;
  text-wrap: balance;
}

/* --------------------------------------------------------------- the camera
   `contain 0%` to `contain 100%` is exactly the window in which the track
   fully covers the viewport, which is exactly when the sticky stage is pinned:
   the pan starts as the scene locks and ends as it releases. */

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {

    .cafe-track {
      max-width: none;
      padding: 0;
      height: 520vh;        /* one wide shot, four tables, and the moves between */
      view-timeline: --cafe block;
      /* The nav is sticky, so without this the stage starts one nav-height
         down the document and its bottom sits below the fold. The nav is
         translucent, so the room sliding under it is the right look anyway. */
      margin-top: calc(-1 * var(--nav-h, 79px));
    }

    .cafe-stage {
      position: sticky;
      top: 0;
      height: 100dvh;       /* never 100vh: the iOS bar would jump the pin */
      border-radius: 0;
    }

    .cafe-lens {
      position: absolute;
      inset: 0;
      overflow: hidden;
      animation: cafe-push linear both;
      animation-timeline: --cafe;
      animation-range: contain 0% contain 100%;
      will-change: transform;
    }

    /* Tall enough that there is room to pan, and never narrower than the
       viewport on a wide monitor. `top` centres it; the transform is left free
       for the pan alone. */
    .cafe-pano {
      position: absolute;
      top: -14%;
      left: 0;
      height: 128%;
      width: auto;
      min-width: 128vw;
      max-width: none;
      object-fit: cover;
      animation: cafe-pan linear both;
      animation-timeline: --cafe;
      animation-range: contain 0% contain 100%;
      will-change: transform;
    }

    .cafe-intro,
    .cafe-table {
      position: absolute;
      left: clamp(20px, 6vw, 88px);
      bottom: clamp(64px, 12vh, 132px);
      right: clamp(20px, 6vw, 88px);
      max-width: 560px;
      padding: 0;
      z-index: 2;
      opacity: 0;
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: --cafe;
    }

    .cafe-intro {
      max-width: 640px;
      animation-name: cafe-intro-out;
      animation-range: contain 0% contain 22%;
    }

    /* Each table's copy holds inside its own dwell, and is gone before the
       camera starts moving again: two blocks of text in one position would
       interleave into garbage mid-move. */
    .cafe-table { animation-name: cafe-copy-in; }

    /* Table one sits at the far left of the room. The panorama is only about
       1.6 viewports wide, so that character cannot be brought to the middle of
       the frame at any pan position: it is pinned near the left edge, which is
       where the copy lives by default. So this one caption moves to the right
       instead of sitting on top of the person it describes. */
    .cafe-table-1 {
      left: auto;
      right: clamp(20px, 6vw, 88px);
      text-align: right;
      margin-left: auto;
      animation-range: contain 18% contain 38%;
    }

    .cafe-table-1 .cafe-needs { margin-left: auto; }
    .cafe-table-2 { animation-range: contain 38% contain 58%; }
    .cafe-table-3 { animation-range: contain 58% contain 78%; }
    .cafe-table-4 { animation-range: contain 78% contain 100%; }

    .cafe-progress {
      display: block;
      position: absolute;
      inset: auto 0 0 0;
      height: 2px;
      margin: 0;
      z-index: 3;
    }

    .cafe-progress span {
      display: block;
      height: 100%;
      background: var(--accent);
      transform-origin: left;
      transform: scaleX(0);
      animation: cafe-progress linear both;
      animation-timeline: --cafe;
      animation-range: contain 0% contain 100%;
    }

    .cafe-outro { padding-block: 60px 0; }
  }
}

/* The pan.
   
   `calc(P*1vw - P%)` is the arithmetic `background-position: P%` does: it maps
   the P point of the image onto the P point of the frame, so 0% puts the left
   edges together and 100% the right edges. Percentages resolve against the
   image's own width, so this holds at any viewport size and needs no clamp.

   Centring each table instead was the obvious first try and it does not work.
   The panorama is only about 1.6 viewports wide, so the only points that can
   physically reach the middle of the frame are the ones between roughly 31%
   and 69%; both end tables clamped to a stop and the camera sat still through
   half the scroll. Reaching them would need a 6x zoom into a 1821px image.

   So the end tables sit near the edges of the frame, which is what the
   reference does too. It reads fine: the faces are around 40% of the frame
   height and the copy is at the very bottom, so they do not fight. */
@keyframes cafe-pan {
  0%,   14%  { transform: translate3d(calc(20vw - 20%), 0, 0); }   /* wide, three tables in view */
  24%,  36%  { transform: translate3d(calc(6vw  - 6%),  0, 0); }
  44%,  56%  { transform: translate3d(calc(38vw - 38%), 0, 0); }
  64%,  76%  { transform: translate3d(calc(68vw - 68%), 0, 0); }
  86%, 100%  { transform: translate3d(calc(96vw - 96%), 0, 0); }
}

/* Wide for the opening line, then in on each table. Pulling back a little to
   travel is what makes it read as one continuous room rather than four cuts. */
@keyframes cafe-push {
  0%,  14%  { transform: scale(1); }
  24%, 36%  { transform: scale(1.14); }
  40%       { transform: scale(1.04); }
  44%, 56%  { transform: scale(1.14); }
  60%       { transform: scale(1.04); }
  64%, 76%  { transform: scale(1.14); }
  80%       { transform: scale(1.04); }
  86%, 100% { transform: scale(1.14); }
}

@keyframes cafe-intro-out {
  0%, 66%   { opacity: 1; transform: translate3d(0, 0, 0); }
  92%, 100% { opacity: 0; transform: translate3d(0, -20px, 0); }
}

@keyframes cafe-copy-in {
  0%, 14%   { opacity: 0; transform: translate3d(0, 18px, 0); }
  26%, 74%  { opacity: 1; transform: translate3d(0, 0, 0); }
  88%, 100% { opacity: 0; transform: translate3d(0, -14px, 0); }
}

@keyframes cafe-progress {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

@media (max-width: 700px) {
  .cafe-track { padding-inline: 16px; }

  @media (prefers-reduced-motion: no-preference) {
    @supports (animation-timeline: view()) {
      .cafe-track { height: 460vh; padding-inline: 0; }
      /* A 2.1:1 panorama in a portrait viewport: let it get taller so faces
         are legible, and accept a harder crop top and bottom. */
      .cafe-pano { height: 116%; top: -8%; min-width: 210vw; }
      .cafe-intro h1 { font-size: clamp(30px, 8.6vw, 40px); }
      .cafe-lede { font-size: 15px; margin-bottom: 18px; }
      .cafe-intro, .cafe-table { bottom: clamp(40px, 7vh, 72px); }
    }
  }
}

/* ------------------------------------------------------- policy pages
   /privacy, /terms, /support. Prose measure and rhythm only — the frame is
   the same container every other page uses. */
.legal { max-width: 720px; }
.legal h2 { font-size: 21px; letter-spacing: -0.02em; margin: 34px 0 10px; }
.legal h3 { font-size: 16.5px; margin: 24px 0 6px; }
.legal p, .legal li { font-size: 15.5px; line-height: 1.65; color: var(--muted); }
.legal ul { padding-left: 22px; margin: 10px 0; }
.legal li { margin: 6px 0; }
.legal li strong, .legal p strong { color: var(--text); font-weight: 600; }
.legal a { color: var(--accent); }
.legal-updated { font-size: 13.5px; color: var(--muted); margin: -2px 0 26px; }
.legal-footer { margin-top: 44px; font-size: 14px; }

/* ------------------------------------------------------- invite page
   /join/<code>. One panel, and the only thing on it that has to carry any
   weight is the row of faces: an invite is a claim that people you know are
   already here, and the faces are the evidence for it. */
/* This page has one job and it is the button. The site's standard section
   rhythm — 48px on the section plus 64px on the container — puts it under the
   fold on a small phone, which on every other page costs a scroll and here
   costs the invite. */
.join-section { padding-top: 0; }
.join-page { padding: 40px 24px 64px; }

.join-card { max-width: 560px; }

.join-faces {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 16px;
  list-style: none;
  margin: 0 0 18px;
  padding: 0;
}
.join-face {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  color: var(--muted);
}
.join-face img,
.join-initial {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  flex: none;
  object-fit: cover;
  border: 1px solid var(--glass-border);
}
/* Not every provider hands back a photo, and a gap where a face should be
   reads as a broken image rather than as a person without one. */
.join-initial {
  display: grid;
  place-items: center;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 14px;
  font-weight: 600;
}
.join-name {
  max-width: 12ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.join-count {
  margin: 0 0 20px;
  font-size: 14px;
  color: var(--faint);
}

.join-action { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
/* The note is the full width of the panel under the buttons, not a third
   thing competing with them for the row. */
.join-action .form-note { flex-basis: 100%; margin-top: 4px; }

.join-why { max-width: 560px; margin-top: 44px; }
