/* Lingogram site — token-based styles. Light theme is the daylight brand;
   dark theme redefines the tokens only (never style inside the media query). */
:root {
  color-scheme: light dark;
  --ground: #f6f5fa;
  --panel: #ffffff;
  --ink: #17131f;
  --muted: #5d5870;
  --line: #e4e1ee;
  --accent: #5b3fbf;
  --accent-ink: #ffffff;
  --accent-soft: #ece7fb;
  --teal: #14a97c;
  --teal-deep: #0b7a5c;
  --teal-soft: #e3f6ee;
  --band: #17131f;
  --band-panel: #221d2e;
  --band-ink: #f5f3fa;
  --band-muted: #a49fb5;
  --band-line: #37313f;
  --player: #16141d;
  --player-panel: #201d29;
  /* The demo's tab strip sits BEHIND its raised active tab, so it needs to be
     a shade off --panel (which the active tab uses) or the tab has nothing to
     lift off. Between --ground and --panel in both themes. */
  --chrome: #eeecf4;
  --demo-edge: rgba(255, 255, 255, 0.08);
  --shadow: 0 24px 60px -30px rgba(35, 20, 90, 0.35);
  --star: #f5a623;
}
@media (prefers-color-scheme: dark) {
  :root {
    --ground: #0f0b1e; --panel: #1e1740; --ink: #f1eefa; --muted: #aaa3c4;
    --line: #352c63; --accent: #a390ff; --accent-ink: #17112e; --accent-soft: #2b2260;
    --teal: #3adfa5; --teal-deep: #3adfa5; --teal-soft: #12382c;
    --band: #080614; --band-panel: #161028; --band-line: #2b2344;
    --player: #0d0b16; --player-panel: #161320; --chrome: #140d30;
    --dtab-lip: rgba(163, 144, 255, 0.32);
    --demo-edge: rgba(150, 130, 255, 0.35);
    --shadow: 0 24px 70px -28px rgba(80, 50, 220, 0.45);
  }
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
body {
  margin: 0; background: var(--ground); color: var(--ink);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  line-height: 1.55; -webkit-font-smoothing: antialiased;
  /* Lets .demo size itself with cqw units: unlike 100vw, 100cqw excludes the
     scrollbar, so the breakout card never overflows under a classic scrollbar. */
  container-type: inline-size;
}
img, svg { max-width: 100%; }
a { color: var(--accent); }
a:focus-visible, button:focus-visible, summary:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px;
}

.wrap { max-width: 1060px; margin: 0 auto; padding: 0 24px; }

/* ---------- header ---------- */
/* The element is `class="site wrap"`, and `header.site` outranks `.wrap`, so
   a `padding: 20px 0` shorthand here silently drops .wrap's 24px gutter —
   below 1060px there is no leftover margin to hide it and the logo/buttons
   sit flush against both screen edges. Set the block padding only. */
header.site { display: flex; align-items: center; gap: 18px; padding-block: 20px; }
.logo { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 19px; letter-spacing: -0.02em; margin-right: auto; color: var(--ink); text-decoration: none; }
.logo-mark {
  width: 38px; height: 38px; border-radius: 11px; flex: none;
  display: grid; place-items: center;
}
/* The logo art supplies its own backdrop — fill the box, keep its corners. */
.logo-mark .logo-img { width: 100%; height: 100%; border-radius: inherit; display: block; }
nav.top { display: flex; gap: 24px; font-size: 14.5px; }
/* nowrap: a label that breaks mid-phrase ("Cómo / funciona") makes the whole
   row two lines tall and reads as ragged next to the ones that still fit.
   Failing visibly-tight beats failing silently-ragged — if a future locale
   is too long, it shows up as a cramped header rather than a broken one.
   The nav labels are audited for length across all 42 locales; see the
   header note in i18n/en.json's nav block. */
nav.top a { color: var(--muted); text-decoration: none; font-weight: 500; white-space: nowrap; }
nav.top a:hover { color: var(--ink); }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 10px; border-radius: 12px; padding: 14px 24px; font-size: 16px; font-weight: 700; text-decoration: none; border: 1px solid transparent; cursor: pointer; }
.btn-primary { background: var(--accent); color: var(--accent-ink); box-shadow: 0 10px 30px -12px var(--accent); }
.btn-primary:hover { filter: brightness(1.08); }
.btn-primary svg { flex: none; }
.btn-ghost { background: var(--panel); color: var(--ink); border-color: var(--line); }
.btn-ghost:hover { border-color: var(--accent); }
/* nowrap at every width, not just the narrow breakpoints below: "Iniciar
   sesión" was breaking in two on a 1280px desktop, which made the whole
   header row taller than the nav beside it. */
.btn-login { padding: 9px 18px; font-size: 14px; white-space: nowrap; }
/* The language switcher. TWO controls, one shown at a time (see the 760px
   block near the bottom): .lang-wrap is the DESKTOP popover, filled by
   demo.js and [hidden] until it is; .lang-link is the phone/tablet link to
   /languages/, server-rendered so it also covers the auth pages. */
.lang-wrap { position: relative; display: inline-flex; flex: none; }
.lang-wrap[hidden] { display: none; }
.lang-face {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--line); background-color: var(--panel); color: var(--ink);
  border-radius: 999px; padding: 8px 11px 8px 13px; cursor: pointer;
  font: inherit; font-size: 13.5px; font-weight: 600; line-height: 1; white-space: nowrap;
  max-width: 150px;
}
.lang-face .lf-name { overflow: hidden; text-overflow: ellipsis; }
/* Explicit size and flex:none. An inline SVG with only a viewBox takes an
   aspect-ratio box off whatever room the flex line gives it, so in locales
   with a short autonym the chevron ballooned to 39px and pushed the whole
   header taller (worst in sl/nl/uk/bg/sk). */
.lang-face .lf-chev {
  width: 12px; height: 12px; flex: none; opacity: .65;
  transition: transform .15s ease;
}
.lang-wrap.open .lang-face .lf-chev { transform: rotate(180deg); }
.lang-wrap:hover .lang-face,
.lang-wrap:focus-within .lang-face,
.lang-wrap.open .lang-face { border-color: var(--accent); }

.lang-panel {
  position: absolute; top: calc(100% + 8px); inset-inline-end: 0; width: 280px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
  box-shadow: 0 24px 60px -20px rgba(0, 0, 0, .45);
  opacity: 0; pointer-events: none; transform: translateY(-6px) scale(.98);
  transition: opacity .14s ease, transform .14s ease;
  z-index: 40; overflow: hidden;
}
/* No `visibility` here on purpose: it is a transitioned property, so it only
   flips at the END of the transition, and a still-hidden element refuses
   focus() the whole time — the search input would never take keyboard focus
   on open. opacity + pointer-events hide it just as completely while leaving
   it focusable the instant `.open` is set. */
.lang-wrap.open .lang-panel { opacity: 1; pointer-events: auto; transform: translateY(0) scale(1); }
.lang-search-row { padding: 10px; border-bottom: 1px solid var(--line); }
.lang-search {
  width: 100%; border: 1px solid var(--line); background: var(--ground); color: var(--ink);
  border-radius: 10px; font: inherit; font-size: 14px; outline: none;
  padding-block: 9px; padding-inline: 34px 12px;
  background-repeat: no-repeat; background-position: 11px center; background-size: 15px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23697086' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
}
[dir="rtl"] .lang-search { background-position: right 11px center; }
.lang-search:focus { border-color: var(--accent); }
.lang-list { max-height: 320px; overflow-y: auto; padding: 6px; }
.lang-group-label {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); padding: 10px 10px 6px;
}
.lang-option {
  display: flex; align-items: center; width: 100%; border: none; background: none;
  text-align: start; padding: 9px 10px; border-radius: 9px; cursor: pointer; color: var(--ink);
  font: inherit; font-size: 14px; font-weight: 500;
}
.lang-option:hover,
.lang-option:focus-visible { background: var(--accent-soft); }
.lang-option .code { margin-inline-start: auto; font-size: 11px; font-weight: 700; color: var(--muted); }
.lang-option.current { color: var(--accent); }
.lang-option.current::after { content: "✓"; margin-inline-start: auto; font-weight: 700; }
.lang-option.current .code { display: none; }
.lang-empty { padding: 24px 14px; text-align: center; color: var(--muted); font-size: 13px; }

/* The link to /languages/. This is the control that ALWAYS works:
   server-rendered, no script, present on the auth pages too. It is visible
   by default at every width, and only steps aside on desktop once demo.js
   has actually built the popover (.lang-ready on <html>, set at the end of
   wireLangSwitch). Written this way round deliberately — hiding it with
   plain CSS and revealing the popover with script means any failure of
   demo.js leaves a desktop visitor with no way at all to change language,
   which is exactly the bug this replaced. No flash in practice: demo.js is
   `defer`, so .lang-ready lands before first paint. */
.lang-link {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  border: 1px solid var(--line); background-color: var(--panel); color: var(--ink);
  border-radius: 999px; padding: 8px 12px; text-decoration: none;
  font-size: 13px; font-weight: 700; line-height: 1; white-space: nowrap;
}
@media (min-width: 761px) {
  .lang-ready .lang-link { display: none; }
}
.lang-link:hover { border-color: var(--accent); color: var(--accent); }
.lang-globe { width: 14px; height: 14px; flex: none; opacity: .7; }
.lang-link:hover .lang-globe { opacity: 1; }

/* ---------- /languages/ ---------- */
/* Gap, not margins: the result count and the empty state come and go as you
   type, and margin-based spacing leaves a hole behind whichever is hidden. */
.lang-page {
  display: flex; flex-direction: column; align-items: stretch;
  gap: 26px; padding-bottom: 70px;
}
.lang-head { max-width: 60ch; margin: 0; }
.lang-head h1 { font-size: clamp(28px, 5vw, 42px); line-height: 1.1; letter-spacing: -0.03em; margin: 0 0 10px; }
.lang-head .sub { color: var(--muted); font-size: 17px; margin: 0; }
.lang-current-row {
  background: var(--accent-soft); color: var(--ink); border-radius: 12px;
  padding: 13px 16px; margin: 0; font-size: 14.5px;
}
.lang-current-row b { color: var(--accent); }

/* Injected by main.js — see the note in build.mjs's languagesPage. */
.lang-search-wrap { position: relative; max-width: 420px; }
.lang-search-field {
  width: 100%; border: 1px solid var(--line); background: var(--panel); color: var(--ink);
  border-radius: 11px; font: inherit; font-size: 15.5px; outline: none; min-height: 48px;
  padding-block: 13px; padding-inline: 42px 40px;
  background-repeat: no-repeat; background-position: 14px center; background-size: 17px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23697086' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
}
/* background-position has no logical equivalent, so mirror it by hand. */
[dir="rtl"] .lang-search-field { background-position: right 14px center; }
.lang-search-field:focus { border-color: var(--accent); }
.lang-search-clear {
  position: absolute; inset-inline-end: 6px; top: 50%; transform: translateY(-50%);
  border: 0; background: none; cursor: pointer; color: var(--muted);
  font-size: 22px; line-height: 1; padding: 8px 11px; border-radius: 8px; display: none;
}
.lang-search-wrap.has-query .lang-search-clear { display: block; }
.lang-search-clear:hover { color: var(--ink); }

#lang-regions { display: flex; flex-direction: column; gap: 30px; }
/* Overrides the bare `section { padding: 76px 0 0 }` further down, which
   spaces the home page's full-width bands — far too much between four short
   lists, and it would stack on top of the flex gap. */
.lang-region { margin: 0; padding: 0; }
.lang-region[hidden] { display: none; }
.lang-region h2 {
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.09em;
  color: var(--muted); margin: 0; padding-bottom: 9px; border-bottom: 1px solid var(--line);
}
.lang-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(196px, 1fr));
  gap: 2px; margin-top: 8px;
}
.lang-entry {
  display: flex; align-items: baseline; gap: 9px; text-decoration: none; color: var(--ink);
  padding: 11px 12px; border-radius: 9px; min-height: 44px;
}
.lang-entry[hidden] { display: none; }
.lang-entry:hover { background: var(--accent-soft); color: var(--accent); }
.lang-entry .le-name { font-size: 16px; font-weight: 500; }
.lang-entry .le-en {
  font-size: 12.5px; color: var(--muted); margin-inline-start: auto;
  text-align: end; white-space: nowrap;
}
.lang-entry:hover .le-en { color: var(--accent); }
.lang-entry.is-current { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.lang-entry.is-current::after { content: "✓"; margin-inline-start: 6px; font-weight: 700; }
.lang-count { font-size: 13px; color: var(--muted); margin: 0; }
.lang-no-match {
  padding: 34px 20px; text-align: center; color: var(--muted);
  border: 1px dashed var(--line); border-radius: 12px;
}
.lang-no-match b { color: var(--ink); display: block; margin-bottom: 4px; }
.mnav { display: none; position: relative; }

/* ---------- hero ---------- */
.hero { text-align: center; padding: 34px 0 8px; }
.eyebrow {
  display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--line); background: var(--panel);
  border-radius: 999px; padding: 7px 16px; font-size: 12px; font-weight: 700; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--accent);
}
.eyebrow::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
h1 { font-size: clamp(38px, 6.2vw, 62px); line-height: 1.05; letter-spacing: -0.035em; font-weight: 800; margin: 22px auto 30px; text-wrap: balance; }
h1 .pop { color: var(--accent); }
.sub { max-width: 56ch; margin: 0 auto 26px; font-size: 18px; color: var(--muted); text-wrap: balance; }
.cta-row { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; align-items: center; }
.pillars {
  list-style: none; margin: 0 auto 30px; padding: 0; max-width: 880px;
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px 30px; text-align: center;
}
.pillars li { font-size: 14.5px; color: var(--muted); line-height: 1.45; }
.pillars b {
  display: block; color: var(--ink); font-size: 15.5px; font-weight: 700; margin-bottom: 3px;
}
.pillars b::before { content: "✓ "; color: var(--teal-deep); }
.proof { display: flex; gap: 10px 22px; justify-content: center; align-items: center; flex-wrap: wrap; margin: 20px 0 0; font-size: 14px; color: var(--muted); font-weight: 550; }
.proof .stars { color: var(--star); letter-spacing: 2px; font-size: 15px; }
.proof b { color: var(--ink); }
.proof .sep { color: var(--line); }

/* ---------- demo player ---------- */
/* Wide enough that the 320px sidebar (the extension's real width) leaves a
   16:9 stage with room to breathe: at 940px the video flattened into a strip
   and the panel read as oversized next to it. The demo is the one block that
   outgrows .wrap, so it breaks out of it rather than widening the page's
   text column — clamped to the viewport so it never forces a scrollbar. */
.demo {
  border-radius: 18px; overflow: hidden;
  box-shadow: var(--shadow); border: 1px solid var(--line);
  width: min(1120px, calc(100cqw - 48px));
  max-width: none;
  /* Wider than .wrap, so auto margins resolve to 0 and the card would hug the
     column's left edge. Center it on the viewport by hand: parent is centered,
     so (100% - own width) / 2 is the (negative) margin that re-centers.
     cqw (from body, sans scrollbar), not vw: vw would overflow + clip the
     right edge whenever a classic scrollbar is visible. */
  margin: 40px 0 0 calc((100% - min(1120px, 100cqw - 48px)) / 2);
}
/* The chrome is a window title bar, not a toolbar row: tabs stand ON its
   bottom edge, so it aligns to flex-end and carries no bottom padding —
   the active tab's own background closes the gap down to the player. */
.demo-chrome {
  background: var(--chrome); padding: 10px 16px 0; display: flex; align-items: flex-end;
  gap: 12px; border-bottom: 1px solid var(--line); font-size: 12.5px; color: var(--muted);
}
.demo-chrome .dots { display: flex; gap: 8px; flex: none; padding-bottom: 11px; }
.dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }

/* ---------- demo browser tabs ---------- */
/* The three sites Lingogram runs on, drawn as browser tabs. Decorative: no
   hover, no pointer, aria-hidden on the container — the active tab is marked
   by shape (raised, fused with the panel below), never by an accent bar,
   which would read as a switch the visitor can operate. */
.dtabs { display: flex; align-items: flex-end; min-width: 0; }
.dtab {
  display: flex; align-items: center; gap: 7px; padding: 8px 16px;
  border-radius: 11px 11px 0 0; font-size: 12.5px; font-weight: 600;
  color: var(--muted); white-space: nowrap; position: relative; top: 1px;
}
/* --dtab-lip is a hairline along the tab's own top edge. On a dark ground the
   raised tab is only ~1.1:1 against the strip behind it, so the silhouette
   alone is too faint to read; the lip restores the lift the light theme gets
   for free from white-on-grey. Transparent in the light theme, where the
   contrast is already there. */
.dtab.is-on {
  background: var(--panel); color: var(--ink); font-weight: 700;
  box-shadow: inset 0 1px 0 var(--dtab-lip, transparent);
}
/* The concave corners that let a real Chrome tab spill into the bar it sits
   on. Two radial gradients, so it costs no markup and follows the theme. */
.dtab.is-on::before, .dtab.is-on::after {
  content: ""; position: absolute; bottom: 0; width: 10px; height: 10px;
}
.dtab.is-on::before { left: -10px; background: radial-gradient(circle at 0 0, transparent 10px, var(--panel) 10.5px); }
.dtab.is-on::after { right: -10px; background: radial-gradient(circle at 100% 0, transparent 10px, var(--panel) 10.5px); }
/* Hairline between two sleeping tabs, as in a real browser. Only between
   them: a divider touching the active tab would cut its concave corner. */
.dtab:not(.is-on) + .dtab:not(.is-on)::before {
  content: ""; position: absolute; left: 0; top: 9px; bottom: 9px; width: 1px;
  background: var(--muted); opacity: 0.25;
}
.dtab:not(.is-on) .dfav { opacity: 0.75; }
/* Site favicons — the real logos (SITE_LOGO in build.mjs), same marks as the
   edition cards further down the page. The SVG carries the brand colour and
   its own rounded tile, so nothing is painted here but the box. */
.dfav { width: 15px; height: 15px; flex: none; display: block; }
.dfav svg { display: block; width: 100%; height: 100%; }

/* Shown in place of the demo when the installed extension owns the page's
   #vtt-* ids. Sized like the player it replaces so the hero doesn't collapse. */
.demo-conflict {
  margin: 0; padding: 48px 32px; min-height: 220px;
  display: flex; flex-direction: column; justify-content: center;
  text-align: center; color: var(--muted); font-size: 15px; line-height: 1.6;
}
.demo-conflict b { display: block; color: var(--ink); font-size: 17px; margin-bottom: 6px; }

.demo-hint { text-align: center; font-size: 13.5px; color: var(--muted); margin-top: 14px; }
.demo-hint b { color: var(--ink); }

/* ---------- mode explainer ----------
   Page furniture, NOT part of the product: the demo frame above is the
   extension itself, so everything explaining it stays outside that frame and
   speaks the page's visual language (tokens, light/dark aware). Underline
   tabs, an ANIMATED miniature — a dark "screenshot of the product" inside
   page copy — and short benefit text. Two-way coupled to the demo
   (src/demo/index.ts). No box around it: boxes read as widgets. */
.mslider { max-width: 860px; margin: 34px auto 0; text-align: left; }
.mtabs {
  display: flex; gap: 26px; justify-content: center; flex-wrap: wrap;
  border-bottom: 1px solid var(--line);
}
.mtab {
  position: relative; display: flex; align-items: center; gap: 8px;
  cursor: pointer; border: none; background: none;
  color: var(--muted); font: inherit; font-size: 14px; font-weight: 650;
  padding: 4px 2px 11px;
  transition: color 0.15s;
}
.mtab svg { width: 16px; height: 16px; flex: none; }
.mtab:hover { color: var(--ink); }
.mtab.is-active { color: var(--accent); }
/* The underline is the active marker AND the auto-advance countdown: while
   the slider self-advances it fills over the interval; once the visitor takes
   over (.m-no-auto) it sits full — a plain active indicator. It re-triggers
   naturally as is-active moves between tabs. */
.mtab.is-active::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px;
  border-radius: 2px; background: var(--accent);
  /* --tab-auto is set from JS on phones, where each tab's film has its own
     length and the countdown must fill over exactly one loop of it. */
  transform-origin: left; animation: tabAuto var(--tab-auto, 6.5s) linear forwards;
}
.mslider.m-no-auto .mtab.is-active::after { animation: none; }
@keyframes tabAuto { from { transform: scaleX(0); } to { transform: scaleX(1); } }
/* Slides stack in one grid cell; the active one fades in. */
.mslides { display: grid; padding-top: 22px; }
.mslide {
  grid-area: 1 / 1; display: flex; gap: 22px; align-items: center;
  opacity: 0; visibility: hidden; transition: opacity 0.35s ease;
}
.mslide.is-active { opacity: 1; visibility: visible; }
.mtext h3 { margin: 0 0 5px; font-size: 17px; letter-spacing: -0.01em; color: var(--ink); }
.mtext p { margin: 0; color: var(--muted); font-size: 14.5px; line-height: 1.55; max-width: 54ch; }
/* Miniature of the player — the same visual language as the real overlay
   (white main line, yellow translation, dark pill). Dark in both themes: it
   is a picture OF the product, and the product is dark. */
.mviz {
  position: relative; display: block; flex: none;
  width: 264px; aspect-ratio: 16 / 8.5;
  border-radius: 12px; overflow: hidden;
  box-shadow: 0 10px 30px -14px rgba(20, 10, 60, 0.55);
  background:
    radial-gradient(120% 130% at 20% 0%, #2b2352 0%, transparent 55%),
    radial-gradient(110% 120% at 85% 100%, #14202e 0%, transparent 60%),
    #0d0b16;
}
.viz-panel {
  position: absolute; top: 0; right: 0; bottom: 0; width: 27%;
  background: rgba(22, 19, 32, 0.92); border-left: 1px solid rgba(255, 255, 255, 0.07);
  display: flex; flex-direction: column; gap: 5px; padding: 9px 8px;
}
/* Direct children only: the ghost transcript rows. The chips in .viz-chiprow
   are also <i> and must not inherit row height/background. */
.viz-panel > i {
  display: block; height: 12px; border-radius: 4px;
  background: rgba(255, 255, 255, 0.09);
}
.viz-panel > i:first-child { background: rgba(124, 92, 255, 0.45); }
/* On-screen slide: the two states of the toggle, named. The label sits under
   whichever surface currently holds the text — over the film, or over the
   panel — so the visitor reads the toggle as a choice of PLACE rather than as
   a feature being switched off. */
.viz-where {
  position: absolute; bottom: 7%;
  padding: 1.5px 7px; border-radius: 999px;
  font-size: 8.5px; font-weight: 750; letter-spacing: 0.04em;
  white-space: nowrap; opacity: 0;
  background: rgba(124, 92, 255, 0.9); color: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}
/* Centred on whatever the film currently occupies: the left 73% while the
   panel is open, the whole frame once it has collapsed. osWhereA moves it. */
.viz-where-screen { left: 36.5%; transform: translateX(-50%); }
/* The panel strip is only 27% wide — narrower than this label — so anchor it to
   the frame's right edge instead of centring it, or it overflows and clips. */
.viz-where-panel { right: 6px; }
.viz-caption {
  position: absolute; left: 0; right: 27%; top: 50%; transform: translateY(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 0 8px;
}
.viz-line {
  display: inline-block; max-width: 100%; white-space: nowrap; overflow: hidden;
  padding: 3px 9px; border-radius: 6px; background: rgba(0, 0, 0, 0.72);
  font-size: 11.5px; font-weight: 600; line-height: 1.35; color: #fff;
}
.viz-tr { color: #ffd042; font-size: 10.5px; }
.viz-masked { letter-spacing: 0.06em; }
/* Guess slide: the taps land on the sidebar row, the reveal lands on the
   overlay. Each word holds its mask and its real text in one grid cell so the
   swap never reflows the line — the pill keeps its width from the first frame,
   which is what makes the reveal read as uncovering rather than retyping. */
.viz-w { position: relative; display: inline-grid; font-weight: inherit; }
.viz-w > s, .viz-w > em { grid-area: 1 / 1; text-decoration: none; font-style: normal; }
.viz-w > em { opacity: 0; }
.viz-w.is-open > em { opacity: 1; }
/* The row being tapped, idle — gRow lights it up on each hit. */
.viz-row-tap { background: rgba(255, 255, 255, 0.09); }
/* On-screen slide: the panel's quick-mode chip row, standing in for the real
   one at the top of the sidebar — abstract active-mode chip, hairline, then
   the On-screen toggle. Only the toggle carries its real SidebarUI glyph:
   one concrete icon among abstract shapes is unmissable. */
/* The sidebar's collapse tab (#vtt-toggle-btn): a chevron riding the panel's
   LEFT edge, half outside it, exactly as the real one does. It travels with the
   panel when it slides, because in the product it is part of the sidebar. */
.viz-tab {
  position: absolute; right: 100%; top: 50%;
  transform: translateY(-50%);
  display: grid; place-items: center;
  width: 12px; height: 22px;
  border-radius: 5px 0 0 5px;
  background: rgba(22, 19, 32, 0.95);
  border: 1px solid rgba(255, 255, 255, 0.09); border-right: 0;
  color: rgba(255, 255, 255, 0.75);
}
.viz-tab svg { width: 9px; height: 9px; display: block; }
.viz-chiprow { display: flex; align-items: center; gap: 4px; flex: none; margin-bottom: 2px; }
.viz-chip {
  position: relative; /* anchors the tap ripple centred inside */
  display: grid; place-items: center; flex: none;
  width: 21px; height: 16px; border-radius: 5px;
  background: rgba(255, 255, 255, 0.08);
  color: rgba(255, 255, 255, 0.55);
}
.viz-chip svg { width: 12px; height: 12px; display: block; }
/* The hairline between exclusive modes and the independent toggle — the same
   divider the real bar draws. */
.viz-chipdiv { display: block; width: 1px; height: 10px; background: rgba(255, 255, 255, 0.14); }
.viz-tapdot {
  position: absolute; left: 50%; top: 32px; width: 22px; height: 22px;
  margin: -11px 0 0 -11px; border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.9);
  background: rgba(255, 255, 255, 0.25);
  opacity: 0; pointer-events: none;
}
/* On-screen: the ripple lives inside the toggle chip and centres itself on
   it — edge-offset arithmetic is what made an earlier version miss the
   button. Smaller than the guess ripple so its 1.7× peak stays in frame. */
.viz-tapdot-chip {
  left: 50%; top: 50%;
  width: 16px; height: 16px; margin: -8px 0 0 -8px;
}
.viz-progress {
  position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
  background: rgba(255, 255, 255, 0.14);
}
.viz-progress i { display: block; width: 34%; height: 100%; background: #7c5cff; }
/* The dictionary miniature: drag-select a word, the quick-add pill, the saved
   marker, the toast — the exact beats of saving a word in the product. Every
   value here is copied from the real thing (quick-add-overlay.ts): browser-blue
   selection, the "+ Lingogram" pill floating ABOVE the word, the saved word
   keeping a blue underline plus a "✓ saved" badge, and a solid green toast in
   the corner. A miniature that invents its own colours teaches the wrong UI. */
/* The quick-add pill hangs above the word, so this caption line must not clip
   it the way the others (which truncate long machine translations) do. */
.viz-line:has(.viz-selwrap) { overflow: visible; }
.viz-selwrap { position: relative; }
.viz-sel {
  position: relative; isolation: isolate;
  border-radius: 2px; padding: 0 1px;
  background: none; color: inherit;
}
/* The drag itself: a highlight band that grows left→right behind the glyphs,
   as if the mouse were pulling it across the word. Animating this layer's
   width (not the word's own background) is what makes it read as a drag
   rather than a block appearing all at once. */
.viz-sel::before {
  content: ''; position: absolute; left: 0; top: -1px; bottom: -1px;
  z-index: -1; width: 0; border-radius: 2px;
  background: rgba(0, 106, 255, 0.55);
}
/* Post-save marker: the extension leaves the word tinted with a blue underline
   rather than a selection block. */
.viz-sel::after {
  content: ''; position: absolute; inset: 0; z-index: -1; border-radius: 4px;
  background: rgba(77, 163, 255, 0.18);
  box-shadow: inset 0 -2px 0 rgba(77, 163, 255, 0.85);
  opacity: 0;
}
/* The I-beam cursor riding the leading edge of the drag. */
.viz-caret {
  position: absolute; left: 0; top: 50%; width: 9px; height: 13px;
  margin-left: -4px; transform: translateY(-50%);
  opacity: 0; pointer-events: none;
}
.viz-caret::before,
.viz-caret::after {
  content: ''; position: absolute; left: 50%; transform: translateX(-50%);
  background: #fff; box-shadow: 0 0 2px rgba(0, 0, 0, 0.9);
}
/* The vertical stem plus the serif caps of a text cursor. */
.viz-caret::before { top: 1px; bottom: 1px; width: 1.5px; }
.viz-caret::after { top: 0; height: 1.5px; width: 7px; box-shadow: 0 11.5px 0 #fff, 0 0 2px rgba(0, 0, 0, 0.9); }
.viz-savedbadge {
  margin-left: 5px; padding: 0 5px; border-radius: 999px;
  font-size: 8px; font-weight: 700; vertical-align: middle; white-space: nowrap;
  color: #86efac; background: rgba(34, 197, 94, 0.16);
  border: 1px solid rgba(34, 197, 94, 0.4);
  opacity: 0;
}
.viz-pill {
  position: absolute; left: 50%; bottom: calc(100% + 4px);
  transform: translateX(-50%);
  padding: 2px 7px; border-radius: 999px;
  background: #2563eb; color: #fff; font-size: 9px; font-weight: 600;
  white-space: nowrap; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.viz-toast {
  position: absolute; right: 30%; bottom: 8%; /* clear of the panel strip */
  padding: 3px 8px; border-radius: 6px;
  background: rgba(22, 163, 74, 0.95);
  color: #fff; font-size: 9px; font-weight: 600;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}
/* The Dictionary tab flashes the instruction that was there all along. */
.demo-hint.hint-flash b { animation: hintFlash 1.8s ease; border-radius: 6px; }
@keyframes hintFlash {
  0%, 100% { background: transparent; }
  25%, 60% { background: var(--accent-soft); box-shadow: 0 0 0 4px var(--accent-soft); }
}

/* ---------- mode slider animations ----------
   Each miniature loops its mode's actual gesture. Timings share a beat
   (4.5–6.5s cycles) so nothing strobes. */
/* Dual: the translation joins the line, holds, and leaves. */
.anim-dual-tr { animation: dualTr 4.5s ease-in-out infinite; }
@keyframes dualTr {
  0%, 8% { opacity: 0; transform: translateY(5px); }
  20%, 82% { opacity: 1; transform: none; }
  94%, 100% { opacity: 0; transform: translateY(5px); }
}
/* Guess: three taps on ONE sidebar row. Taps 1 and 2 uncover a word each on
   the overlay; tap 3 completes the line AND brings the translation up with it
   — finishing the line is what earns the answer, so it is one beat, not two.
   Every element runs the same 6s duration and differs only in the % at which
   it flips, keeping the beats locked. Taps land at 16 / 38 / 60%; each reveal
   follows its tap by ~2%. */
.viz-tapdot { animation: gTap 6s linear infinite; }
.viz-row-tap { animation: gRow 6s linear infinite; }
.anim-gw1 > s { animation: gHide1 6s linear infinite; }
.anim-gw1 > em { animation: gShow1 6s linear infinite; }
.anim-gw2 > s { animation: gHide2 6s linear infinite; }
.anim-gw2 > em { animation: gShow2 6s linear infinite; }
.anim-gw3 > s { animation: gHide3 6s linear infinite; }
.anim-gw3 > em { animation: gShow3 6s linear infinite; }
.anim-gtr { animation: gTr 6s linear infinite; }
/* Word 1 uncovers on the first tap (18%), word 2 on the second (40%), word 3
   on the third (62%); all re-mask together at the end of the loop (92%). */
@keyframes gHide1 { 0%, 17% { opacity: 1; } 19%, 92% { opacity: 0; } 94%, 100% { opacity: 1; } }
@keyframes gShow1 { 0%, 17% { opacity: 0; } 19%, 92% { opacity: 1; } 94%, 100% { opacity: 0; } }
@keyframes gHide2 { 0%, 39% { opacity: 1; } 41%, 92% { opacity: 0; } 94%, 100% { opacity: 1; } }
@keyframes gShow2 { 0%, 39% { opacity: 0; } 41%, 92% { opacity: 1; } 94%, 100% { opacity: 0; } }
@keyframes gHide3 { 0%, 61% { opacity: 1; } 63%, 92% { opacity: 0; } 94%, 100% { opacity: 1; } }
@keyframes gShow3 { 0%, 61% { opacity: 0; } 63%, 92% { opacity: 1; } 94%, 100% { opacity: 0; } }
/* The translation rides the same tap that opens the last word. */
@keyframes gTr {
  0%, 61% { opacity: 0; transform: translateY(4px); }
  66%, 92% { opacity: 1; transform: none; }
  94%, 100% { opacity: 0; transform: translateY(4px); }
}
/* One ripple per tap: fire, fade, repeat — three times inside the loop. */
@keyframes gTap {
  0%, 14% { opacity: 0; transform: scale(0.5); }
  16% { opacity: 0.95; transform: scale(1); }
  26%, 35.99% { opacity: 0; transform: scale(1.7); }
  36% { transform: scale(0.5); }
  38% { opacity: 0.95; transform: scale(1); }
  48%, 57.99% { opacity: 0; transform: scale(1.7); }
  58% { transform: scale(0.5); }
  60% { opacity: 0.95; transform: scale(1); }
  70%, 100% { opacity: 0; transform: scale(1.7); }
}
/* The tapped row flashes on each hit, the way the real active line does. */
@keyframes gRow {
  0%, 14% { background: rgba(255, 255, 255, 0.09); }
  16%, 21% { background: rgba(124, 92, 255, 0.55); }
  27%, 36% { background: rgba(255, 255, 255, 0.09); }
  38%, 43% { background: rgba(124, 92, 255, 0.55); }
  49%, 58% { background: rgba(255, 255, 255, 0.09); }
  60%, 65% { background: rgba(124, 92, 255, 0.55); }
  71%, 100% { background: rgba(255, 255, 255, 0.09); }
}
/* On-screen: the slide is about WHERE the subtitles live, so the loop plays the
   collapse tab — tap it at 20% and the sidebar slides off to the right, leaving
   the film and its captions the whole frame; tap it again at 62% and the panel
   comes back and the reading returns to it. Same 6s beat as the guess slide, so
   switching tabs doesn't change tempo. */
/* 8s, not the 6s the other slides use: this loop tells a four-tap story and at
   6s the states flick past too fast to read. The tempo change is deliberate. */
.anim-ostap { animation: osTap 8s linear infinite; }
.anim-ostap-chip { animation: osTapChip 8s linear infinite; }
.anim-oschip { animation: osChip 8s linear infinite; }
.anim-ospanel { animation: osPanel 8s ease-in-out infinite; }
.anim-ostab { animation: osTab 8s ease-in-out infinite; }
.anim-oscap { animation: osCap 8s ease-in-out infinite; }
/* The collapse tab's two ripples: close the panel (56%), reopen it (84%). Each
   lands just before its flip, so the tap reads as the cause. */
@keyframes osTap {
  0%, 54% { opacity: 0; transform: scale(0.5); }
  56% { opacity: 0.95; transform: scale(1); }
  64%, 81.99% { opacity: 0; transform: scale(1.7); }
  82% { transform: scale(0.5); }
  84% { opacity: 0.95; transform: scale(1); }
  92%, 100% { opacity: 0; transform: scale(1.7); }
}
/* The On-screen chip's two ripples: off (12%), then back on (36%). The captions
   must be restored BEFORE the panel collapses — a loop that hid them and then
   slid the panel away would leave a frame with nothing to read anywhere, which
   is a state the product never puts you in. */
@keyframes osTapChip {
  0%, 10% { opacity: 0; transform: scale(0.5); }
  12% { opacity: 0.95; transform: scale(1); }
  20%, 33.99% { opacity: 0; transform: scale(1.7); }
  34% { transform: scale(0.5); }
  36% { opacity: 0.95; transform: scale(1); }
  44%, 100% { opacity: 0; transform: scale(1.7); }
}
/* The On-screen slide's "Show me": the on-video captions in the DEMO answer
   with one soft glow, so the eye finds them without any panel state changing.
   Filter only — the overlay's position is the extension's own layout and must
   not be nudged. */
#vtt-video-overlay.os-flash { animation: osFlash 1.4s ease; }
@keyframes osFlash {
  0%, 100% { filter: none; }
  25% { filter: brightness(1.55) drop-shadow(0 0 16px rgba(124, 92, 255, 0.85)); }
  70% { filter: brightness(1.1) drop-shadow(0 0 6px rgba(124, 92, 255, 0.35)); }
}
/* ---- The On-screen loop's states, shared by every keyframe below ----
   A   0–10%   captions on the film AND the panel open (the default look)
   B  18–34%   chip tapped OFF: captions leave the film, reading moves to the
               panel, whose rows light up
   A' 42–54%   chip tapped back ON: captions return to the film
   C  64–82%   tab tapped: panel collapsed, captions on the FULL frame
   then the tab is tapped again and it returns to A.
   The captions are restored (A') BEFORE the panel collapses, so the loop never
   passes through a frame with nothing to read anywhere — a state the product
   does not put you in. Every animation keys off these same numbers: a control
   and its effect must never disagree, or the miniature teaches the wrong
   cause. */
/* The chip is lit whenever the captions are on the film, dim while they are
   not: it IS the control that put them there. */
@keyframes osChip {
  0%, 10% { background: rgba(124, 92, 255, 0.85); color: #fff; }
  18%, 34% { background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.45); }
  42%, 100% { background: rgba(124, 92, 255, 0.85); color: #fff; }
}
/* The panel slides out past its own right edge and back. 105%, not 100%, so
   the tab riding its left edge clears the frame too. */
@keyframes osPanel {
  0%, 56% { transform: translateX(0); }
  64%, 82% { transform: translateX(105%); }
  92%, 100% { transform: translateX(0); }
}
/* The tab's chevron points the way the tap will move the panel: right (→ close)
   while open, left (← reopen) while collapsed. It is the same flip the real
   #vtt-toggle-btn does, and it is what makes the collapsed state read as
   "tap here to bring it back" rather than as something broken. */
@keyframes osTab {
  0%, 56% { transform: translateY(-50%) rotate(0deg); }
  64%, 82% { transform: translateY(-50%) rotate(180deg); }
  92%, 100% { transform: translateY(-50%) rotate(0deg); }
}
/* The captions: on the film, gone at the first chip tap, back at the second —
   and then widening to the full frame as the panel collapses under them.
   `right` animates (not a transform) so they look like they re-flow into the
   space, which is what actually happens. Unlike the earlier version there is no
   fade around the widening: the captions are already on screen and STAY on
   screen through the collapse — that continuity is the whole point of the
   reordering. */
@keyframes osCap {
  0%, 10% { opacity: 1; right: 27%; }
  18%, 34% { opacity: 0; right: 27%; }
  42%, 56% { opacity: 1; right: 27%; }
  64%, 82% { opacity: 1; right: 0; }
  92%, 100% { opacity: 1; right: 27%; }
}
/* The counterpoint: the panel's transcript rows light up exactly when the film
   stops carrying the text — the reading moved there, it didn't stop. Nothing
   is ever lost, so nothing in the loop should read as "off". */
.anim-osrow { animation: osRow 8s ease-in-out infinite; }
@keyframes osRow {
  0%, 10% { background: rgba(255, 255, 255, 0.09); }
  18%, 34% { background: rgba(124, 92, 255, 0.5); }
  42%, 100% { background: rgba(255, 255, 255, 0.09); }
}
/* Each label names where the text currently is, and only one is ever up, so
   the pair reads as one setting with two positions rather than two badges.
   "In sidebar" belongs to state B, "On screen" to A, A' and C. */
.anim-oswhere-a { animation: osWhereA 8s ease-in-out infinite; }
.anim-oswhere-b { animation: osWhereB 8s ease-in-out infinite; }
/* "On screen" — up whenever the film carries the captions, re-centring with
   them: on the film half while the panel is open, on the whole frame once it
   has collapsed. It rides along rather than fading, same as the captions. */
@keyframes osWhereA {
  0%, 10% { opacity: 1; left: 36.5%; }
  18%, 34% { opacity: 0; left: 36.5%; }
  42%, 56% { opacity: 1; left: 36.5%; }
  64%, 82% { opacity: 1; left: 50%; }
  92%, 100% { opacity: 1; left: 36.5%; }
}
/* "In sidebar" — up only while the panel is doing the reading. */
@keyframes osWhereB {
  0%, 14% { opacity: 0; }
  22%, 32% { opacity: 1; }
  38%, 100% { opacity: 0; }
}
/* Dictionary — the product's four beats, in order: drag-select the word, the
   "+ Lingogram" pill pops above it, the click lands (selection releases, the
   word keeps the saved underline + "✓ saved"), the toast confirms. `linear`
   throughout: a hand pulling a selection doesn't ease. */
.anim-sel { animation: selText 6s linear infinite; }
.anim-sel::before { animation: selDrag 6s linear infinite; }
.anim-caret { animation: caretDrag 6s linear infinite; }
.anim-saved::after { animation: savedIn 6s linear infinite; }
.anim-badge { animation: badgeIn 6s linear infinite; }
.anim-pill { animation: pillIn 6s linear infinite; }
.anim-toast { animation: toastIn 6s linear infinite; }
/* The band sweeps across the word — mouse down at its left edge, dragged to
   the right — then holds until the pill is clicked and the selection releases. */
@keyframes selDrag {
  0%, 8% { width: 0; }
  22%, 48% { width: 100%; }
  54%, 100% { width: 0; }
}
/* The glyphs go white under the band. It passes the word in ~0.8s, too fast to
   letter-space the flip, so it lands mid-sweep where the band already covers
   most of the word. */
@keyframes selText {
  0%, 13% { color: inherit; }
  17%, 48% { color: #fff; }
  54%, 100% { color: inherit; }
}
/* The cursor rides the band's leading edge and lifts on mouse-up. */
@keyframes caretDrag {
  0%, 6% { opacity: 0; left: 0; }
  8% { opacity: 1; left: 0; }
  22% { opacity: 1; left: 100%; }
  28%, 100% { opacity: 0; left: 100%; }
}
@keyframes savedIn {
  0%, 48% { opacity: 0; }
  56%, 90% { opacity: 1; }
  98%, 100% { opacity: 0; }
}
@keyframes badgeIn {
  0%, 50% { opacity: 0; transform: scale(0.8); }
  58%, 90% { opacity: 1; transform: scale(1); }
  98%, 100% { opacity: 0; transform: scale(0.8); }
}
@keyframes pillIn {
  0%, 22% { opacity: 0; transform: translateX(-50%) scale(0.85); }
  30%, 44% { opacity: 1; transform: translateX(-50%) scale(1); }
  /* The click: the pill shrinks and goes, as it does when the save fires. */
  50%, 100% { opacity: 0; transform: translateX(-50%) scale(0.85); }
}
@keyframes toastIn {
  0%, 50% { opacity: 0; transform: translateY(6px); }
  58%, 88% { opacity: 1; transform: none; }
  96%, 100% { opacity: 0; transform: translateY(6px); }
}
@media (prefers-reduced-motion: reduce) {
  .anim-dual-tr, .anim-gtr, .viz-tapdot, .viz-row-tap,
  .anim-ostap, .anim-ostap-chip, .anim-oschip, .anim-ospanel, .anim-ostab,
  .anim-oscap, .anim-osrow, .anim-oswhere-a, .anim-oswhere-b,
  .viz-w > s, .viz-w > em,
  .anim-sel, .anim-sel::before, .anim-caret, .anim-saved::after,
  .anim-badge, .anim-pill, .anim-toast,
  #vtt-video-overlay.os-flash { animation: none; }
  /* Static fallback shows the revealed state, not the mask. */
  .viz-w > s { opacity: 0; }
  .viz-w > em { opacity: 1; }
  .anim-gtr { opacity: 1; transform: none; }
  .viz-tapdot { opacity: 0; }
  /* On-screen rests in state A — captions on the film, panel open, chip lit.
     With no motion to show the toggles firing, naming the two places side by
     side IS the explanation, so both labels stay up. */
  .anim-oswhere-a, .anim-oswhere-b { opacity: 1; }
  .anim-oschip { background: rgba(124, 92, 255, 0.85); color: #fff; }
  /* Dictionary rests at the saved end-state: no selection, no cursor, no pill —
     just the word marked as kept, which is what the slide is about. */
  .anim-sel { color: inherit; }
  .anim-caret, .anim-pill { opacity: 0; }
  .anim-saved::after, .anim-badge { opacity: 1; }
}

/* ---------- sections ---------- */
section { padding: 76px 0 0; }
.kicker { font-size: 12.5px; font-weight: 750; letter-spacing: 0.14em; text-transform: uppercase; color: var(--teal-deep); }
h2 { font-size: clamp(28px, 4vw, 40px); letter-spacing: -0.03em; line-height: 1.1; margin: 10px 0 14px; text-wrap: balance; }
.lede { color: var(--muted); max-width: 62ch; font-size: 17px; margin: 0; }

/* Brand marks — the real site logos (SITE_LOGO in build.mjs). Each SVG paints
   its own rounded tile and brand colour, so these rules only size the box.
   The .mark-* classes stay as hooks (.ed-soon overrides .mark for the
   "suggest a site" card, which has no logo of its own). */
.mark { width: 40px; height: 40px; border-radius: 10px; display: grid; place-items: center; flex: none; font-weight: 800; overflow: hidden; }
.mark-sm { width: 22px; height: 22px; border-radius: 6px; font-size: 10px; }
.mark > svg { display: block; width: 100%; height: 100%; }

/* editions */
.editions { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; margin-top: 32px; }
.ed {
  display: flex; align-items: flex-start; gap: 14px; text-decoration: none;
  background: var(--panel); border: 1px solid var(--line); border-radius: 16px; padding: 20px;
}
.ed:hover { border-color: var(--accent); }
.ed-t b { color: var(--ink); font-size: 16.5px; display: block; }
.ed-t span { color: var(--muted); font-size: 14px; line-height: 1.45; display: block; margin-top: 2px; }
.ed-t .go { color: var(--accent); font-weight: 700; font-size: 13.5px; margin-top: 8px; }
.ed-soon { border-style: dashed; align-items: center; }
.ed-soon .mark { background: var(--accent-soft); color: var(--accent); font-size: 20px; }

.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 32px; }
.step { background: var(--panel); border: 1px solid var(--line); border-radius: 16px; padding: 22px; }
.step b { display: block; font-size: 17px; margin: 10px 0 6px; }
.step p { margin: 0; color: var(--muted); font-size: 14.5px; }
.step-n { width: 30px; height: 30px; border-radius: 9px; background: var(--accent-soft); color: var(--accent); display: grid; place-items: center; font-weight: 800; font-size: 14px; }

/* ---------- dictionary band ---------- */
.band { background: var(--band); color: var(--band-ink); margin-top: 76px; padding: 76px 0; }
.band .kicker { color: #3adfa5; }
.band h2 { color: var(--band-ink); }
.band .lede { color: var(--band-muted); }
.dict { margin-top: 32px; background: var(--band-panel); border: 1px solid var(--band-line); border-radius: 16px; overflow: hidden; }
.dict-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 18px 24px; border-bottom: 1px solid var(--band-line); }
.dict-head b { font-size: 17px; }
.dict-count { font-size: 13.5px; color: var(--band-muted); font-variant-numeric: tabular-nums; }
.dict-card { padding: 16px 24px; border-bottom: 1px solid var(--band-line); }
.dict-card:last-of-type { border-bottom: none; }
.dict-w { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.dict-w b { font-size: 16.5px; }
.dict-tr { color: var(--band-muted); font-size: 14.5px; }
.dict-src { margin-left: auto; display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; font-weight: 650; color: var(--band-muted); }
.dict-ctx { margin: 6px 0 0; color: var(--band-muted); font-size: 14.5px; }
.dict-ctx mark { background: rgba(58, 223, 165, 0.16); color: #3adfa5; font-weight: 650; border-radius: 3px; padding: 0 3px; }
.dict-foot { padding: 14px 24px; font-size: 13.5px; color: var(--band-muted); border-top: 1px solid var(--band-line); }
.dict-foot b { color: #3adfa5; }

/* features */
.features { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 32px; }
.feat { background: var(--panel); border: 1px solid var(--line); border-radius: 16px; padding: 26px; }
.feat .tag { display: inline-block; font-size: 11.5px; font-weight: 750; letter-spacing: 0.1em; text-transform: uppercase; background: var(--teal-soft); color: var(--teal-deep); border-radius: 6px; padding: 4px 9px; margin-bottom: 12px; }
.feat h3 { margin: 0 0 8px; font-size: 20px; letter-spacing: -0.02em; }
.feat p { margin: 0; color: var(--muted); font-size: 15px; }
.feat kbd { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; background: var(--accent-soft); color: var(--accent); border-radius: 5px; padding: 2px 7px; }

.privacy { margin-top: 32px; background: var(--panel); border: 1px solid var(--line); border-radius: 16px; padding: 30px; display: grid; grid-template-columns: 1fr 1fr; gap: 14px 30px; }
.privacy .cell { font-size: 15px; color: var(--muted); display: flex; gap: 10px; align-items: baseline; }
.privacy .cell::before { content: "✓"; color: var(--teal-deep); font-weight: 800; }
.privacy .cell b { color: var(--ink); }

/* faq */
details.qa { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 0 20px; margin-top: 10px; }
details.qa summary { cursor: pointer; font-weight: 650; font-size: 16px; list-style: none; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 0; }
details.qa summary::-webkit-details-marker { display: none; }
details.qa summary::after { content: "+"; font-size: 20px; font-weight: 500; color: var(--accent); flex: none; transition: transform 0.2s; }
details.qa[open] summary::after { transform: rotate(45deg); }
details.qa p { color: var(--muted); font-size: 15px; margin: 0 0 16px; max-width: 70ch; }

/* final */
.final { text-align: center; padding: 84px 0 30px; }
.final .logo-mark { width: 64px; height: 64px; border-radius: 18px; margin: 0 auto 20px; }
.final h2 { max-width: 22ch; margin-inline: auto; }
.final .proof { margin-top: 18px; }

/* Grid, not `flex + space-between + wrap`: with flex the columns are spaced by
   their own content width, so they never line up, and a wrap drops a lone
   column onto its own row hard against the left edge (LEGAL used to orphan
   that way at 768–1024px). The brand column gets ~1.6× the link columns. */
footer.site {
  border-top: 1px solid var(--line); margin-top: 60px;
  /* padding-block, not a shorthand: `footer.site` outranks the `.wrap` on the
     same element, so `padding: 30px 0 44px` would zero its 24px gutter. */
  padding-block: 30px 44px;
  display: grid; grid-template-columns: 1.6fr repeat(3, 1fr); gap: 24px 40px;
  font-size: 13.5px; color: var(--muted);
}
.f-col b { display: block; color: var(--ink); font-size: 13px; letter-spacing: 0.06em; text-transform: uppercase; margin-bottom: 10px; }
.f-col a { display: block; color: var(--muted); text-decoration: none; margin-top: 6px; }
.f-col a:hover { color: var(--ink); }

/* ---------- document pages (privacy etc.) ---------- */
.doc { max-width: 72ch; margin: 30px auto 90px; padding: 0 24px; }
.doc h1 { font-size: clamp(30px, 4.5vw, 40px); letter-spacing: -0.03em; line-height: 1.12; text-wrap: balance; }
.doc h2 { font-size: 22px; margin: 2em 0 0.6em; }
.doc h3 { font-size: 17px; margin: 1.6em 0 0.5em; }
.doc p, .doc li { color: var(--muted); font-size: 15.5px; }
.doc strong { color: var(--ink); }
.doc hr { border: none; border-top: 1px solid var(--line); margin: 2.2em 0; }
/* The "turn it off" miniature on /help/analytics/ — the sidebar route to the
   setting, since that is where someone watching a video already is. Same
   grammar as the home page's mode slides: a mock of a real surface, a ripple
   on each control being taught, and a caption naming the resulting state.

   Markup, not a recorded GIF: the label inside is the same extension locale
   string the steps quote, so the drawing translates with the page.

   Every measurement is the extension's own (see the extension stylesheets
   under apps, src/assets/styles.css):
   the switch is 26×15 with 11px of knob travel, the accent is --vtt-accent,
   and the privacy row carries a lock and NO group heading — the sidebar has
   none. It stays dark in both themes because the real panel is dark. */
/* container-type: inline-size makes the box ignore its own contents when
   sizing — and every layer inside is absolutely positioned, so the height
   has to come from the aspect ratio applied to a definite width. Setting
   both on the same element collapses it to zero; the ratio therefore lives
   on the figure and the query container on the frame inside it. */
.hviz {
  margin: 0 0 1.6em;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--line); border-radius: 16px;
  overflow: hidden;
}
.hviz-scene {
  position: relative; display: block; width: 100%; height: 100%;
  background: linear-gradient(160deg, #241c3d, #141024 60%, #0e0b18);
  container-type: inline-size;
  /* Everything inside is sized in cqw so the whole scene scales as one piece
     — a mock built from px would fall apart between 640px and a phone. */
  font-size: 2.1cqw;
  /* One sign for every horizontal offset in the scene: transforms and slide
     keyframes multiply by --flip, so RTL is a single variable away instead of
     a mirrored copy of each keyframe block. */
  --flip: 1;
  /* The mock panel's surface, written once — the real sidebar's
     rgba(25,25,25,0.86) with the alpha raised for the miniature. */
  --hviz-panel: rgba(25, 25, 25, 0.94);
  --hviz-line: rgba(255, 255, 255, 0.08);
}
.hviz-scene:dir(rtl) { --flip: -1; }
.hviz-film { position: absolute; inset: 0; }
.hviz-orb {
  /* Centred on the film — the part of the frame the panel does not cover,
     which swaps sides with the panel in RTL. inset-inline-start measures from
     the opposite edge there, so the self-centering translate flips with it. */
  position: absolute; inset-inline-start: 33%; top: 44%; transform: translate(calc(-50% * var(--flip)), -50%);
  width: 12cqw; height: 12cqw; border-radius: 50%; background: rgba(255, 255, 255, 0.1);
}
.hviz-cap {
  position: absolute; inset-inline-start: 33%; bottom: 9%; transform: translateX(calc(-50% * var(--flip)));
  white-space: nowrap; text-align: center;
  font-size: 2cqw; color: #fff; text-shadow: 0 1px 6px rgba(0, 0, 0, 0.6);
}
.hviz-cap i { display: block; font-style: normal; font-size: 1.8cqw; color: #b9b2d6; margin-top: 0.4cqw; }

/* The panel, and the collapse tab riding its edge — the control that opens it. */
.hviz-side {
  position: absolute; top: 0; inset-inline-end: 0; bottom: 0; width: 33%;
  background: var(--hviz-panel); border-inline-start: 1px solid var(--hviz-line);
  display: flex; flex-direction: column;
  transform: translateX(calc(100% * var(--flip)));
}
.hviz-tab {
  position: absolute; inset-inline-start: -3.8cqw; top: 50%; margin-top: -4.7cqw;
  width: 3.8cqw; height: 9.4cqw;
  background: var(--hviz-panel);
  border: 1px solid var(--hviz-line); border-inline-end: 0;
  border-start-start-radius: 1.3cqw; border-end-start-radius: 1.3cqw;
  display: grid; place-items: center;
}
.hviz-tab svg { width: 2.2cqw; height: 2.2cqw; color: #cfc9e6; }
.hviz-head {
  display: flex; align-items: center; gap: 1.3cqw;
  padding: 1.6cqw 1.9cqw; border-bottom: 1px solid var(--hviz-line);
}
.hviz-logo { display: block; width: 2.5cqw; height: 2.5cqw; border-radius: 0.8cqw; background: linear-gradient(135deg, #7c8dff, #14a97c); flex: none; }
.hviz-ghost { display: block; height: 1.1cqw; width: 11cqw; border-radius: 0.5cqw; background: rgba(255, 255, 255, 0.16); }
.hviz-gear {
  position: relative; margin-inline-start: auto;
  width: 3.4cqw; height: 3.4cqw; border-radius: 0.9cqw;
  display: grid; place-items: center; color: #cfc9e6;
}
.hviz-gear svg { width: 2.2cqw; height: 2.2cqw; }
.hviz-rows { display: flex; flex-direction: column; gap: 1.1cqw; padding: 1.7cqw 1.9cqw; }
.hviz-rows i { display: block; height: 1.4cqw; border-radius: 0.6cqw; background: rgba(255, 255, 255, 0.09); }
.hviz-rows i:first-child { background: rgba(124, 141, 255, 0.42); }

/* The settings panel: hidden until the gear is tapped, then it fills the
   bottom of the sidebar the way the real one does. */
.hviz-set { margin-top: auto; display: flex; flex-direction: column; opacity: 0; }
.hviz-fake { display: flex; align-items: center; padding: 1.2cqw 1.9cqw; }
.hviz-fake i { display: block; height: 1.2cqw; border-radius: 0.6cqw; background: rgba(255, 255, 255, 0.1); flex: 1; }
.hviz-fake i.hviz-short { flex: 0 0 60%; }
.hviz-prow {
  display: flex; align-items: center; gap: 1.1cqw;
  padding: 1.6cqw 1.9cqw 1.9cqw; border-top: 1px solid var(--hviz-line);
  color: #cfc9e6;
}
.hviz-prow > svg { width: 2cqw; height: 2cqw; flex: none; }
.hviz-label { font-size: 1.65cqw; line-height: 1.3; color: rgba(255, 255, 255, 0.82); }
/* .vtt-switch, to the pixel ratio: 26×15 track, 11px knob, 11px of travel. */
.hviz-sw {
  position: relative; flex: none; margin-inline-start: auto;
  width: 4.1cqw; height: 2.35cqw; border-radius: 999px; background: #7c8dff;
}
.hviz-sw::after {
  content: ""; position: absolute; top: 0.31cqw; inset-inline-start: 0.31cqw;
  width: 1.72cqw; height: 1.72cqw; border-radius: 50%; background: #fff;
  transform: translateX(calc(1.72cqw * var(--flip)));
}

/* One ripple per control, each centred on its own target. Same fire-fade
   shape and colors as the home page's .viz-tapdot — the tap cue is shared
   visual vocabulary across every miniature. */
.hviz-rip {
  position: absolute; left: 50%; top: 50%;
  width: 3.4cqw; height: 3.4cqw; margin: -1.7cqw 0 0 -1.7cqw;
  border-radius: 50%; border: 0.31cqw solid rgba(255, 255, 255, 0.9);
  background: rgba(255, 255, 255, 0.25);
  opacity: 0; pointer-events: none;
}

/* Names the state the panel is in, so a switch going dark reads as a choice
   rather than as something breaking. */
.hviz-state {
  position: absolute; bottom: 3.5%; inset-inline-start: 2.5%;
  max-width: 60%;
  font-size: 1.9cqw; font-weight: 650; letter-spacing: -0.01em;
  color: #b9b2d6; opacity: 0;
}
.hviz-state-off { color: #3adfa5; }

/* 9s loop. Beats: settled → tap the tab (10%) → panel slides in → tap the
   gear (30%) → settings open → tap the switch (52%) → it goes dark, and the
   scene holds there long enough to read before resetting. */
@keyframes hvizSide {
  0%, 11% { transform: translateX(calc(100% * var(--flip))); }
  22%, 96% { transform: translateX(0); }
  100% { transform: translateX(calc(100% * var(--flip))); }
}
@keyframes hvizChev {
  0%, 11% { transform: scaleX(1); }
  22%, 96% { transform: scaleX(-1); }
  100% { transform: scaleX(1); }
}
@keyframes hvizSet {
  0%, 31% { opacity: 0; }
  36%, 96% { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes hvizGear {
  0%, 31% { background: transparent; }
  36%, 96% { background: rgba(124, 141, 255, 0.22); }
  100% { background: transparent; }
}
@keyframes hvizSwitch {
  0%, 53% { background: #7c8dff; }
  57%, 100% { background: rgba(255, 255, 255, 0.18); }
}
@keyframes hvizKnob {
  0%, 53% { transform: translateX(calc(1.72cqw * var(--flip))); }
  57%, 100% { transform: translateX(0); }
}
/* One ripple keyframe for all three controls: fire at the start of the cycle,
   then stay out of the way. Each control's animation-delay (below) places its
   tap in its own beat of the 9s loop — the delay offsets every iteration of
   an infinite animation, not just the first. */
@keyframes hvizRip {
  0% { opacity: 0; transform: scale(0.5); }
  3% { opacity: 0.95; transform: scale(1); }
  11%, 100% { opacity: 0; transform: scale(1.7); }
}
@keyframes hvizStateOn {
  0%, 36% { opacity: 0; }
  40%, 53% { opacity: 1; }
  57%, 100% { opacity: 0; }
}
@keyframes hvizStateOff {
  0%, 57% { opacity: 0; }
  61%, 96% { opacity: 1; }
  100% { opacity: 0; }
}
.hviz-side { animation: hvizSide 9s cubic-bezier(0.22, 1, 0.36, 1) infinite; }
.hviz-tab svg { animation: hvizChev 9s cubic-bezier(0.22, 1, 0.36, 1) infinite; }
.hviz-set { animation: hvizSet 9s linear infinite; }
.hviz-gear { animation: hvizGear 9s linear infinite; }
.hviz-sw { animation: hvizSwitch 9s linear infinite; }
.hviz-sw::after { animation: hvizKnob 9s linear infinite; }
/* Delays are the old per-control fire windows: tab 8%, gear 28%, switch 50%
   of the 9s loop. */
.hviz-rip { animation: hvizRip 9s linear infinite; }
.hviz-rip-tab { animation-delay: 0.72s; }
.hviz-rip-gear { animation-delay: 2.52s; }
.hviz-rip-sw { animation-delay: 4.5s; }
.hviz-state-on { animation: hvizStateOn 9s linear infinite; }
.hviz-state-off { animation: hvizStateOff 9s linear infinite; }

/* Reduced motion: no loop. Hold the end state — panel open, settings shown,
   switch off — which is the outcome the page is about. */
@media (prefers-reduced-motion: reduce) {
  .hviz-side, .hviz-tab svg, .hviz-set, .hviz-gear, .hviz-sw, .hviz-sw::after,
  .hviz-rip, .hviz-state-on, .hviz-state-off { animation: none; }
  .hviz-side { transform: translateX(0); }
  .hviz-tab svg { transform: scaleX(-1); }
  .hviz-set { opacity: 1; }
  .hviz-gear { background: rgba(124, 141, 255, 0.22); }
  .hviz-sw { background: rgba(255, 255, 255, 0.18); }
  .hviz-sw::after { transform: translateX(0); }
  .hviz-rip { opacity: 0; }
  .hviz-state-on { opacity: 0; }
  .hviz-state-off { opacity: 1; }
}

/* The two steps on /help/analytics/. Wider gap than a prose list: these are
   things to DO one after the other, and the reader is looking away at a
   browser toolbar between them. */
.doc ol.help-steps { display: flex; flex-direction: column; gap: 10px; margin: 0 0 1.2em; padding-inline-start: 22px; }
.doc ol.help-steps li { color: var(--muted); font-size: 15.5px; line-height: 1.6; }
.doc ol.help-steps b { color: var(--ink); }

/* "Back" on doc pages — quiet, above the title. */
.doc .doc-back {
  display: inline-flex; align-items: center; gap: 6px;
  margin-bottom: 14px;
  font-size: 14.5px; font-weight: 600;
  color: var(--muted); text-decoration: none;
}
.doc .doc-back:hover { color: var(--ink); }
.doc .doc-back svg { width: 15px; height: 15px; }
[dir="rtl"] .doc-back svg { transform: scaleX(-1); }

/* /help/addresses/ — the full list, in columns. Plain text on purpose: the
   page is for recognizing your own address, not for navigating out. */
.doc .host-list {
  list-style: none; padding: 0; margin: 0 0 1.4em;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 6px 18px;
  font-size: 14px; line-height: 1.45; color: var(--muted);
}
.doc table { border-collapse: collapse; width: 100%; }
.doc th, .doc td { border: 1px solid var(--line); padding: 8px 12px; font-size: 14px; text-align: left; color: var(--muted); }

/* ---------- welcome / uninstall ---------- */
.narrow { max-width: 640px; margin: 40px auto 90px; padding: 0 24px; text-align: center; }
.narrow .steps { grid-template-columns: 1fr; text-align: left; }
.narrow textarea {
  width: 100%; min-height: 120px; margin-top: 18px; resize: vertical;
  background: var(--panel); color: var(--ink); border: 1px solid var(--line); border-radius: 12px;
  padding: 14px; font: inherit; font-size: 16px; /* 16px: see .auth-field input */
}
.keys { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-top: 22px; }
.keys span { font-size: 13.5px; color: var(--muted); background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 7px 12px; }
.keys kbd { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; background: var(--accent-soft); color: var(--accent); border-radius: 5px; padding: 1px 6px; }

/* First-run disclosure that anonymous stats are on. Deliberately quiet — it is
   a fact the user is entitled to on day one, not a step in the three-step flow
   above, and styling it louder would compete with "save your first word". */
.welcome-privacy { max-width: 60ch; margin: 26px auto 0; font-size: 13px; line-height: 1.6; color: var(--muted); text-align: center; }
.welcome-privacy a { color: var(--muted); text-decoration: underline; text-underline-offset: 2px; }
.welcome-privacy a:hover { color: var(--accent); }

/* ---------- login / register ---------- */
/* Styled in the landing's own visual language: an accent aura behind the card
   (same radial-gradient trick as .demo), the eyebrow pill and the big
   clamp() headline with a .pop accent word from the hero. */
.auth-wrap {
  position: relative; display: flex; align-items: center; justify-content: center;
  padding: 28px 24px 44px; overflow: hidden;
}
.auth-wrap::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(60% 55% at 50% 0%, var(--accent-soft) 0%, transparent 60%),
    radial-gradient(45% 40% at 85% 100%, var(--teal-soft) 0%, transparent 60%);
  opacity: 0.9;
}
.auth-card {
  position: relative; z-index: 1;
  width: 100%; max-width: 440px; background: var(--panel); border: 1px solid var(--line);
  border-radius: 22px; box-shadow: var(--shadow); padding: 28px 32px 26px; text-align: center;
}
.auth-logo { width: 48px; height: 48px; border-radius: 14px; margin: 0 auto 12px; box-shadow: 0 10px 30px -14px var(--accent); }
.auth-eyebrow {
  display: inline-flex; align-items: center; gap: 8px; background: var(--accent-soft);
  border-radius: 999px; padding: 5px 14px; font-size: 11px; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent); margin-bottom: 10px;
}
.auth-eyebrow::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
.auth-title { font-size: clamp(22px, 3.6vw, 28px); line-height: 1.1; letter-spacing: -0.03em; font-weight: 800; margin: 0 0 6px; text-wrap: balance; }
.auth-title .pop { color: var(--accent); }
.auth-sub { margin: 0 0 18px; color: var(--muted); font-size: 15px; text-wrap: balance; }
.auth-form { display: flex; flex-direction: column; gap: 11px; text-align: left; }
.auth-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.auth-field { display: flex; flex-direction: column; gap: 5px; }
.auth-field > span { font-size: 13px; font-weight: 700; color: var(--ink); letter-spacing: -0.01em; }
/* 16px, not 15: iOS Safari zooms the page in on focus whenever a field's text
   is under 16px, which yanks the layout around on the first tap of the form. */
.auth-field input {
  width: 100%; background: var(--ground); color: var(--ink); border: 1px solid var(--line);
  border-radius: 12px; padding: 11px 15px; font: inherit; font-size: 16px; transition: border-color .15s, box-shadow .15s;
}
.auth-field input::placeholder { color: var(--muted); opacity: 0.7; }
.auth-field input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.auth-row-end { display: flex; justify-content: flex-end; margin-top: -6px; }
.auth-link { font-size: 13.5px; font-weight: 600; text-decoration: none; }
.auth-link:hover { text-decoration: underline; }
.auth-submit { width: 100%; margin-top: 4px; padding: 12px 24px; font-size: 15.5px; }
.auth-submit:disabled { opacity: 0.6; cursor: default; }
.auth-error { display: none; margin: 0; color: #d13b3b; font-size: 13.5px; font-weight: 600; }
.auth-note { display: none; margin: 0; color: var(--teal-deep); font-size: 13.5px; font-weight: 600; }
.auth-alt { margin: 14px 0 0; font-size: 14.5px; color: var(--muted); }
.auth-alt a { font-weight: 700; }
.auth-fine { margin: 8px 0 0; font-size: 12.5px; color: var(--muted); }
.auth-google { width: 100%; gap: 10px; font-size: 15px; padding: 11px 20px; font-weight: 700; }
.auth-google:hover { border-color: var(--accent); }
.auth-google:disabled { opacity: 0.6; cursor: default; }
.auth-google-icon { flex: none; }
/* Busy state for the Google button. The backend call runs after the popup
   closes, and on a cold Cloud Run instance that can take tens of seconds —
   without a moving indicator the page reads as frozen. The spinner replaces
   the Google mark in place so the button neither resizes nor reflows. */
.auth-google[data-busy] .auth-google-icon { display: none; }
.auth-google-spinner {
  flex: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 2px solid var(--line);
  border-top-color: var(--accent);
  animation: authSpin 0.7s linear infinite;
}
@keyframes authSpin { to { transform: rotate(360deg); } }
/* Same indicator on the email/password submit. That button is filled, so the
   track is drawn from the label colour rather than the page's border colour. */
.auth-submit-spinner {
  display: inline-block;
  vertical-align: -3px;
  margin-right: 8px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.35);
  border-top-color: currentColor;
  animation: authSpin 0.7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .auth-submit-spinner { animation: none; }
}
/* Honour a reduced-motion preference: keep the state visible, drop the spin. */
@media (prefers-reduced-motion: reduce) {
  .auth-google-spinner { animation: none; border-top-color: var(--accent); }
}
.auth-divider { display: flex; align-items: center; gap: 14px; margin: 4px 0; color: var(--muted); font-size: 12px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; }
.auth-divider::before, .auth-divider::after { content: ""; flex: 1; height: 1px; background: var(--line); }
/* "optional" tag next to a field label */
.auth-optional { font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); background: var(--ground); border: 1px solid var(--line); border-radius: 6px; padding: 1px 6px; margin-left: 6px; }
/* password field: input + show/hide toggle */
.auth-pw-wrap { position: relative; display: block; }
.auth-pw-wrap input { width: 100%; padding-right: 46px; }
/* 44px square: the minimum comfortable touch target (Apple HIG / WCAG 2.5.8).
   The hover chip stays 34px via background-clip so the button grows only in
   hit area, not in looks. */
.auth-pw-toggle {
  position: absolute; right: 1px; top: 50%; transform: translateY(-50%);
  display: grid; place-items: center; width: 44px; height: 44px; padding: 5px; box-sizing: border-box;
  background: none; border: none; border-radius: 8px; cursor: pointer; color: var(--muted);
}
.auth-pw-toggle:hover { color: var(--accent); background: var(--accent-soft); background-clip: content-box; }
/* per-field inline error (blur validation) */
.auth-field-error { display: none; margin: 0; color: #d13b3b; font-size: 12.5px; font-weight: 600; }
.auth-field.is-invalid input { border-color: #d13b3b; }
.auth-field.is-invalid .auth-field-error { display: block; }
/* Caps Lock hint */
.auth-caps { margin: 0; color: var(--star); font-size: 12px; font-weight: 700; }
/* password strength meter */
.auth-strength { display: flex; align-items: center; gap: 10px; }
.auth-strength-bar { flex: 1; height: 6px; border-radius: 999px; background: var(--line); overflow: hidden; }
.auth-strength-bar > span { display: block; height: 100%; width: 0; border-radius: 999px; transition: width .2s, background .2s; }
.auth-strength-label { font-size: 12px; font-weight: 700; min-width: 52px; text-align: right; }
.auth-strength[data-level="1"] .auth-strength-bar > span { width: 33%; background: #e0564d; }
.auth-strength[data-level="2"] .auth-strength-bar > span { width: 66%; background: var(--star); }
.auth-strength[data-level="3"] .auth-strength-bar > span { width: 100%; background: var(--teal); }
.auth-strength[data-level="1"] .auth-strength-label { color: #e0564d; }
.auth-strength[data-level="2"] .auth-strength-label { color: var(--star); }
.auth-strength[data-level="3"] .auth-strength-label { color: var(--teal-deep); }
@media (prefers-color-scheme: dark) { .auth-field-error { color: #ff8b8b; } }
@media (prefers-color-scheme: dark) { .auth-strength[data-level="3"] .auth-strength-label { color: var(--teal); } }
@media (prefers-color-scheme: dark) { .auth-error { color: #ff8b8b; } }
/* Short viewports (laptops, split screens): tighten and drop the eyebrow so the
   taller register form clears the fold without scrolling. Two steps: ≤820px
   trims the chrome, ≤700px goes further for very short windows. */
@media (max-height: 820px) {
  .auth-wrap { padding: 14px 24px 20px; }
  .auth-card { padding: 20px 30px 20px; }
  .auth-logo { width: 40px; height: 40px; margin-bottom: 8px; }
  .auth-eyebrow { display: none; }
  .auth-title { font-size: clamp(20px, 3.2vw, 25px); }
  .auth-sub { margin-bottom: 12px; font-size: 14px; }
  .auth-form { gap: 9px; }
  .auth-field input { padding: 10px 14px; }
  .auth-google { padding: 10px 20px; }
  .auth-alt { margin-top: 10px; }
  .auth-fine { margin-top: 6px; }
}
@media (max-height: 700px) {
  .auth-wrap { padding: 10px 24px 14px; }
  .auth-card { padding: 16px 28px 16px; }
  .auth-logo { width: 34px; height: 34px; margin-bottom: 6px; }
  .auth-sub { margin-bottom: 10px; }
  .auth-form { gap: 8px; }
}

/* Tablets in portrait (iPad mini 768 → iPad Pro 1024). Above 760 the phone
   rules below don't apply, so this range used to get the full desktop layout
   squeezed into ~800px: the nav, both auth buttons and the 2–3 column grids
   all competing for one header row. Relax the columns and tighten the nav
   instead of switching to the burger — there is room for the links, just not
   at desktop spacing. */
@media (min-width: 761px) and (max-width: 1024px) {
  .wrap { padding: 0 32px; }
  nav.top { gap: 16px; font-size: 14px; }
  header.site { gap: 12px; }
  .btn-login { padding: 9px 14px; font-size: 13.5px; white-space: nowrap; }
  /* .steps stays at 3 columns: it holds exactly 3 cards, so dropping to 2
     strands the third next to a column of dead space. */
  .pillars { gap: 8px 20px; }
  section { padding: 60px 0 0; }
  /* Four columns get cramped here; give the brand its own full-width row. */
  footer.site { grid-template-columns: repeat(3, 1fr); }
  footer.site .f-col:first-child { grid-column: 1 / -1; }
}

/* Touch pointers: bring the interactive text to a 44px-tall target. These are
   all sized for a mouse otherwise, so scope it to coarse pointers and leave
   desktop spacing alone. `inline-flex` because min-height is ignored on the
   default `display: inline` of an <a>. */
@media (pointer: coarse) {
  nav.top a,
  .auth-link,
  .auth-alt a,
  .auth-fine a { display: inline-flex; align-items: center; min-height: 44px; }
  .btn-login, .auth-google, .auth-submit, .logo { min-height: 44px; }
  /* The header's language link and every row on /languages/. */
  .lang-link, .lang-entry { min-height: 44px; }
  /* `flex`, not `inline-flex`: these are stacked one per line (display: block
     above), and going inline would run the whole column onto a single line. */
  .f-col a { display: flex; align-items: center; min-height: 44px; margin-top: 0; }
  /* Sits in a text run — grow the hit area without moving the surrounding copy. */
  .auth-alt a, .auth-fine a { margin-block: -10px; }
}

@media (max-width: 760px) {
  nav.top { display: none; }
  .mnav { display: block; }
  .mnav summary { list-style: none; cursor: pointer; border: 1px solid var(--line); background: var(--panel); border-radius: 10px; padding: 8px 12px; font-size: 18px; line-height: 1; }
  .mnav summary::-webkit-details-marker { display: none; }
  /* max-width as well as min-: at 320px a bare min-width:210px panel is wider
     than the space left of the burger, and since it is laid out even while the
     <details> is closed it dragged the page into a horizontal scroll. */
  .mnav-panel {
    position: absolute; right: 0; top: 46px; z-index: 20;
    min-width: min(210px, calc(100vw - 32px)); max-width: calc(100vw - 32px);
    background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
    box-shadow: var(--shadow); padding: 8px; display: flex; flex-direction: column;
  }
  .mnav-panel a { text-decoration: none; color: var(--ink); font-weight: 600; font-size: 15px; padding: 11px 14px; border-radius: 9px; }
  .mnav-panel a:hover { background: var(--accent-soft); }
  header.site { gap: 10px; }
  .btn-login { padding: 8px 13px; font-size: 13.5px; white-space: nowrap; }
  /* THE SWAP. The desktop popover goes and the link to /languages/ (visible
     by default — see .lang-link above) is left holding the header. A 280px
     panel hung off a pill that sits ~106px from the left edge put most of
     its width — and every language NAME, leaving only the codes — off the
     left of a 375px screen, where nothing could scroll it back. The page has
     the room the panel never had. `!important` beats the inline `hidden`
     attribute demo.js clears when it fills the panel. */
  .lang-wrap { display: none !important; }
  /* Two even columns — 196px auto-fill tracks leave an orphan at this width. */
  .lang-grid { grid-template-columns: 1fr 1fr; }
  /* 44px touch target for the burger (see the coarse-pointer block above). */
  .mnav summary { min-height: 44px; display: grid; place-items: center; padding: 8px 12px; }
  /* Phones show the active tab's film instead of the live embed
     (demo/index.ts); the select-a-word hint promises an interaction that
     isn't there. The mode slider stays for its tabs and benefit copy, but
     the slides' animated miniatures go: the card above already plays the
     same story with the real product — two animations telling it at once
     compete for the eye. */
  .demo-still { display: block; width: 100%; height: auto; }
  .demo-hint { display: none; }
  .mtabs { gap: 14px; }
  .mtab { font-size: 13px; gap: 6px; }
  .mslide { flex-direction: column; align-items: stretch; gap: 14px; }
  .mviz { display: none; }
  /* The window dots go — pure decoration, and the three site names are worth
     more than they are. All three tabs keep their names: measured, the strip
     needs ~273px and the card offers 290px even on a 360px phone. */
  .demo-chrome { padding: 9px 10px 0; gap: 6px; }
  .demo-chrome .dots { display: none; }
  .dtab { padding: 7px 11px; font-size: 12px; }
  .steps, .features, .privacy, .editions { grid-template-columns: 1fr; }
  .pillars { grid-template-columns: 1fr; gap: 10px; max-width: 340px; }
  .auth-card { padding: 24px 20px; }
  .auth-row { grid-template-columns: 1fr; }
  /* Full-width CTAs: side-by-side buttons wrap to one per row on a phone
     anyway, and a half-width button next to a wrapped one looks accidental. */
  .cta-row .btn { width: 100%; }
  section { padding: 56px 0 0; }
  .band { margin-top: 56px; padding: 56px 0; }
  details.qa summary { padding: 18px 0; }
  /* Brand block above, then the three link columns two-up. */
  footer.site { grid-template-columns: repeat(2, 1fr); gap: 20px 24px; }
  footer.site .f-col:first-child { grid-column: 1 / -1; }
}

/* Below 480px the header can't seat the wordmark, the language pill and both
   auth buttons — the chameleon mark carries the brand alone. */
@media (max-width: 480px) {
  .logo-name { display: none; }
}

/* Small phones (iPhone SE and friends, ≤400px). The 24px gutters eat 13% of a
   375px screen, so cards lose more width to padding than they can spare. */
@media (max-width: 400px) {
  .wrap { padding: 0 16px; }
  .ed, .step, .feat { padding: 18px; }
  .privacy { padding: 22px 18px; }
  .dict-head, .dict-card, .dict-foot { padding-left: 16px; padding-right: 16px; }
  details.qa { padding: 0 16px; }
  .auth-wrap { padding-left: 14px; padding-right: 14px; }
  .auth-card { padding: 22px 16px; }
  /* The eyebrow's 0.14em tracking pushes it onto three lines at this width. */
  .eyebrow { letter-spacing: 0.08em; font-size: 11px; }
  /* One column, and the English gloss goes: at 375px "Português" beside
     "Portuguese" leaves neither room to breathe. The autonym is what a
     speaker of that language recognises, so the gloss is what yields. */
  .lang-grid { grid-template-columns: 1fr; }
  .lang-entry .le-en { display: none; }
}

/* Very narrow phones (320px: iPhone SE 1st gen, Android S-sizes). The header
   runs logo + Log in + Sign up + burger, which needs ~364px — the surplus used
   to push the burger off-screen and force a horizontal scroll on the whole
   page. Shrink the row to fit rather than dropping a control: the burger menu
   carries no login link, so hiding "Log in" here would strip the only way in
   at this width. Hiding the *wordmark* is safe — the logo image still links
   home right beside it. */
@media (max-width: 380px) {
  /* Clip the wordmark instead of `font-size: 0` / `display: none` so the link
     keeps its accessible name for screen readers. */
  .logo { gap: 0; overflow: hidden; max-width: 34px; white-space: nowrap; }
  .logo-mark { width: 34px; height: 34px; }
  header.site { gap: 8px; }
  .btn-login { padding: 8px 10px; font-size: 13px; }
  .mnav summary { padding: 8px 10px; min-width: 40px; }
  .mnav-panel { top: 50px; }
  /* Only here do three named tabs actually overrun the card (~273px needed
     against 250px available at 320px). Tighten the tabs rather than hide the
     names: the sites are the point of the strip. */
  .demo-chrome { padding: 9px 6px 0; }
  .dtab { padding: 7px 8px; gap: 5px; font-size: 11.5px; }
  .dfav { width: 13px; height: 13px; }
}

/* ------------------------------------------------------------------- welcome
   The post-install page (see welcomePage in build.mjs). Scoped under .wl, and
   reuses the existing tokens plus the .mark-* badges from the editions section
   rather than introducing a parallel design system. */

.wl { max-width: 660px; margin: 34px auto 90px; padding: 0 24px; }

.wl-hello { display: flex; align-items: center; gap: 12px; margin-bottom: 22px; }
.wl-hello-note { font-size: 13.5px; color: var(--muted); line-height: 1.45; }

.wl-h1 {
  font-size: clamp(29px, 4.6vw, 40px);
  line-height: 1.14;
  letter-spacing: -0.03em;
  margin: 0 0 14px;
  text-wrap: balance;
}

.wl-lede {
  font-size: 18.5px;
  line-height: 1.55;
  color: var(--muted);
  margin: 0 0 30px;
  max-width: 40ch;
}
.wl-lede b { color: var(--ink); font-weight: 600; }

/* --- the tour --- */

.wl-tut {
  margin: 0 0 14px;
  border-radius: 16px;
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--player);
  box-shadow: var(--shadow);
}

/* Click-to-play facade. The <img> is the whole page weight until the visitor
   presses play; main.js swaps in the iframe on click. */
.wl-facade {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
  padding: 0;
  background: var(--player);
  cursor: pointer;
  overflow: hidden;
}
.wl-facade img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 400ms cubic-bezier(0.23, 1, 0.32, 1);
}
.wl-facade::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(10,8,16,.05) 0%, rgba(10,8,16,.45) 100%);
}

.wl-play {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: 66px; height: 66px;
  border-radius: 50%;
  background: rgba(255,255,255,.94);
  color: #17131f;
  display: grid; place-items: center;
  z-index: 2;
  box-shadow: 0 8px 28px -8px rgba(0,0,0,.55);
  transition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1), background 200ms ease;
}
.wl-play svg { width: 24px; height: 24px; margin-left: 3px; }

.wl-facade-meta {
  position: absolute;
  /* Logical, not `left`: the card mirrors in Arabic and Hebrew, and a physical
     offset left this caption alone in the bottom-left while the rest of the
     card flipped. */
  inset-inline-start: 16px; bottom: 14px;
  z-index: 2;
  color: #fff;
  font-size: 13.5px;
  font-weight: 600;
  text-shadow: 0 1px 6px rgba(0,0,0,.5);
}

@media (hover: hover) and (pointer: fine) {
  .wl-facade:hover img { transform: scale(1.03); }
  .wl-facade:hover .wl-play { transform: translate(-50%,-50%) scale(1.06); background: #fff; }
}
.wl-facade:active .wl-play { transform: translate(-50%,-50%) scale(0.97); }
.wl-facade:focus-visible { outline: 3px solid var(--accent); outline-offset: -3px; }

.wl-frame { width: 100%; aspect-ratio: 16/9; border: 0; display: block; }

.wl-tut-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px;
  padding: 11px 14px;
  background: var(--player-panel);
  border-top: 1px solid rgba(255,255,255,.07);
  font-size: 12.5px;
  /* The band tokens, not the page ones: this strip sits on the dark player
     chrome and stays dark in both themes. */
  color: var(--band-muted);
}
.wl-tut-foot a {
  color: var(--band-ink);
  font-weight: 600;
  text-decoration: none;
  background: rgba(255,255,255,.1);
  border-radius: 7px;
  padding: 7px 12px;
  white-space: nowrap;
}
.wl-tut-foot a:hover { background: rgba(255,255,255,.18); }

.wl-note { font-size: 12.5px; color: var(--muted); margin: 0 0 32px; line-height: 1.5; }

/* --- open a video --- */

.wl-cta-h { font-size: 17px; font-weight: 700; letter-spacing: -0.02em; margin: 0 0 4px; }
.wl-cta-s { font-size: 14.5px; color: var(--muted); margin: 0 0 16px; line-height: 1.5; }

.wl-opens { display: flex; flex-direction: column; gap: 10px; margin-bottom: 30px; }

.wl-open {
  display: flex; align-items: center; gap: 13px;
  padding: 15px 17px;
  border-radius: 13px;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--ink);
  text-decoration: none;
  /* The rezka card is a <button> (see linkFor in build.mjs), so the row's
     look cannot lean on anchor defaults: the font and start-alignment undo
     the UA button styles, and the cursor is set for all three — an href-ed
     anchor gets it from the UA, a button never does. */
  font: inherit; text-align: start; cursor: pointer;
  transition: transform 160ms cubic-bezier(0.23, 1, 0.32, 1), border-color 160ms ease;
}
.wl-open b { display: block; font-size: 15.5px; font-weight: 600; letter-spacing: -0.01em; }
.wl-open-body { flex: 1; min-width: 0; }
.wl-open-body span { display: block; font-size: 12.5px; color: var(--muted); margin-top: 3px; }
.wl-open-go { width: 16px; height: 16px; flex: none; opacity: .55; }

.wl-open-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}
.wl-open-primary .wl-open-body span { color: rgba(255,255,255,.75); }
.wl-open-primary .wl-open-go { opacity: .85; }

@media (hover: hover) and (pointer: fine) {
  .wl-open:hover { border-color: var(--accent); }
}
.wl-open:active { transform: scale(0.985); }

/* --- the asides --- */

.wl-asides {
  border-top: 1px solid var(--line);
  padding-top: 24px;
  margin-bottom: 26px;
  display: flex; flex-direction: column; gap: 18px;
}
.wl-asides p { font-size: 15px; line-height: 1.6; color: var(--muted); margin: 0; }

/* Coverage notice on /welcome/, one edition only, above the video: it is a
   caveat about whether the thing will work at all, so it has to land before
   someone spends a minute watching. Amber-tinted rather than red — nothing is
   broken, and most visitors are already covered. */
.wl-notice {
  display: flex; gap: 12px; align-items: flex-start;
  margin: 0 0 26px; padding: 14px 16px;
  border-radius: 13px;
  background: color-mix(in srgb, var(--star) 12%, var(--panel));
  border: 1px solid color-mix(in srgb, var(--star) 38%, var(--line));
}
.wl-notice-icon { width: 19px; height: 19px; flex: none; margin-top: 1px; color: var(--star); }
.wl-notice-body { flex: 1; min-width: 0; }
.wl-notice-body > p { margin: 0; font-size: 14.5px; line-height: 1.55; color: var(--ink); }
.wl-notice-body > p b { font-weight: 650; }

.wl-asides b { color: var(--ink); font-weight: 600; }

.wl-keys { border-top: 1px solid var(--line); padding-top: 24px; }
.wl-keys > p { font-size: 14.5px; color: var(--muted); margin: 0 0 12px; }
.wl-keys .keys { justify-content: flex-start; margin-top: 0; }

.wl-signoff {
  margin-top: 30px;
  padding-top: 24px;
  border-top: 1px solid var(--line);
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--muted);
}
.wl-signoff a { color: var(--accent); font-weight: 600; }

@media (prefers-reduced-motion: reduce) {
  .wl-facade img, .wl-play, .wl-open { transition: none; }
}

@media (max-width: 560px) {
  .wl-h1 { font-size: 27px; }
  .wl-play { width: 54px; height: 54px; }
  .wl-play svg { width: 20px; height: 20px; }
}

/* Privacy disclosure on /welcome/. Closed by default: it belongs next to the
   sign-in ask, but it must not stand between someone and their first saved
   word. The shield and the chevron carry the "this is about your data" and
   "there is more here" signals without a paragraph of preamble. */
.wl-priv {
  border: 1px solid var(--line);
  border-radius: 13px;
  background: var(--panel);
  margin-bottom: 26px;
  overflow: hidden;
}

.wl-priv summary {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px;
  cursor: pointer;
  font-size: 14.5px;
  font-weight: 600;
  color: var(--ink);
  list-style: none;
  user-select: none;
}
/* Safari still paints its own triangle without this. */
.wl-priv summary::-webkit-details-marker { display: none; }

.wl-priv summary > svg:first-child {
  width: 17px; height: 17px;
  flex: none;
  color: var(--accent);
}

.wl-priv summary > span { flex: 1; }

.wl-priv-chev {
  width: 16px; height: 16px;
  flex: none;
  color: var(--muted);
  transition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1);
}
.wl-priv[open] .wl-priv-chev { transform: rotate(180deg); }

@media (hover: hover) and (pointer: fine) {
  .wl-priv summary:hover { background: var(--ground); }
}
.wl-priv summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.wl-priv-body {
  padding: 2px 16px 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.wl-priv-body p {
  margin: 0;
  font-size: 14px;
  line-height: 1.6;
  color: var(--muted);
}
.wl-priv-body b { color: var(--ink); font-weight: 600; }

.wl-priv-more a {
  color: var(--accent);
  font-weight: 600;
  font-size: 13.5px;
}

@media (prefers-reduced-motion: reduce) {
  .wl-priv-chev { transition: none; }
}
