/* ============================================================
   AccentCoach — LEARNER APP
   Depends on brand.css, which must load first and is the single
   source of the palette. This file styles the app shell and every
   practice view; the landing page and the login overlay keep their
   own CSS inline in index.html and are deliberately untouched.

   Nothing here is functional. app.js reaches the DOM by id and by
   a set of class names it both writes and re-queries — notably
   practiceBlock(), which finds its parts with
   root.querySelector(".record" / "canvas.wave" / ".result-box" /
   "audio" / ".listen" / "select"). Those names, and the order of
   that first <select>, are load-bearing: restyle them, never
   rename them and never insert another <select> ahead of it.
   ============================================================ */

:root{
  /* ---- aliases app.js writes into inline styles ----
     app.js emits var(--ink-soft) x9, var(--rec) x6, var(--navy) and
     var(--line); index.html emits var(--rec-soft). None of them were
     ever defined for this page, so those declarations were invalid
     and silently dropped — the affected text simply inherited its
     colour. Defining them here is what makes that markup render as
     it was written to. */
  --ink-soft:var(--ink-2);
  --rec:var(--red);
  --rec-soft:var(--red-dim);
  --navy:var(--violet);
  --line:var(--edge);

  /* the rail is 244px to match the admin panel exactly */
  --sidebar:244px;
  --nav-ink:#F2F6F5;
  --nav-ink-2:rgba(242,246,245,.62);
  --nav-ink-3:rgba(242,246,245,.38);
  --nav-line:rgba(242,246,245,.12);
  --top-h:58px;
}

*{margin:0;padding:0;box-sizing:border-box;}
html{scroll-behavior:smooth;}
body{
  background:var(--bg);color:var(--ink);
  font-family:var(--font-body);
  font-size:15.5px;line-height:1.6;min-height:100vh;
  -webkit-font-smoothing:antialiased;
}
.serif{font-family:var(--font-display);}
.mono{font-family:var(--font-mono);}

/* ambient studio glow */
body::before{
  content:"";position:fixed;inset:0;z-index:0;pointer-events:none;
  background:
    radial-gradient(720px 400px at 78% -8%, rgba(12,155,138,.07), transparent 60%),
    radial-gradient(640px 420px at 8% 108%, rgba(106,90,224,.06), transparent 60%);
}

h1{font-family:var(--font-display);font-weight:900;font-size:clamp(28px,3.6vw,40px);line-height:1.05;letter-spacing:-.02em;}
h2{font-family:var(--font-display);font-weight:600;font-size:22px;letter-spacing:-.01em;margin:30px 0 14px;}
.sub{color:var(--ink-2);font-size:15px;max-width:70ch;}
.eyebrow{font-family:var(--font-mono);font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--teal);}

a{color:var(--teal);}
button{font-family:inherit;}
button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,
textarea:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--teal);outline-offset:2px;}

/* ============================================================
   APP SHELL — dark rail + content column (mirrors the admin panel)
   ============================================================ */
#shell{display:flex;min-height:100vh;position:relative;z-index:1;}

#sidebar{
  width:var(--sidebar);flex-shrink:0;position:sticky;top:0;height:100vh;
  display:flex;flex-direction:column;overflow:hidden;
  background:linear-gradient(165deg,#0E2A2E 0%,#12303A 46%,#1A2340 100%);
  color:var(--nav-ink);
}
/* teal→violet bloom, the same pairing as the login aside and admin rail */
#sidebar::before{
  content:"";position:absolute;inset:-30% -40% auto -40%;height:130%;
  background:radial-gradient(closest-side,rgba(12,155,138,.34),transparent 72%),
             radial-gradient(closest-side,rgba(106,90,224,.28),transparent 70%);
  background-position:16% 10%, 84% 78%;background-repeat:no-repeat;
  background-size:66% 58%, 62% 56%;filter:blur(8px);pointer-events:none;
}
@media (prefers-reduced-motion: no-preference){
  #sidebar::before{animation:navDrift 22s ease-in-out infinite alternate;}
}
@keyframes navDrift{
  0%{transform:translate3d(-3%,-2%,0) scale(1);}
  100%{transform:translate3d(4%,3%,0) scale(1.08);}
}

.brand{
  position:relative;z-index:2;display:flex;align-items:center;gap:11px;
  padding:22px 18px 16px;
}
.brand-mark{width:32px;height:32px;flex-shrink:0;}
.brand-name{font-family:var(--font-display);font-weight:900;font-size:19px;letter-spacing:-.02em;line-height:1;}
.brand-name .c{color:var(--teal);}
.brand-sub{font-family:var(--font-mono);font-size:9px;letter-spacing:.18em;text-transform:uppercase;margin-top:3px;}
/* scoped to the rail so the auth panel's own brand treatment is untouched */
#sidebar .brand-name,#topbar .brand-name{color:#FFFFFF;}
#sidebar .brand-sub{color:var(--nav-ink-3);}

.nav-scroll{
  position:relative;z-index:2;flex:1;overflow-y:auto;
  padding:2px 12px 12px;display:flex;flex-direction:column;
}
.nav-group{margin-top:16px;}
.nav-group:first-child{margin-top:0;}
.nav-group-label{
  font-family:var(--font-mono);font-size:9.5px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--nav-ink-3);padding:0 10px 7px;
}
.nav-item{
  display:flex;align-items:center;gap:11px;width:100%;text-align:left;
  background:none;border:none;color:var(--nav-ink-2);cursor:pointer;
  padding:9px 11px;border-radius:10px;font-size:14px;font-weight:500;
  transition:background .16s var(--ease),color .16s var(--ease);
  position:relative;
}
.nav-item:hover{background:rgba(255,255,255,.07);color:var(--nav-ink);}
.nav-item.active{background:rgba(255,255,255,.1);color:#FFFFFF;font-weight:600;}
.nav-item.active::before{
  content:"";position:absolute;left:-12px;top:50%;transform:translateY(-50%);
  width:3px;height:20px;border-radius:0 3px 3px 0;background:var(--teal);
  box-shadow:0 0 9px rgba(12,155,138,.65);
}
.nav-ic{width:18px;height:18px;flex-shrink:0;opacity:.8;}
.nav-item.active .nav-ic{opacity:1;color:var(--teal);}
.nav-item .tag{
  margin-left:auto;font-family:var(--font-mono);font-size:8.5px;letter-spacing:.1em;
  padding:2px 6px;border-radius:6px;background:rgba(106,90,224,.28);
  color:#C9C0FF;border:1px solid rgba(106,90,224,.4);
}

.userchip{
  position:relative;z-index:2;font-size:13px;color:var(--nav-ink-2);
  display:flex;gap:9px;align-items:center;padding:14px 18px;
  margin-top:auto;border-top:1px solid var(--nav-line);
}
.userchip button{
  border:none;background:none;color:#F2A5A0;font-weight:600;
  cursor:pointer;font-size:13px;font-family:inherit;
}
.userchip button:hover{color:#FFFFFF;}

#content{flex:1;min-width:0;padding:34px clamp(20px,4vw,48px) 90px;max-width:1240px;}

/* ---- mobile top bar + drawer ----
   app.js toggles #sidebar.open and #scrim.show (it creates #scrim on
   first use). Those two class names are the contract — the admin
   panel's body.nav-open pattern is deliberately NOT used here. */
#topbar{display:none;}
.hamb{
  width:38px;height:36px;flex-shrink:0;border-radius:10px;cursor:pointer;
  background:rgba(255,255,255,.08);border:1px solid var(--nav-line);
  color:var(--nav-ink);font-size:16px;line-height:1;
  display:flex;align-items:center;justify-content:center;
}
@media(max-width:1000px){
  #sidebar{
    position:fixed;left:0;top:0;z-index:80;width:min(280px,84vw);
    transform:translateX(-100%);transition:transform .26s var(--ease);
    box-shadow:0 0 50px rgba(10,20,24,.4);
  }
  #sidebar.open{transform:translateX(0);}
  #topbar{
    display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:60;
    height:var(--top-h);padding:0 16px;margin:-22px -18px 18px;
    background:linear-gradient(120deg,#0E2A2E,#16303C 58%,#1A2340);
    border-bottom:1px solid rgba(10,20,24,.5);
  }
  #content{padding:22px 18px 80px;}
  #scrim{
    position:fixed;inset:0;z-index:70;background:rgba(10,20,24,.5);
    opacity:0;pointer-events:none;transition:opacity .25s var(--ease);
    backdrop-filter:blur(2px);
  }
  #scrim.show{opacity:1;pointer-events:auto;}
}

/* ============================================================
   CARDS
   ============================================================ */
.card{
  background:var(--bg-3);border:1px solid var(--edge);border-radius:var(--r);
  padding:20px;box-shadow:var(--shadow-sm);
}
.card.clickable{cursor:pointer;transition:transform .16s var(--ease),border-color .16s,box-shadow .16s;}
.card.clickable:hover{transform:translateY(-3px);border-color:var(--edge-2);box-shadow:0 12px 30px rgba(28,36,54,.1);}
.grid{display:grid;gap:15px;}
.grid.cols3{grid-template-columns:repeat(auto-fill,minmax(240px,1fr));}
.grid.cols2{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));}

/* ============================================================
   HERO
   ============================================================ */
.hero{position:relative;border-radius:var(--r-lg);overflow:hidden;
  background:linear-gradient(135deg,#FFFFFF,#EDF6F4);border:1px solid var(--edge);
  padding:clamp(26px,4vw,44px);margin-bottom:26px;
  box-shadow:0 20px 50px -30px rgba(12,155,138,.35);}
.hero-wave{position:absolute;inset:0;width:100%;height:100%;opacity:.6;}
.hero-inner{position:relative;z-index:2;max-width:640px;}
.hero h1{margin:14px 0 12px;}
.hero .grad{background:linear-gradient(100deg,var(--teal),var(--violet));-webkit-background-clip:text;background-clip:text;color:transparent;}
.hero-ipa{font-family:var(--font-display);font-size:15px;letter-spacing:.5em;color:var(--ink-3);margin-top:8px;user-select:none;}

/* ============================================================
   PAGE HEADER — mono eyebrow + display title, one hairline rule
   (the same composition the admin panel uses)
   ============================================================ */
.page-head{margin-bottom:24px;}
.page-head .eyebrow{display:block;margin-bottom:8px;}
.page-head .sub{margin-top:10px;}
.page-head::after{
  content:"";display:block;height:2px;width:56px;margin-top:16px;border-radius:2px;
  background:linear-gradient(90deg,var(--teal),var(--violet));
}

/* section card with a ruled head */
.sec-card{margin-bottom:14px;}
.card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:14px;}
.card-head h2{margin:0;font-size:18px;}
.card-tag{
  font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:.1em;
  color:var(--ink-2);background:var(--bg-2);border:1px solid var(--edge);
  padding:4px 10px;border-radius:99px;
}

/* label + description on the left, control hard right */
.set-row{display:flex;justify-content:space-between;align-items:center;gap:18px;flex-wrap:wrap;margin-top:16px;}
.set-copy{min-width:0;}
.set-copy strong{font-size:15px;}
.set-desc{font-size:13px;margin-top:4px;}

/* ============================================================
   STATS — each variant sets --stat-c once; the top hairline, the
   corner bloom, the icon tile and the numeral all read from it.
   .streak and .score are pre-existing class names, kept as-is.
   ============================================================ */
.stat-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:14px;margin:0 0 8px;}
.stat{
  --stat-c:var(--teal);
  background:var(--bg-3);border:1px solid var(--edge);border-radius:var(--r);
  padding:17px 18px;position:relative;overflow:hidden;box-shadow:var(--shadow-sm);
  transition:transform .16s var(--ease),border-color .16s,box-shadow .16s;
}
.stat:hover{transform:translateY(-3px);border-color:var(--edge-2);box-shadow:0 14px 30px rgba(28,36,54,.09);}
.stat::before{
  content:"";position:absolute;left:0;top:0;width:100%;height:3px;
  background:linear-gradient(90deg,var(--stat-c),transparent 78%);
}
.stat::after{
  content:"";position:absolute;right:-24px;top:-24px;width:80px;height:80px;
  border-radius:50%;background:var(--stat-c);opacity:.09;
}
.stat.score{--stat-c:var(--teal);}
.stat.streak{--stat-c:var(--amber);}
.stat.recs{--stat-c:var(--violet);}
.stat.weak{--stat-c:var(--red);}
.stat-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:11px;position:relative;z-index:1;}
.stat .lbl{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:var(--ink-3);line-height:1.3;}
.stat-ic{
  flex-shrink:0;width:28px;height:28px;border-radius:8px;display:flex;
  align-items:center;justify-content:center;color:var(--stat-c);
  background:var(--bg-2);
  background:color-mix(in srgb, var(--stat-c) 12%, transparent);
}
.stat-ic svg{width:15px;height:15px;}
.stat .num{
  font-family:var(--font-display);font-weight:900;font-size:34px;line-height:1;
  color:var(--stat-c);position:relative;z-index:1;
}

/* placement-test nudge */
.diag-prompt{border-left:3px solid var(--amber);margin-top:16px;}
.diag-prompt-title{display:block;margin-top:10px;font-size:17px;}
.diag-prompt-desc{font-size:14px;margin:6px 0 14px;}

/* "Jump in" tiles */
.jump{--jump-c:var(--teal);}
.jump.j-teal{--jump-c:var(--teal);}
.jump.j-violet{--jump-c:var(--violet);}
.jump.j-amber{--jump-c:var(--amber);}
.jump .ipa-glyph{color:var(--jump-c);}
.jump-desc{font-size:14px;margin-top:5px;}
.card.clickable.jump:hover{border-color:var(--jump-c);box-shadow:0 14px 30px -14px var(--jump-c);}

.prog-actions{margin-top:22px;}
button.ghost.danger{color:var(--red);border-color:var(--red-dim);}
button.ghost.danger:hover{border-color:var(--red);background:var(--red-dim);}

/* ============================================================
   TYPO BITS
   ============================================================ */
.ipa-glyph{font-family:var(--font-display);font-weight:900;font-size:46px;line-height:1;color:var(--teal);display:block;margin-bottom:10px;}
.pill{display:inline-block;font-family:var(--font-mono);font-size:11px;font-weight:700;padding:3px 10px;border-radius:99px;background:var(--teal-dim);color:var(--teal-d);letter-spacing:.03em;}
.pill.amber{background:var(--amber-dim);color:#8A6224;}
.pill.red{background:var(--red-dim);color:var(--red);}
.pill.gray{background:var(--bg-2);color:var(--ink-2);border:1px solid var(--edge);}
.pill.violet{background:var(--violet-dim);color:var(--violet-d);}

/* ============================================================
   BUTTONS
   ============================================================ */
button.primary{background:var(--teal);color:#FFFFFF;border:none;border-radius:99px;padding:12px 24px;font-size:15px;font-weight:700;cursor:pointer;box-shadow:0 10px 22px -12px rgba(12,155,138,.9);transition:filter .16s,transform .1s;}
button.primary:hover{filter:brightness(1.08);transform:translateY(-1px);}
button.primary:active{transform:scale(.97);}
button.ghost{background:transparent;border:1px solid var(--edge-2);border-radius:99px;padding:11px 22px;font-size:14px;font-weight:600;color:var(--ink);cursor:pointer;transition:border-color .16s,background .16s;}
button.ghost:hover{border-color:var(--teal);background:var(--bg-3);}
button.listen{background:var(--teal);color:#FFFFFF;border:none;border-radius:99px;padding:11px 22px;font-size:14px;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;gap:8px;transition:filter .16s,transform .1s;}
button.listen:hover{filter:brightness(1.08);} button.listen:active{transform:scale(.97);}
button.record{background:var(--red);color:#fff;border:none;border-radius:99px;padding:11px 22px;font-size:14px;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;gap:9px;transition:filter .16s,transform .1s;}
button.record:hover{filter:brightness(1.06);} button.record:active{transform:scale(.97);}
button.record.armed{animation:vu 1.1s infinite;}
@keyframes vu{0%{box-shadow:0 0 0 0 rgba(221,75,68,.5);}70%{box-shadow:0 0 0 14px rgba(221,75,68,0);}100%{box-shadow:0 0 0 0 rgba(221,75,68,0);}}
button:disabled{opacity:.45;cursor:not-allowed;}
/* an anchor that must read as a ghost button — used for /api/export, which
   navigates, so it has to stay a real link rather than become a button */
a.btn-link{
  display:inline-flex;align-items:center;gap:7px;text-decoration:none;
  background:transparent;border:1px solid var(--edge-2);border-radius:99px;
  padding:11px 22px;font-size:14px;font-weight:600;color:var(--ink);cursor:pointer;
  transition:border-color .16s,background .16s;
}
a.btn-link:hover{border-color:var(--teal);background:var(--bg-3);color:var(--ink);}
.dot{width:9px;height:9px;border-radius:50%;background:#fff;display:inline-block;}
.btn-row{display:flex;gap:11px;flex-wrap:wrap;align-items:center;margin:13px 0;}

/* ============================================================
   PRACTICE — the furniture practiceBlock() builds and re-queries.
   Every class name below is found again by app.js via
   root.querySelector(); style them freely, never rename them.
   ============================================================ */
.practice-panel{background:var(--bg-3);border:1px solid var(--edge);border-radius:var(--r);padding:24px;margin-top:16px;box-shadow:var(--shadow-sm);}
.target-word{font-family:var(--font-display);font-weight:600;font-size:42px;letter-spacing:-.01em;margin:8px 0 2px;}
.target-ipa{color:var(--ink-2);font-size:16px;font-family:var(--font-mono);margin-bottom:14px;}
canvas.wave{width:100%;height:84px;background:var(--bg-2);border:1px solid var(--edge);border-radius:12px;display:block;margin:12px 0;}
/* the live trace is drawn on a dark ground on purpose — it is the one
   "recording booth" surface in the app, and the teal/amber traces need it */
canvas.wave.live{background:#0A0E18;border-color:#0A0E18;box-shadow:inset 0 0 24px rgba(0,0,0,.5);}
.wave-legend{font-family:var(--font-mono);font-size:10.5px;color:var(--ink-3);margin:-6px 0 8px;}
.wave-legend b.t{color:var(--teal);} .wave-legend b.p{color:var(--amber);}

/* the speed picker that sits inside every practiceBlock row */
.speed-row select{padding:7px 10px;font-size:13px;border-radius:8px;}

/* shared bits the practice views lean on */
.stack{margin-top:18px;}
.chip-row{margin:14px 0;display:flex;gap:8px;flex-wrap:wrap;align-items:center;}
.panel-hint{font-size:14px;}
.field-label{
  display:block;font-family:var(--font-mono);font-size:10.5px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--ink-2);margin-bottom:7px;
}
.ink-amber{color:var(--amber);}
.sec-head{
  display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;
  margin:32px 0 12px;padding-bottom:10px;border-bottom:1px solid var(--edge);
}
.sec-head h2{margin:0;font-size:19px;}
.sec-tag{
  font-family:var(--font-mono);font-size:9.5px;text-transform:uppercase;
  letter-spacing:.14em;color:var(--ink-3);margin-left:auto;white-space:nowrap;
}
.filter-bar{margin:16px 0;gap:9px;}
.filter-label{
  font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-3);margin-right:2px;
}
/* filterTwisters() toggles .on for the selected difficulty (v1.20.1 — it
   used to write inline styles, which no stylesheet could match). */
.filter-bar button.ghost{padding:8px 16px;font-size:13px;}
.filter-bar button.ghost.on{background:var(--teal);border-color:var(--teal);color:#FFFFFF;}
.filter-bar button.ghost.on:hover{background:var(--teal-d);border-color:var(--teal-d);}

/* free talk */
.ft-prompt-label{font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);}
.ft-prompt{font-size:19px;}
.ft-timer{font-family:var(--font-mono);font-weight:700;color:var(--ink-2);}

/* ---------------- MOUTH DIAGRAM ---------------- */
.mouth-diagram-wrap{display:flex;gap:22px;align-items:center;flex-wrap:wrap;margin:14px 0;padding:18px;background:var(--bg-2);border:1px solid var(--edge);border-radius:var(--r);}
.mouth-diagram{flex-shrink:0;text-align:center;max-width:440px;flex:1 1 360px;}
/* Bigger than the 180px it used to be. This is the main teaching visual on the
   page and it was previously smaller than the practice-word chips beside it. */
.mouth-diagram svg{width:100%;max-width:420px;height:auto;}
.mouth-diagram-cap{font-size:11px;color:var(--ink-3);letter-spacing:.04em;margin-top:6px;
  max-width:420px;white-space:normal;line-height:1.5;}

/* ---- mouth diagram parts (v1.69.1, redrawn v1.90.0) ----
   LANDMARKS OVER PRETTINESS. Each part is styled by what it IS, so the
   articulation reads: the roof and teeth are structure; the tongue is the
   actor; air is dashed because it moves; voicing is at the larynx and carries
   a word. What changed in v1.90.0 is that the STRUCTURE became legible and the
   FOCUS became the loudest thing on screen, in that order. */
.ms{display:block;}
/* Orientation, not decoration — one hairline in the label gutter. It answers
   "what am I looking at, and which way does it face", which nothing did. */
.ms-face{fill:none;stroke:var(--edge-2);stroke-width:1.6;stroke-linecap:round;
  stroke-linejoin:round;opacity:.55;}
.ms-cavity{fill:none;stroke:var(--edge-2);stroke-width:1.6;stroke-dasharray:2 3;}
.ms-roof{fill:none;stroke:var(--ink-2);stroke-width:2.4;stroke-linecap:round;}
.ms-ridge{fill:none;stroke:var(--ink-2);stroke-width:3;stroke-linecap:round;}
.ms-pharynx{fill:none;stroke:var(--edge-2);stroke-width:1.8;}
/* The teeth are drawn ABOVE the tongue now, so they need an opaque fill: the
   point is that the tongue passes BETWEEN them rather than over them. */
.ms-teeth path{fill:var(--bg-3);stroke:var(--ink-2);stroke-width:1.7;stroke-linejoin:round;}
/* The tongue is the thing being instructed, so it is the one filled, heavy,
   saturated element. Everything else is context. */
.ms-tongue{fill:var(--teal);fill-opacity:.26;stroke:var(--teal);stroke-width:4;
  stroke-linejoin:round;stroke-linecap:round;}
.ms-tip{fill:var(--teal);}
/* A tip that has come THROUGH the teeth is ringed, so th does not depend on
   the reader noticing a few pixels of overlap. */
.ms-tip-through{stroke:var(--bg-3);stroke-width:3;}
.ms-velum{fill:none;stroke:var(--ink-2);stroke-width:3.2;stroke-linecap:round;}
/* A BODY, NOT A LINE. Two open strokes are two brackets however they are
   curved. Filled like the tongue on purpose: both are soft tissue that moves,
   the teeth are opaque, and structure is a thin stroke — that grammar is what
   lets the parts be told apart without reading a label. */
.ms-lip{fill:var(--red);fill-opacity:.30;stroke:var(--red);stroke-width:2.2;
  stroke-linejoin:round;stroke-linecap:round;}
.ms-compact .ms-lip{stroke-width:3;}
/* --indigo does not exist in brand.css, so the first version of this was
   stroked with nothing and the airflow simply never appeared. Checked the
   token list rather than guessing a second time — and --font-body is the sans
   here, there is no --font-sans. */
.ms-air{fill:none;stroke:var(--amber);stroke-width:2.8;stroke-linecap:round;opacity:.95;}
.ms-air-blocked{opacity:.3;}
/* --cyan is an ALIAS FOR --teal in brand.css, so voicing used to be drawn in
   exactly the tongue's hue: two unrelated concepts, one colour. */
.ms-voice{fill:none;stroke:var(--violet);stroke-width:2.8;}
.ms-voice-off{stroke:var(--ink-3);opacity:.55;}

/* ---- where the sound is made ----
   Marked by CONTRAST rather than by a fifth hue: a ring in the primary ink
   over a background halo, with the surrounding structure stepping back. A new
   colour would collide with tongue/air/lips, and contrast survives greyscale. */
.ms-focus-halo{fill:none;stroke:var(--bg-3);stroke-width:7;opacity:.95;}
.ms-focus-ring{fill:none;stroke:var(--ink);stroke-width:2.6;stroke-dasharray:3.5 4;}
.ms-focus-lead{fill:none;stroke:var(--ink);stroke-width:1.4;opacity:.55;}
/* Everything that is not the subject steps back when there IS a subject. */
.ms-has-focus .ms-bg{opacity:.42;}
.ms-compact .ms-focus-ring{stroke-width:3.4;stroke-dasharray:4 4;}
.ms-compact .ms-focus-halo{stroke-width:6;}

/* ---- labels ----
   Was 8.5px uppercase mono in --ink-3, drawn directly on the lines it named.
   A plate is what makes small type readable over a drawing, and sentence case
   beats uppercase mono at this size by a distance. */
.ms-lbl{font-family:var(--font-body);font-size:11px;fill:var(--ink-2);
  letter-spacing:.01em;text-anchor:middle;text-transform:none;font-weight:500;}
.ms-lbl-bg{fill:var(--bg-3);opacity:.86;}
.ms-lblg-focus .ms-lbl{fill:var(--ink);font-weight:650;}
.ms-lblg-focus .ms-lbl-bg{fill:var(--bg-3);opacity:.96;stroke:var(--edge-2);stroke-width:1;}
.ms-lblg-tongue .ms-lbl{fill:var(--teal-d);font-weight:650;}
.ms-lblg-lips .ms-lbl{fill:var(--red);}
.ms-lblg-air .ms-lbl{fill:var(--amber);}
.ms-lblg-voice .ms-lbl{fill:var(--violet);}
.ms-lblg-off .ms-lbl,.ms-lblg-faint .ms-lbl{fill:var(--ink-3);}
.ms-lbl-off{fill:var(--ink-3);}
@media (prefers-reduced-motion: reduce){
  /* The markup already ships without <animate> when the preference is set;
     this is the belt-and-braces half, for a preference that changes after the
     page rendered. */
  .ms animate{display:none;}
}

/* The key. Colour AND the word, never colour alone — the printable report's
   rule, and the diagram is the other thing in this app that gets printed. */
.mouth-key{display:flex;gap:14px;flex-wrap:wrap;justify-content:center;
  margin-top:8px;font-size:11.5px;color:var(--ink-2);}
.mouth-key span{display:inline-flex;align-items:center;gap:5px;}
.mouth-key i{width:15px;height:3px;border-radius:2px;display:inline-block;font-style:normal;}
.mk-tongue{background:var(--teal);}
.mk-air{background:var(--amber);}
.mk-lips{background:var(--red);}
.mk-focus{width:12px;height:12px;border-radius:50%;border:2.2px dashed var(--ink);background:none;}

.mouth-diagram-side{flex:1;min-width:200px;}

/* ---------------- VIEW SWITCHING (critical) ---------------- */
.view{display:none;}
.view.active{display:block;animation:viewin .28s ease;}
@keyframes viewin{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:none;}}
@media (prefers-reduced-motion: reduce){.view.active{animation:none;}}

/* app.js toggles .show — the base rule must stay display:none */
.result-box{margin-top:14px;padding:20px;border-radius:var(--r);background:var(--bg-2);border:1px solid var(--edge);display:none;box-shadow:inset 0 1px 0 rgba(255,255,255,.6);}
.result-box.show{display:block;animation:pop .3s ease;}
/* premium instant result */
.ir-wrap{display:flex;gap:20px;align-items:center;flex-wrap:wrap;}
.ir-ring{position:relative;width:110px;height:110px;flex-shrink:0;}
.ir-ring svg{width:100%;height:100%;}
.ir-num{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:var(--font-display);font-weight:900;font-size:28px;}
.ir-num.good{color:var(--teal);} .ir-num.mid{color:var(--amber);} .ir-num.low{color:var(--red);}
.ir-body{flex:1;min-width:200px;}
.ir-verdict{font-weight:700;font-size:15px;margin-bottom:6px;}
.ir-words{font-size:20px;margin-bottom:6px;letter-spacing:.01em;}
.ir-heard{font-size:13px;color:var(--ink-2);}
.ir-legend{font-size:12px;color:var(--ink-2);margin-top:6px;}
.ir-underline{border-bottom:2px solid var(--red);}
.ir-nohear{font-size:14px;}
/* ---- an unscorable take (v1.99.38) ----
   The note says WHY there is no number, and the box under it carries the one
   control a sequenced drill was missing: a way on that is not "give up". */
.ir-selfcheck{font-size:14px;}
/* The note is raw inline text, unlike every other result body, so the button
   would otherwise flow onto the end of that sentence and sit on top of it —
   found on the screen, which is the only thing that finds this. */
.ir-selfcheck .ir-next{display:block;}
.ir-noscore{margin-bottom:8px;font-size:13.5px;color:var(--ink-2);line-height:1.5;}
.ir-noscore strong{color:var(--ink);}
/* ---- pronunciation assessment (v1.25.0) ----
   The per-sound strip a word-match score can never produce. Tier colours match
   .word-green/amber/red elsewhere, so a colour means the same thing app-wide. */
.ir-assess:empty{display:none;}
.ir-measuring{display:inline-block;margin-top:8px;font-size:12.5px;color:var(--ink-2);opacity:.85;animation:pulse-dim 1.1s ease-in-out infinite;}
@keyframes pulse-dim{0%,100%{opacity:.85;}50%{opacity:.4;}}
.ir-measured{margin-top:9px;font-size:12px;color:var(--ink-2);line-height:1.45;}
.ir-phonemes{display:flex;flex-wrap:wrap;gap:10px;margin-top:9px;}
.ph-word{display:inline-flex;flex-direction:column;gap:4px;padding:5px 7px;border:1px solid var(--edge);border-radius:8px;background:var(--bg-2);}
.ph-label{font-size:10.5px;color:var(--ink-3);font-family:var(--font-mono);letter-spacing:.04em;}
.ph-row{display:inline-flex;gap:3px;}
.ph-syls{display:inline-flex;gap:4px;flex-wrap:wrap;}
.ph-syl{font-family:var(--font-mono);font-size:11.5px;padding:1px 5px;border-radius:5px;display:inline-flex;gap:4px;align-items:baseline;cursor:default;}
.ph-syl b{font-size:9.5px;opacity:.75;font-weight:700;}
.ph-sc{font-family:var(--font-mono);font-size:15px;line-height:1.5;padding:0 3px;border-radius:4px;cursor:default;}
.ph-green{color:#0B7D6E;background:rgba(45,212,189,.16);}
.ph-amber{color:#8A5A12;background:rgba(240,166,60,.18);}
.ph-red{color:#A82A25;background:rgba(226,90,84,.16);}
.ph-gray{color:var(--ink-3);}
/* ---- sentence targets (v1.26.0) ----
   A twelve-word sentence is ~40 phonemes; all of them in a row is wallpaper,
   not a reading. So words come first, and only the weak ones open up. */
.ir-words-acoustic{display:flex;flex-wrap:wrap;gap:5px;margin-top:9px;align-items:baseline;}
.wd-chip{font-size:15px;line-height:1.5;padding:1px 6px;border-radius:5px;cursor:default;}
.wd-omit{color:var(--ink-3);text-decoration:line-through;text-decoration-thickness:2px;opacity:.75;}
.wd-ins{color:#8A5A12;border-bottom:2px dotted #C98A2E;}
.wd-drill{display:flex;flex-direction:column;gap:3px;}
.wd-drill .ph-label{color:var(--ink-2);}
.ir-next{margin-top:16px;background:var(--ink);color:#fff;border:none;border-radius:99px;padding:11px 26px;font-family:inherit;font-size:14px;font-weight:700;cursor:pointer;box-shadow:0 10px 22px -14px rgba(28,36,54,.9);transition:transform .12s,background .2s;}
.ir-next:hover{transform:translateY(-2px);background:var(--teal);}
.ir-next:active{transform:translateY(0);}
/* diagnostic summary */
.diag-summary{text-align:center;animation:pop .4s ease;}
.diag-cols{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:16px;text-align:left;}
@media(max-width:600px){.diag-cols{grid-template-columns:1fr;}}
.diag-col-h{font-family:var(--font-mono);font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-2);margin-bottom:8px;}
.diag-row{display:flex;align-items:center;gap:10px;padding:7px 0;border-bottom:1px solid var(--edge);}
.diag-row span:nth-child(2){flex:1;}
.diag-score{font-family:var(--font-display);font-weight:900;font-size:15px;}
.diag-score.good{color:var(--teal);} .diag-score.low{color:var(--red);}
/* lesson complete */
.lesson-complete{text-align:center;padding:26px;animation:pop .4s ease;}
@keyframes pop{from{opacity:0;transform:translateY(8px) scale(.99);}to{opacity:1;transform:none;}}
.score-big{font-family:var(--font-display);font-weight:900;font-size:48px;line-height:1;}
.score-big.good{color:var(--teal);} .score-big.mid{color:var(--amber);} .score-big.low{color:var(--red);}
.heard{font-size:15px;margin-top:8px;}
.heard .word-ok{color:var(--teal);font-weight:600;}
.heard .word-bad{color:var(--red);font-weight:600;text-decoration:underline wavy;}
.word-green{color:var(--teal);font-weight:600;}
.word-yellow{color:var(--amber);font-weight:600;background:var(--amber-dim);border-radius:5px;padding:0 4px;}
.word-red{color:var(--red);font-weight:600;background:var(--red-dim);border-radius:5px;padding:0 4px;}
.ltr-bad{text-decoration:underline wavy var(--red);text-underline-offset:3px;}
.live-word{display:inline-block;transition:color .15s;margin-right:5px;}
.live-word.green{color:var(--teal);} .live-word.yellow{color:var(--amber);} .live-word.red{color:var(--red);}
.live-word.pending{color:var(--ink-3);opacity:.5;}

.word-chip{display:inline-block;background:var(--bg-2);border:1px solid var(--edge);border-radius:11px;padding:8px 15px;margin:4px 5px 0 0;font-size:15px;cursor:pointer;transition:border-color .14s,color .14s,transform .1s,box-shadow .14s;}
.word-chip:hover{border-color:var(--teal);color:var(--teal);box-shadow:0 4px 12px -6px rgba(12,155,138,.7);}
.word-chip:active{transform:scale(.96);}
.word-chip.sel{border-color:var(--teal);background:var(--teal);color:#FFFFFF;font-weight:600;box-shadow:0 8px 18px -10px rgba(12,155,138,.9);}

.tip-box{border-left:3px solid var(--teal);background:var(--teal-dim);padding:14px 17px;border-radius:0 var(--r-sm) var(--r-sm) 0;font-size:15px;margin:14px 0;}
.mouth-box{display:flex;gap:16px;align-items:center;flex-wrap:wrap;margin:12px 0;}
.speed-row{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--ink-2);}
select,input[type=text],input[type=password],input[type=number],textarea{
  border:1px solid var(--edge-2);border-radius:var(--r-sm);padding:10px 13px;font-size:14.5px;
  background:var(--bg-2);color:var(--ink);font-family:inherit;
  transition:border-color var(--dur) var(--ease),box-shadow var(--dur) var(--ease),background var(--dur) var(--ease);
}
select:hover,input[type=text]:hover,input[type=password]:hover,input[type=number]:hover,textarea:hover{border-color:var(--ink-3);}
select:focus,input[type=text]:focus,input[type=password]:focus,input[type=number]:focus,textarea:focus{
  outline:none;background:var(--bg-3);border-color:var(--teal);box-shadow:0 0 0 4px var(--teal-dim);
}
.type-box{width:100%;font-family:var(--font-display);font-size:20px;padding:15px;border:1px solid var(--edge-2);border-radius:12px;background:var(--bg-2);color:var(--ink);resize:vertical;min-height:74px;}

/* quiz */
.quiz-opts{display:flex;gap:12px;margin:16px 0;flex-wrap:wrap;}
.quiz-opt{flex:1;min-width:140px;background:var(--bg-2);border:2px solid var(--edge);border-radius:var(--r);padding:22px;font-family:var(--font-display);font-size:26px;cursor:pointer;text-align:center;transition:border-color .14s,transform .1s,box-shadow .14s;}
.quiz-opt:hover{border-color:var(--teal);transform:translateY(-2px);box-shadow:0 12px 26px -14px rgba(12,155,138,.8);}
.quiz-opt.correct{border-color:var(--teal);background:var(--teal-dim);box-shadow:0 0 0 4px var(--teal-dim);}
.quiz-opt.wrong{border-color:var(--red);background:var(--red-dim);box-shadow:0 0 0 4px var(--red-dim);}
.quiz-feedback{min-height:24px;font-weight:600;margin-top:4px;}

/* progress */
.bar-track{height:9px;background:var(--bg-2);border-radius:99px;overflow:hidden;margin-top:8px;border:1px solid var(--edge);}
.bar-fill{height:100%;background:var(--teal);border-radius:99px;width:0;transition:width .5s cubic-bezier(.2,.8,.2,1);}
.prog-row{display:flex;align-items:center;gap:13px;padding:11px 0;border-bottom:1px solid var(--edge);}
.prog-row:last-child{border-bottom:none;}
.prog-sym{font-family:var(--font-display);font-weight:900;font-size:25px;width:54px;color:var(--teal);}
.prog-mid{flex:1;} .prog-pct{font-family:var(--font-mono);font-weight:700;width:52px;text-align:right;}
/* ---- sounds to work on (v1.28.0) ----
   Direction is carried by an arrow AND a signed number, never by colour
   alone — the same rule the printable report follows. */
.tr-meta{color:var(--ink-soft);font-size:13px;}
.tr-up{color:var(--teal);font-weight:700;}
.tr-down{color:var(--rec);font-weight:700;}
.tr-flat{color:var(--ink-soft);}
.tr-none{color:var(--ink-soft);font-style:italic;}
.tr-go{flex:none;padding:6px 13px;font-size:12.5px;}
.tr-foot{font-size:12px;margin:12px 0 0;line-height:1.5;}

.back-link{
  background:var(--bg-2);border:1px solid var(--edge);color:var(--ink-2);
  font-size:13.5px;font-weight:600;cursor:pointer;padding:7px 15px 7px 12px;
  margin-bottom:18px;display:inline-flex;gap:7px;align-items:center;border-radius:99px;
  transition:color .14s,border-color .14s,background .14s;font-family:inherit;
}
.back-link:hover{color:var(--ink);border-color:var(--edge-2);background:var(--bg-3);}

/* ============================================================
   VIEW ACTIONS (v1.21.0) — in-app Home + restart-this-drill,
   sitting under the page-head rule on every practice view.

   Deliberately the small size the filter bars use, not the size
   of Listen/Record: this is chrome for leaving or restarting,
   and it must never read as part of the exercise.
   ============================================================ */
.view-actions{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:-8px 0 22px;}
.view-actions button.ghost{padding:8px 16px;font-size:13px;display:inline-flex;gap:7px;align-items:center;}
.view-actions .va-ic{font-size:14px;line-height:1;}
/* in the phoneme detail the row follows "← All sounds", which owns the gap */
#soundDetail .view-actions{margin-top:0;}
/* the AI gate banner: was light amber text on a light amber panel — a
   leftover from the dark theme, effectively invisible */
.banner{background:var(--amber-dim);border:1px solid #EBD3AC;border-radius:var(--r-sm);padding:13px 16px;font-size:14px;margin-bottom:18px;display:none;color:#7A5A2A;}
.banner.show{display:block;}
.twister-text{font-family:var(--font-display);font-size:22px;line-height:1.5;margin:12px 0;}
audio{width:100%;margin-top:10px;}
/* empty states read as a deliberate placeholder rather than lost text */
.empty{
  color:var(--ink-3);font-size:14.5px;padding:22px;text-align:center;
  background:var(--bg-2);border:1px dashed var(--edge-2);border-radius:var(--r-sm);
}

/* diagnostic */
.diag-progress{display:flex;gap:5px;margin:14px 0;}
.diag-dot{flex:1;height:6px;border-radius:99px;background:var(--bg-2);border:1px solid var(--edge);}
.diag-dot.done{background:var(--teal);border-color:var(--teal);} .diag-dot.cur{background:var(--amber);border-color:var(--amber);}
.freetalk-transcript{background:var(--bg-2);border:1px solid var(--edge);border-radius:12px;padding:16px;font-size:16px;line-height:1.7;margin-top:14px;min-height:60px;}

/* ============================================================
   AI STUDIO — violet is the AI accent across all three views
   ============================================================ */
.ai-head::after{background:linear-gradient(90deg,var(--violet),var(--teal));}
.eyebrow-ai{color:var(--violet);}
.ai-run{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:14px 0 0;}
.ai-run .grow{flex:1;min-width:220px;}
.ai-status{font-size:13px;}

/* Conversation Partner start panel */
.setup-label{
  font-family:var(--font-mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;
  font-weight:400;color:var(--ink-2);margin-top:22px;margin-bottom:8px;
  display:flex;align-items:center;gap:8px;
}
#convSetup > .setup-label:first-child{margin-top:0;}
.setup-optional{
  font-family:var(--font-body);font-size:10.5px;letter-spacing:0;text-transform:none;
  color:var(--ink-3);background:var(--bg-2);border:1px solid var(--edge);
  border-radius:99px;padding:1px 8px;
}
.setup-hint{font-size:12.5px;margin-top:7px;color:var(--ink-3);}
.setup-actions{margin-top:24px;}
.setup-status{font-size:13px;}
/* app.js fills this with .word-chip spans, which carry their own margins —
   zero them so the container's gap is the only spacing */
.scenario-grid{margin-top:8px;display:flex;flex-wrap:wrap;gap:8px;}
.scenario-grid .word-chip,.chip-row .word-chip{margin:0;}
.setup-roles{display:flex;gap:12px;flex-wrap:wrap;}
.setup-roles label{flex:1;min-width:190px;font-size:12.5px;color:var(--ink-2);display:block;}
.setup-roles input{width:100%;margin-top:5px;}
.coach-tip{display:block;margin-top:7px;padding-top:7px;border-top:1px dashed var(--edge-2);font-size:13.5px;color:var(--violet);}

/* A GATED REGION (v1.79.0) — controls that cannot act, and do not pretend to.

   The banner above these panels says WHY; this makes the controls agree with
   it. Before this, Conversation's whole setup form stayed fully interactive
   with only "Start conversation" greyed out at the bottom of a scrolling page,
   and Lesson Maker's topic chips stayed live beside a disabled Generate
   button — so both screens read as working.

   setPanelGated() sets `disabled` on every real form control, which is what
   actually stops them and what a screen reader announces. The pointer block
   below is for the CHIPS, which are <span>s: they cannot be disabled, and
   without it the click reaches the delegated handler. Both halves are needed —
   neither alone serves both audiences. Nothing here depends on the dimming:
   remove the opacity and the region is still inert. */
.is-gated{opacity:.55;}
.is-gated .word-chip,.is-gated [aria-disabled="true"]{pointer-events:none;cursor:not-allowed;}
.is-gated .word-chip:hover{border-color:var(--edge);color:inherit;box-shadow:none;}
.is-gated button[disabled],.is-gated input[disabled],
.is-gated select[disabled],.is-gated textarea[disabled]{cursor:not-allowed;}

/* live conversation panel */
.scenario-bar{justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;
  margin:0 0 14px;padding-bottom:13px;border-bottom:1px solid var(--edge);}
.conv-log{max-height:340px;overflow-y:auto;margin:14px 0;padding-right:4px;}
.conv-log::-webkit-scrollbar{width:8px;}
.conv-log::-webkit-scrollbar-thumb{background:var(--edge-2);border-radius:99px;}
.rt-status{font-size:13px;}
.conv-fallback{font-size:13px;margin-top:10px;}
/* hands-free loop: one state indicator */
.rt-state{display:inline-flex;align-items:center;gap:8px;font-size:13.5px;font-weight:600;color:var(--ink-2);min-height:22px;}
.rt-dot{width:9px;height:9px;border-radius:50%;background:var(--edge-2);flex-shrink:0;}
.rt-listening .rt-dot{background:var(--teal);animation:rtPulse 1.2s ease-in-out infinite;}
.rt-thinking .rt-dot{background:var(--amber);animation:rtPulse .9s ease-in-out infinite;}
.rt-speaking .rt-dot{background:var(--violet);animation:rtPulse 1s ease-in-out infinite;}
.rt-paused .rt-dot{background:var(--ink-3);}
.rt-ended .rt-dot{background:var(--edge-2);}
.rt-listening{color:var(--teal);} .rt-speaking{color:var(--violet);}
@keyframes rtPulse{0%,100%{opacity:1;transform:scale(1);}50%{opacity:.45;transform:scale(.82);}}
@media (prefers-reduced-motion: reduce){ .rt-dot{animation:none !important;} }
.rt-interim{font-size:14px;color:var(--ink-3);font-style:italic;margin-top:6px;min-height:20px;}
/* strictness segment */
.strict-seg{display:inline-flex;border:1px solid var(--edge-2);border-radius:99px;overflow:hidden;}
.strict-seg button{border:none;background:var(--bg-2);padding:8px 17px;font-size:13px;font-weight:600;color:var(--ink-2);cursor:pointer;transition:background .14s,color .14s;font-family:inherit;}
.strict-seg button:hover{background:var(--bg-3);color:var(--ink);}
.strict-seg button.on{background:var(--teal);color:#FFFFFF;}

/* stress tapper — app.js adds .tapped then .stressed */
.syl{display:inline-block;padding:7px 15px;margin:4px;border-radius:var(--r-sm);background:var(--bg-2);border:1px solid var(--edge);font-family:var(--font-display);font-size:21px;transition:background .16s var(--ease),border-color .16s,transform .12s;}
.syl.stressed{background:var(--amber);color:#1A1408;font-weight:700;border-color:var(--amber);transform:scale(1.06);box-shadow:0 8px 18px -10px rgba(217,138,31,.9);}
.syl.tapped{outline:2px solid var(--teal);outline-offset:1px;}

/* conversation bubbles */
.bubble{max-width:80%;padding:13px 17px;border-radius:18px;margin:10px 0;font-size:15px;line-height:1.5;animation:pop .25s ease;}
.bubble.user{background:var(--teal);color:#FFFFFF;margin-left:auto;border-bottom-right-radius:5px;font-weight:500;box-shadow:0 8px 20px -12px rgba(12,155,138,.9);}
.bubble.ai{background:var(--bg-2);border:1px solid var(--edge);color:var(--ink);border-bottom-left-radius:5px;box-shadow:var(--shadow-sm);}
.bubble.ai .replay{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--violet);cursor:pointer;font-weight:700;margin-top:7px;display:inline-block;}
.bubble.ai .replay:hover{color:var(--violet-d);}
.coach-notes{background:var(--violet-dim);border-left:3px solid var(--violet);border-radius:0 var(--r-sm) var(--r-sm) 0;padding:18px 20px;font-size:15px;line-height:1.7;white-space:pre-wrap;}

/* end-of-conversation language report */
.rep{margin:4px 0 14px;animation:pop .25s ease;}
.rep-head{font-family:var(--font-display);font-weight:900;font-size:22px;letter-spacing:-.01em;color:var(--ink);margin-bottom:4px;}
.rep-sub{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3);margin-bottom:16px;}
.rep-card{background:var(--bg-3);border:1px solid var(--edge);border-radius:var(--r);padding:17px 19px;margin-bottom:11px;box-shadow:var(--shadow-sm);}
.rep-card h3{font-family:var(--font-body);font-size:12px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--ink-2);margin:0 0 10px;}
.rep-card p{margin:0;}
.rep-summary{background:var(--teal-dim);border-color:rgba(12,155,138,.22);font-size:15px;line-height:1.65;box-shadow:none;}
.rep-summary h3{color:var(--teal-d);}
.rep-good{font-size:14.5px;line-height:1.55;color:var(--ink-2);}
.rep-good::before{content:"✓ ";color:var(--teal);font-weight:700;}
.rep-item{border-left:3px solid var(--edge-2);padding:2px 0 2px 13px;margin-bottom:12px;}
.rep-label{font-size:12.5px;font-weight:600;color:var(--violet);margin-bottom:3px;}
.rep-said{font-size:14.5px;line-height:1.5;color:var(--ink-2);text-decoration:line-through;text-decoration-color:var(--edge-2);}
.rep-better{font-size:14.5px;line-height:1.5;color:var(--ink);font-weight:500;margin-top:2px;}
.rep-better::before{content:"→ ";color:var(--teal);font-weight:700;}
.rep-note{font-size:13px;color:var(--ink-2);margin-top:4px;line-height:1.5;}
.rep-text{font-size:14.5px;line-height:1.65;color:var(--ink);}
.rep-chips{margin-top:8px;display:flex;flex-wrap:wrap;gap:7px;}
.rep-chip{background:var(--bg-2);border:1px solid var(--edge);border-radius:99px;padding:5px 12px;font-size:13px;color:var(--ink);}
.rep-chip.filler{background:var(--amber-dim);border-color:var(--amber-dim);font-family:var(--font-mono);font-size:12px;}
.rep-msg{font-size:14.5px;line-height:1.65;color:var(--ink-2);}
.rep-err{color:var(--red);}
.rep-actions{margin-top:16px;}
.rep-step{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:11px 0;border-top:1px solid var(--edge);}
.rep-card .rep-step:first-of-type{border-top:none;padding-top:0;}
.rep-step-main{flex:1;min-width:190px;}
.rep-step-name{font-size:14.5px;font-weight:600;color:var(--ink);}
.rep-topic{display:inline-block;margin-top:5px;background:var(--violet-dim);color:var(--violet-d);border-radius:99px;padding:3px 11px;font-size:12.5px;}
button.rep-go{flex-shrink:0;}

/* ipa chart */
.ipa-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:11px;margin-top:12px;}
.ipa-cell{background:var(--bg-3);border:1px solid var(--edge);border-radius:13px;padding:14px;text-align:center;cursor:pointer;transition:transform .12s,border-color .14s;box-shadow:var(--shadow-sm);}
.ipa-cell:hover{border-color:var(--teal);transform:translateY(-2px);}
.ipa-cell .g{font-family:var(--font-display);font-weight:900;font-size:30px;color:var(--teal);}
.ipa-cell .ex{font-family:var(--font-mono);font-size:11px;color:var(--ink-3);}

.app-foot{text-align:center;color:var(--ink-3);font-size:12.5px;font-family:var(--font-mono);padding:20px;border-top:1px solid var(--edge);display:flex;flex-direction:column;align-items:center;gap:10px;}
/* Legal links inside the app (v1.62.0) — a learner should not have to log out
   to read the policy governing the account they are signed in to. */
.app-foot-links{display:flex;gap:18px;flex-wrap:wrap;justify-content:center;font-family:var(--font-body);}
.app-foot-links a{font-size:12.5px;color:var(--ink-2);text-decoration:none;}
.app-foot-links a:hover{color:var(--teal);text-decoration:underline;}
.app-foot-links a:focus-visible{outline:2px solid var(--teal);outline-offset:2px;border-radius:3px;}
/* The app footer is a child of <body>, not of #shell, so it is on screen
   underneath the landing page too — that predates this version. What must not
   happen is the legal links appearing TWICE there, once in the landing footer
   and again below it, so on the landing page this copy stands down. The
   landing footer's own links are the ones a visitor sees. */
body[data-state="landing"] .app-foot-links{display:none;}

/* ============================================================
   RESPONSIVE SWEEP
   The rail already goes off-canvas at 1000px (see the shell block).
   Everything below is about the content column at narrow widths.
   ============================================================ */
@media(max-width:720px){
  .practice-panel{padding:18px;}
  .quiz-opt{min-width:120px;padding:18px;font-size:23px;}
  .setup-roles label{min-width:100%;}
  .bubble{max-width:92%;}
  .mouth-diagram-wrap{gap:16px;padding:15px;}
  .mouth-diagram svg{width:100%;max-width:100%;height:auto;}
}
@media(max-width:640px){
  .target-word{font-size:32px;}
  .hero h1{font-size:30px;}
  .stat .num{font-size:29px;}
  .ipa-grid{grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:9px;}
  .ipa-cell{padding:11px;}
  .ipa-cell .g{font-size:25px;}
  .twister-text{font-size:19px;}
  .score-big{font-size:40px;}
  /* the score ring and its verdict stack rather than squeeze side by side */
  .ir-wrap{gap:14px;}
  .ir-ring{width:92px;height:92px;}
  .prog-sym{width:44px;font-size:22px;}
  .sec-head{margin-top:26px;}
  /* full-width primary actions read better than half-width pills on a phone */
  .btn-row button.record,.btn-row button.listen{flex:1;justify-content:center;min-width:140px;}
}
@media(max-width:420px){
  .quiz-opts{flex-direction:column;}
  .quiz-opt{min-width:0;}
  .strict-seg{width:100%;}
  .strict-seg button{flex:1;padding:8px 10px;}
  .stat-row{grid-template-columns:1fr 1fr;}
}

@media (prefers-reduced-motion: reduce){*{animation:none!important;transition:none!important;scroll-behavior:auto!important;}}


/* ============================================================
   NUMBERS (v1.30.0)
   The display is big because it is what the learner recognises; the spoken
   form sits under it because it is what they actually have to produce — and
   what they are scored against.
   ============================================================ */
.num-display{
  font-family:var(--font-display);font-weight:900;font-size:34px;
  letter-spacing:-.02em;line-height:1.05;color:var(--ink);
}
.num-say{
  font-family:var(--font-mono);font-size:15px;color:var(--teal);
  margin-top:4px;letter-spacing:.01em;
}
.num-note{font-size:13px;color:var(--ink-soft);margin-top:6px;}
.numf.on{background:var(--ink);color:#fff;border-color:var(--ink);}
@media(max-width:640px){ .num-display{font-size:28px;} }

/* ============================================================
   PRACTICE SESSION (v1.29.0)
   Peripheral by design. A ticking clock in the middle of the page is a nag;
   this sits out of the reading path and shows minutes, not seconds, until the
   last stretch actually warrants precision.
   ============================================================ */
#sessChip{
  position:fixed;right:18px;bottom:18px;z-index:65;
  display:flex;align-items:center;gap:10px;
  padding:9px 12px 9px 13px;border-radius:99px;
  background:var(--bg-3);border:1px solid var(--edge);
  box-shadow:0 8px 26px -16px rgba(28,36,54,.55);
  font-size:13px;
}
.sess-dot{
  width:8px;height:8px;border-radius:99px;background:var(--teal);flex:none;
}
@media (prefers-reduced-motion: no-preference){
  .sess-dot{animation:sessPulse 2.4s ease-in-out infinite;}
}
@keyframes sessPulse{0%,100%{opacity:1;}50%{opacity:.35;}}
.sess-body{display:flex;flex-direction:column;line-height:1.2;}
.sess-time{font-family:var(--font-mono);font-weight:700;font-size:13px;color:var(--ink);}
.sess-label{font-size:10px;color:var(--ink-3);letter-spacing:.02em;}
.sess-end{
  flex:none;border:1px solid var(--edge);background:transparent;color:var(--ink-2);
  font-family:inherit;font-size:11px;font-weight:700;cursor:pointer;
  padding:4px 10px;border-radius:99px;
}
.sess-end:hover{border-color:var(--rec);color:var(--rec);}
/* The last two minutes earn some urgency — and only then. */
#sessChip.endgame{border-color:#EBD3AC;background:var(--amber-dim);}
#sessChip.endgame .sess-time{color:#7A5A2A;}
#sessChip.endgame .sess-dot{background:var(--amber);}
/* Paused is shown, not hidden: a frozen timer is how the learner learns this
   measures practice rather than the clock on the wall. */
#sessChip.paused .sess-dot{background:var(--ink-3);animation:none;}
#sessChip.paused .sess-time{color:var(--ink-3);}
@media(max-width:520px){ #sessChip{right:12px;bottom:12px;} }

/* ---- the wrap-up ---- */
#sessSummary{
  position:fixed;inset:0;z-index:95;display:flex;align-items:center;justify-content:center;
  padding:22px;background:rgba(12,18,30,.55);backdrop-filter:blur(3px);
}
.ss-card{
  width:min(520px,100%);max-height:88vh;overflow:auto;
  background:var(--bg-3);border:1px solid var(--edge);border-radius:var(--r-lg,16px);
  padding:26px 26px 22px;box-shadow:0 30px 70px -30px rgba(10,20,40,.6);
}
.ss-eyebrow{
  font-family:var(--font-mono);font-size:10px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--teal);
}
.ss-title{font-family:var(--font-display);font-weight:900;font-size:26px;margin:6px 0 16px;letter-spacing:-.02em;}
.ss-grid{display:grid;grid-template-columns:auto 1fr;gap:8px 16px;align-items:baseline;}
.ss-k{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--ink-3);white-space:nowrap;}
.ss-v{font-size:14px;color:var(--ink);overflow-wrap:anywhere;}
.ss-note{font-size:12.5px;color:var(--ink-2);margin:18px 0 0;line-height:1.5;}
.ss-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:18px;}

/* ============================================================
   YOUR PLAN (v1.42.0) — the learner's own view of what they hold
   ============================================================ */
.plan-card{
  border:1px solid var(--edge);border-radius:var(--r,16px);
  background:var(--bg-1,#fff);padding:20px 22px;margin-bottom:18px;
}
.plan-card-head{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;flex-wrap:wrap;}
.plan-card-head h2{font-family:var(--font-display);font-weight:900;font-size:26px;margin:4px 0 2px;letter-spacing:-.02em;}
.plan-card-head .sub{font-size:13.5px;margin:0;}

/* The number is large because "how long have I got" is the one thing
   a learner opens this card to find out. */
.plan-days{text-align:right;flex:none;}
.plan-days-n{display:block;font-family:var(--font-display);font-weight:900;font-size:34px;line-height:1;color:var(--teal);}
.plan-days-l{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--ink-3);}

/* Every feature is listed, held or not, each with a word saying which.
   A list of only what you have cannot show what you are missing, and a
   tick alone would leave the state carried by colour and shape only. */
.plan-feats{list-style:none;padding:0;margin:16px 0 0;display:grid;gap:7px;}
.plan-feats li{display:flex;align-items:baseline;gap:9px;font-size:14px;}
.plan-feat-mark{font-family:var(--font-mono);font-size:12px;flex:none;width:1.6em;}
.plan-feats li.has .plan-feat-mark{color:var(--teal);}
.plan-feats li.hasnt{color:var(--ink-3);}
.plan-feats li.hasnt .plan-feat-mark{color:var(--ink-3);}
.plan-feat-why{font-size:12px;color:var(--ink-3);margin-left:6px;}

/* v1.98.2 — ONE ROW PER ACTIVE GRANT, each carrying its OWN countdown.

   Only shown when a learner holds two or more, which a standalone
   time-limited pack makes ordinary. The per-row date is the whole point:
   the single number in .plan-days above cannot be true of two plans at
   once, so when there are two it is hidden and these replace it. */
.plan-grants{list-style:none;padding:0;margin:14px 0 0;display:grid;gap:8px;}
.plan-grants li{
  display:flex;justify-content:space-between;align-items:baseline;gap:12px;
  flex-wrap:wrap;padding:9px 12px;border:1px solid var(--edge);
  border-radius:10px;background:var(--bg-2,transparent);
}
.plan-grant-name{font-weight:700;font-size:14px;}
.plan-grant-until{font-size:12px;color:var(--ink-3);display:block;font-weight:400;}
/* Mono and teal so it reads as the same KIND of number as .plan-days-n,
   which it is standing in for. */
.plan-grant-days{
  font-family:var(--font-mono);font-size:12px;color:var(--teal);
  white-space:nowrap;flex:none;
}

/* THE CLIFF, NAMED BEFORE IT ARRIVES (v1.99.36).

   Amber and not red, deliberately: nothing has gone wrong, nothing is being
   withdrawn early, and the learner is not being asked to do anything. It is a
   date they are entitled to know about while they can still act on it, which
   is the opposite of the failure it replaces — a feature that simply stopped
   working one morning with no sentence anywhere on the page. */
.plan-loss{
  margin:12px 0 0;padding:10px 12px;border:1px solid var(--amber);
  border-radius:10px;background:var(--bg-2,transparent);
  font-size:13px;line-height:1.55;color:var(--ink-2);
}
.plan-loss b{color:var(--amber);font-weight:700;}

.plan-redeem{margin-top:20px;padding-top:16px;border-top:1px solid var(--edge);}
.plan-redeem label{display:block;font-family:var(--font-mono);font-size:10px;
  text-transform:uppercase;letter-spacing:.1em;color:var(--ink-3);margin-bottom:7px;}
.plan-redeem-row{display:flex;gap:8px;flex-wrap:wrap;}
.plan-redeem-row input{
  flex:1 1 200px;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  letter-spacing:.06em;text-transform:uppercase;
}
.plan-redeem-msg{font-size:13.5px;font-weight:600;min-height:19px;margin-top:9px;}
.plan-redeem-msg.ok{color:var(--teal);}
.plan-redeem-msg.bad{color:var(--rec,#C4553C);}

/* ---- native-language personalisation (v1.63.0) ----
   The reason a sound is near the top is stated in WORDS, never by colour or
   position alone: the same rule the printable report follows. */
.ph-why{
  display:block;margin-top:7px;font-size:11.5px;line-height:1.35;
  font-family:var(--font-mono);letter-spacing:.01em;
}
.ph-why-measured{color:var(--red);}
.ph-why-language{color:var(--ink-2);}
.card.is-focus{border-color:var(--teal);box-shadow:0 0 0 1px var(--teal-dim) inset;}

.lang-prompt{margin-bottom:16px;}
.lang-prompt-title{display:block;font-size:16px;margin-bottom:4px;}
.lang-prompt-desc{margin-bottom:12px;}
.lang-prompt-row{display:flex;gap:10px;flex-wrap:wrap;align-items:center;}
.lang-prompt-row select{
  flex:1 1 220px;min-width:180px;padding:10px 12px;
  border:1px solid var(--edge);border-radius:var(--r-sm);
  background:var(--bg-3);color:var(--ink);font-family:inherit;font-size:14px;
}
/* .auth-optional, .auth-hint and #authLangWrap select lived here until
   v1.99.30, styling the sign-up form's first-language field. That field moved
   into the onboarding; these went with it rather than being left behind, since
   an unreferenced rule is indistinguishable from one whose element somebody
   forgot to render. .ob-select in index.html is its replacement. */

/* ---- practice streak (v1.63.0) ----
   The number always carries a word: a flame alone means nothing to a first-
   time viewer and nothing at all to a screen reader. */
/* ---- status cluster (v1.68.2) ----
   TWO KINDS OF INFORMATION IN ONE BLOCK, kept apart by typographic register
   rather than by a divider. The level is a name that changes a few times a
   year, so it is set in the display face and reads as a title. The streak is a
   number that changes every day, so it is set in the mono face at label size
   and reads as a readout. Give both the same weight and the eye has to choose
   between them on every glance; give them different jobs and it does not. */
.status-cluster{
  position:relative;z-index:2;
  margin:0 18px 10px;padding:10px 12px;border-radius:10px;
  background:rgba(255,255,255,.07);border:1px solid var(--nav-line);
}
.sc-level{display:flex;align-items:center;gap:8px;}
.sc-badge{
  flex:none;width:20px;height:20px;display:grid;place-items:center;
  border-radius:6px;background:rgba(12,155,138,.22);
}
.sc-badge svg{width:12px;height:12px;stroke:#5FD8C6;fill:none;stroke-width:2.2;
  stroke-linecap:round;stroke-linejoin:round;}
.sc-name{
  font-family:var(--font-display);font-weight:700;font-size:13.5px;
  color:#FFFFFF;line-height:1.2;
}
/* The companion line. Indented to the level's text column so the two read as
   one block rather than two stacked chips. */
.sc-streak{
  display:flex;align-items:center;gap:5px;margin:5px 0 0 28px;
  font-family:var(--font-mono);font-size:9.5px;letter-spacing:.11em;
  text-transform:uppercase;color:var(--nav-ink-3);
}
.sc-streak.is-empty{display:none;}
.streak-flame{font-size:11px;line-height:1;}
.sc-n{color:#FFFFFF;font-weight:700;letter-spacing:.04em;font-size:11px;}
.sc-off{opacity:.8;}

.streak-nudge{
  display:flex;align-items:center;gap:12px;
  margin:0 0 16px;padding:12px 14px;
  border:1px solid var(--edge);border-left:3px solid var(--amber);
  border-radius:var(--r-sm);background:var(--bg-2);font-size:14px;
}
.nudge-text{flex:1;}
.nudge-x{
  border:none;background:none;cursor:pointer;color:var(--ink-3);
  font-size:15px;line-height:1;padding:4px 6px;border-radius:6px;
}
.nudge-x:hover{color:var(--ink);background:var(--bg);}

/* ---- white-label (v1.66.0) ----
   The operator's mark is rendered by the app and cannot be removed by any
   agency-supplied field: a white-labelled page still says whose software it
   is running on. */
.powered-by{
  position:fixed;right:10px;bottom:8px;z-index:5;
  font-family:var(--font-mono);font-size:9.5px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--ink-3);opacity:.75;pointer-events:none;
}
.brand-img{object-fit:contain;border-radius:7px;}
@media print{ .powered-by{display:none;} }

/* ---- milestones (v1.68.2) ----
   The ladder shows every tier, because "what is next" is the question this
   card exists to answer. Nothing depends on colour alone: reached steps carry
   a tick and the word "reached", and the current one is named in text. */
.ms-steps{list-style:none;margin:0 0 14px;padding:0;display:flex;flex-direction:column;gap:2px;}
.ms-step{
  display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:var(--r-sm);
  font-size:14px;color:var(--ink-soft);
}
.ms-step.is-done{color:var(--ink);}
.ms-step.is-now{background:var(--bg-2);color:var(--ink);font-weight:600;}
.ms-dot{
  flex:none;width:18px;height:18px;border-radius:50%;display:grid;place-items:center;
  font-size:11px;line-height:1;color:#fff;background:var(--edge);
}
.ms-step.is-done .ms-dot{background:var(--teal);}
.ms-step.is-now .ms-dot{background:var(--teal);box-shadow:0 0 0 3px rgba(12,155,138,.18);}
.ms-name{flex:1;}
.ms-word{
  font-family:var(--font-mono);font-size:9.5px;letter-spacing:.11em;
  text-transform:uppercase;color:var(--ink-3);
}
.ms-step.is-now .ms-word{color:var(--teal);}
.ms-next{margin:0 0 8px;font-size:14px;color:var(--ink);}
.ms-basis{margin:0;font-size:12.5px;color:var(--ink-3);line-height:1.5;}

/* The per-sound badge. The word carries it; the star is decoration and is
   hidden from screen readers. `measured` vs `word-match` is the same
   distinction the printable report makes, for the same reason. */
.ph-mastered{
  display:inline-flex;align-items:center;gap:5px;margin-left:6px;
  padding:2px 7px;border-radius:999px;
  background:rgba(12,155,138,.12);border:1px solid rgba(12,155,138,.35);
  font-family:var(--font-mono);font-size:9.5px;letter-spacing:.09em;
  text-transform:uppercase;color:var(--teal);white-space:nowrap;
}
.ph-ms-src{opacity:.7;border-left:1px solid rgba(12,155,138,.35);padding-left:5px;}
.card.is-mastered{border-color:rgba(12,155,138,.4);}

/* The milestone banner. Deliberately close in weight to the streak nudge
   rather than louder than it: this is a moment worth marking, not an
   interruption, and the two must be able to sit above each other without the
   page turning into a stack of announcements. */
.ms-toast{
  display:flex;align-items:center;gap:12px;
  margin:0 0 16px;padding:12px 14px;
  border:1px solid var(--edge);border-left:3px solid var(--teal);
  border-radius:var(--r-sm);background:var(--bg-2);font-size:14px;
}
.ms-toast-ic{color:var(--teal);font-size:15px;line-height:1;}
.ms-toast-text{flex:1;}

/* ---- grid thumbnails (v1.69.2) ----
   Quiet: the card's job is still the name and the score, and the diagram is a
   hint that this sound has one. Muted until hover so twelve of them do not
   shout at once. */
.ph-thumb{margin:-4px 0 6px;line-height:0;}
.ph-thumb svg{width:100%;max-width:150px;height:auto;opacity:.55;
  transition:opacity var(--dur) var(--ease);}
.card.clickable:hover .ph-thumb svg{opacity:1;}
/* The strike arc on a still tap: the sound IS a movement, so a frozen frame
   has to say so or it reads as a held sound. */
.ms-move{fill:none;stroke:var(--amber);stroke-width:2.2;stroke-linecap:round;}
.ms-move-head{fill:var(--amber);}
.ms-compact .ms-tongue{stroke-width:5;}
@media (max-width:640px){ .ph-thumb svg{max-width:120px;} }

/* ---- shadowing (v1.70.2) ----
   The ribbon is the cursor AND the score's raw material: the same word offsets
   drive the travelling highlight and the alignment picture below, so the thing
   the learner tracked is the thing they are judged against. */
.sh-stage{padding:22px;text-align:center;}
.sh-ribbon{
  font-family:var(--font-display);font-size:clamp(20px,3.4vw,30px);line-height:1.6;
  margin:6px 0 14px;color:var(--ink-3);
}
.sh-w{transition:color .12s linear, background .12s linear;padding:1px 3px;border-radius:5px;}
.sh-w.done{color:var(--ink-2);}
.sh-w.on{color:var(--ink);background:var(--teal-wash,rgba(12,155,138,.16));}
.sh-count{
  font-family:var(--font-display);font-weight:900;font-size:52px;line-height:1;
  color:var(--teal);height:56px;
}
.sh-controls{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:6px;}
/* A control that cannot act must not look like it can. Disabled is a real
   visual state here, not just a blocked click. */
.sh-controls button[disabled]{opacity:.45;cursor:not-allowed;filter:saturate(.5);}
/* And while it is preparing, it says so on the button itself rather than only
   in a small grey line underneath — which is where the first version put it. */
.sh-controls button.is-loading{position:relative;}
.sh-controls button.is-loading::after{
  content:"";position:absolute;left:0;right:0;bottom:0;height:2px;border-radius:2px;
  background:linear-gradient(90deg,transparent,var(--teal),transparent);
  animation:shLoad 1.1s linear infinite;
}
@keyframes shLoad{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
@media (prefers-reduced-motion: reduce){
  .sh-controls button.is-loading::after{animation:none;background:var(--teal);opacity:.5;}
}
.sh-hint{margin:12px 0 0;font-size:13.5px;color:var(--ink-3);}
.sh-hint.live{color:var(--teal);font-weight:600;}
.sh-hint.warn{color:var(--amber);}

/* The headphone check is a MEASUREMENT, so its result reads as a finding
   rather than a warning somebody can wave away. */
.sh-calib{padding:14px 16px;font-size:14px;line-height:1.55;border-left:3px solid var(--amber);}
.sh-calib.ok{border-left-color:var(--teal);}
.sh-calib .sh-ok{color:var(--teal);}
.sh-calib-row{display:flex;gap:10px;margin-top:10px;flex-wrap:wrap;}

.sh-res{padding:20px;}
.sh-res-head{display:flex;align-items:baseline;gap:14px;}
.sh-score{font-family:var(--font-display);font-weight:900;font-size:44px;line-height:1;color:var(--ink);}
.sh-score span{font-size:20px;color:var(--ink-3);}
.sh-score[data-band="good"]{color:var(--teal);}
.sh-score[data-band="mid"]{color:var(--amber);}
.sh-score[data-band="low"]{color:var(--red);}
.sh-res-basis{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.11em;
  text-transform:uppercase;color:var(--ink-3);}
.sh-parts{display:flex;gap:22px;margin:16px 0;flex-wrap:wrap;}
.sh-part{display:flex;flex-direction:column;gap:2px;}
.sh-part-l{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.11em;
  text-transform:uppercase;color:var(--ink-3);}
.sh-part-v{font-family:var(--font-display);font-weight:700;font-size:22px;color:var(--ink);}
.sh-part.off .sh-part-v{color:var(--ink-3);}
.sh-part-n{font-size:10.5px;color:var(--ink-3);}

/* TWO RIBBONS. The score is holistic by design; this is the explanation, and
   it shows "consistently half a beat late" in a way a number cannot. */
.sh-align{display:grid;grid-template-columns:auto 1fr;gap:6px 10px;align-items:center;margin:8px 0 14px;}
.sh-align-l{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.09em;
  text-transform:uppercase;color:var(--ink-3);}
.sh-track{position:relative;height:16px;background:var(--bg-2);border-radius:8px;overflow:hidden;}
.sh-blk{position:absolute;top:3px;height:10px;border-radius:5px;}
.sh-track.ref .sh-blk{background:var(--ink-3);}
.sh-track.me .sh-blk{background:var(--teal);}
.sh-res-why{font-size:14px;color:var(--ink-2);line-height:1.55;}


/* ---------------- CERTIFICATES panel (v1.78.2) ----------------
   Rows, not cards: this is a list of periods, and a learner will usually have
   one or two. Nothing here depends on colour alone — a revoked certificate
   carries the word, and a claimable one says "Earned". */
.cert-list{display:flex;flex-direction:column;gap:10px;margin-top:4px;}
.cert-list + .cert-list{margin-top:12px;}
.cert-list-muted{opacity:.82;}
.cert-row{
  display:flex;gap:16px;flex-wrap:wrap;align-items:flex-start;
  justify-content:space-between;
  padding:14px 16px;border:1px solid var(--line);border-radius:10px;
  background:var(--bg-2);
}
.cert-row-main{flex:1;min-width:220px;}
.cert-row-main > strong{font-size:15px;color:var(--ink);}
.cert-row-void{border-style:dashed;}
.cert-code-inline{
  font-family:'Space Mono',monospace;font-size:12px;letter-spacing:.05em;
  color:var(--ink-soft);margin-top:6px;overflow-wrap:anywhere;
}
.cert-need{margin:8px 0 0;padding-left:18px;font-size:13px;color:var(--ink-2);line-height:1.7;}
/* WHICH KIND OF CERTIFICATE THIS ROW IS (v1.93.2). A plan certificate and a
   curriculum level's certify different things, and a list showing only a
   subject line would make somebody guess which document they are about to
   print. In the mono face at label size, above the name, so it reads as a
   category rather than as part of the title — the same typographic separation
   the sidebar uses to keep the level and the streak apart. */
.cert-kind{
  display:block;font-family:'Space Mono',monospace;font-size:10px;
  letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);
  margin-bottom:3px;
}

/* THE VOCABULARY BUILDER (v1.85.0) — a taught word, not a drilled one.

   Three registers in each card, because they answer three different
   questions and a reader should be able to skim any one of them down the
   list: the TERM in the display face (what am I learning), the meaning in
   body text (what does it mean), the example italic and quoted (how is it
   used). The Practice button is last because it is what you do after
   reading, not instead of it. */
.vocab-wrap{margin-top:18px;border-top:1px solid var(--edge);padding-top:16px;}
.vocab-h{margin:0 0 4px;}
.vocab-list{list-style:none;margin:14px 0 0;padding:0;display:grid;gap:12px;}
.vocab-item{border:1px solid var(--edge);border-radius:var(--r-sm);
  background:var(--bg-2);padding:14px 16px;}
.vocab-term{font-family:var(--font-display);font-weight:800;font-size:20px;
  line-height:1.2;color:var(--ink);}
.vocab-meaning{margin-top:4px;font-size:14.5px;color:var(--ink-2);}
.vocab-eg{margin-top:8px;font-size:15px;font-style:italic;color:var(--ink);}
/* Its OWN class rather than a descendant rule on .btn: `btn` is a panel
   class this app borrows unstyled in two places, and styling it here would
   quietly define it for the whole learner app. tests/css-classes.js caught
   exactly that. */
.vocab-practice{margin-top:10px;}
@media (min-width:760px){ .vocab-list{grid-template-columns:1fr 1fr;} }

/* ============================================================
   MY VOCABULARY (v1.92.2)

   The saved list and the due session share one view and one visual language.
   Two rules run through all of it:

   NOTHING DEPENDS ON COLOUR ALONE. Every status pill carries its WORD — New,
   Learning, Mastered — and the colour is a second channel, never the only
   one. The same rule the printable report follows, for the same reason: a
   distinction nobody can read in greyscale is decoration.

   THE WORD IS THE SUBJECT. In a card the term is in the display face, its
   meaning in body text, the example sentence quoted — the three registers
   the Builder's own cards already use, so a word looks like itself whichever
   screen it is on.
   ============================================================ */
.vl-tabs{display:flex;gap:6px;margin:0 0 16px;border-bottom:1px solid var(--edge);}
.vl-tab{appearance:none;background:none;border:0;border-bottom:2px solid transparent;
  padding:9px 14px;margin-bottom:-1px;cursor:pointer;color:var(--ink-2);
  font-family:var(--font-body);font-size:14.5px;font-weight:600;
  transition:color var(--dur) var(--ease),border-color var(--dur) var(--ease);}
.vl-tab:hover{color:var(--ink);}
.vl-tab.on{color:var(--ink);border-bottom-color:var(--teal);}

.vl-summary{display:flex;flex-wrap:wrap;gap:16px;margin:0 0 14px;
  font-size:14px;color:var(--ink-2);font-family:var(--font-mono);}
.vl-summary strong{color:var(--ink);font-family:var(--font-body);}

.vl-list{list-style:none;margin:0;padding:0;display:grid;gap:12px;}
.vl-item{border:1px solid var(--edge);border-radius:var(--r-sm);
  background:var(--bg-2);padding:14px 16px;}
.vl-head{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;}
.vl-term{font-family:var(--font-display);font-weight:800;font-size:20px;
  line-height:1.2;color:var(--ink);}
.vl-term-lg{font-size:clamp(26px,5vw,34px);}
.vl-meaning{margin-top:4px;font-size:14.5px;color:var(--ink-2);}
.vl-eg{margin-top:8px;font-size:15px;font-style:italic;color:var(--ink);}
.vl-meta{margin-top:8px;font-size:13px;color:var(--ink-3);font-family:var(--font-mono);
  display:flex;flex-wrap:wrap;gap:6px;}
.vl-due{color:var(--ink-2);}
.vl-del{color:var(--red);}

/* The word is always there; the tint is the second channel, not the only one. */
.vl-pill{font-size:11.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
  padding:3px 8px;border-radius:999px;border:1px solid var(--edge-2);
  color:var(--ink-2);background:var(--bg-3);white-space:nowrap;}
.vl-new{color:var(--violet-d);border-color:var(--violet-dim);background:var(--violet-dim);}
.vl-learning{color:var(--ink-2);border-color:var(--amber-dim);background:var(--amber-dim);}
.vl-mastered{color:var(--teal-d);border-color:var(--teal-dim);background:var(--teal-dim);}

.vl-drill{padding:20px;}
.vl-progress{font-family:var(--font-mono);font-size:12.5px;color:var(--ink-3);
  letter-spacing:.04em;margin-bottom:10px;}
.vl-target{margin:14px 0 4px;font-size:clamp(18px,3.4vw,22px);line-height:1.4;}
.vl-skip-row{margin-top:14px;}

/* Reserved space, so the card does not jump when the verdict arrives. */
.vl-grade{min-height:0;font-size:14.5px;font-weight:600;
  transition:opacity var(--dur) var(--ease);opacity:0;}
.vl-grade.show{min-height:22px;margin:10px 0 2px;opacity:1;}
.vl-grade.hit{color:var(--teal-d);}
.vl-grade.miss{color:var(--ink-2);}

/* The Builder's Save button. `on` is the kept state, and it is a WORD on the
   button as well as a colour — the button reads "✓ Saved". */
.vocab-save{margin-top:10px;}
.vocab-save.on{color:var(--teal-d);border-color:var(--teal-dim);background:var(--teal-dim);}
.vocab-note{margin-top:6px;min-height:0;color:var(--red);}

/* The nav count. Rides the existing .tag pill so it inherits its shape, and is
   hidden entirely at zero rather than showing a 0 — a scheduler with nothing
   due should be quiet. */
.due-tag{background:var(--teal);color:#fff;border-color:var(--teal);}

@media (min-width:760px){ .vl-list{grid-template-columns:1fr 1fr;} }


/* ============================================================
   THE CURRICULUM MAP (v1.93.3) — "Your Path"

   A vertical list of level cards. Two registers per card, the separation the
   sidebar status cluster already uses: the level NUMBER in the mono face at
   label size reads as a readout, the level NAME in the display face reads as
   a title. Give both the same weight and the eye has to choose on every card.

   NOTHING DEPENDS ON COLOUR ALONE. Every state is a WORD in the pill, and the
   left rule is a second, redundant signal — solid for the level you are on,
   filled for one you finished, dashed for one placement skipped past. A card
   read in greyscale, or by a screen reader, loses nothing.
   ============================================================ */
.cur-summary{margin:0 0 16px;}
.cur-card{
  position:relative;
  border:1px solid var(--edge);border-radius:var(--r);
  background:var(--bg-3);padding:18px 20px 16px 22px;
  margin-bottom:14px;
}
/* The left rule. A second signal, never the only one. */
.cur-card::before{
  content:"";position:absolute;left:0;top:14px;bottom:14px;width:3px;
  border-radius:0 3px 3px 0;background:var(--edge-2);
}
.cur-current::before{background:var(--teal);}
.cur-complete::before{background:var(--teal-d);}
.cur-placed_past::before{background:repeating-linear-gradient(
  180deg,var(--edge-2) 0 4px,transparent 4px 8px);}
.cur-ahead::before{background:var(--edge);}
.cur-placed_past,.cur-ahead{background:var(--bg-2);}

.cur-head{display:flex;align-items:center;justify-content:space-between;gap:12px;}
.cur-num{
  font-family:var(--font-mono);font-size:10.5px;letter-spacing:.12em;
  text-transform:uppercase;color:var(--ink-3);
}
.cur-state{
  font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;
  text-transform:uppercase;padding:3px 9px;border-radius:999px;
  border:1px solid var(--edge-2);color:var(--ink-2);white-space:nowrap;
}
.cur-current .cur-state{background:var(--teal);border-color:var(--teal);color:#fff;}
.cur-complete .cur-state{background:var(--teal-dim);border-color:var(--teal);color:var(--teal-d);}
.cur-title{
  font-family:var(--font-display);font-size:22px;font-weight:600;
  color:var(--ink);margin:6px 0 0;
}
.cur-blurb{margin:4px 0 0;}
.cur-note{margin:10px 0 0;max-width:62ch;}

.cur-sounds{margin:14px 0 0;border-top:1px solid var(--edge);}
.cur-sound{
  display:flex;align-items:center;gap:12px;flex-wrap:wrap;
  padding:10px 0;border-bottom:1px solid var(--edge);
}
.cur-sym{
  font-family:var(--font-display);font-size:24px;color:var(--teal-d);
  min-width:34px;text-align:center;
}
.cur-sname{font-weight:600;color:var(--ink);min-width:130px;}
.cur-sprog{flex:1;min-width:180px;font-size:13px;color:var(--ink-2);}
.cur-ok{color:var(--teal-d);font-weight:600;}
/* WHICH MEASUREMENT EARNED IT, beside the tick — the score_source doctrine.
   Quiet, because it is a qualifier on the claim rather than the claim. */
.cur-ev{
  font-family:var(--font-mono);font-size:10px;letter-spacing:.08em;
  text-transform:uppercase;color:var(--ink-3);margin-left:8px;
}
.cur-n{font-variant-numeric:tabular-nums;}

.cur-applied{
  display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
  margin-top:12px;padding:10px 12px;border-radius:var(--r-sm);
  background:var(--bg-2);border:1px solid var(--edge);
}
.cur-applied.is-done{background:var(--teal-dim);border-color:var(--teal-dim);}
.cur-applied-k{
  font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--ink-3);
}
.cur-applied-v{font-weight:600;color:var(--ink);font-variant-numeric:tabular-nums;}
.cur-applied-note{flex-basis:100%;margin:2px 0 0;font-size:12.5px;}

/* A QUIET INLINE ACTION, and it is a <button> rather than an anchor because
   it acts on this page rather than navigating. `a.btn-link` is deliberately
   scoped to anchors — it exists for /api/export, which really does navigate —
   so putting that class on a button gave a native grey OS button with a
   default border, sitting in the middle of the card. Found by looking at the
   screen: tests/css-classes.js asks whether a class is DEFINED, not whether
   its selector matches the element wearing it. */
.cur-go{
  background:transparent;border:1px solid var(--edge-2);border-radius:99px;
  padding:6px 14px;font:inherit;font-size:13px;font-weight:600;
  color:var(--ink-2);cursor:pointer;white-space:nowrap;
  transition:border-color .16s,color .16s,background .16s;
}
.cur-go:hover{border-color:var(--teal);color:var(--ink);background:var(--bg-3);}

.cur-drills{margin-top:12px;}
.cur-cta{margin-top:12px;align-items:center;}
.cur-cta-note{font-size:12.5px;}

/* THE HOME LINE. One row, and it is a link: the question a position raises is
   "what is left", and a number with nowhere to go is a tease. */
.cur-line{
  display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;
  margin:18px 0 0;padding:12px 16px;
  border:1px solid var(--edge);border-radius:var(--r-sm);background:var(--bg-2);
}
.cur-line-k{
  font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--ink-3);
}
.cur-line-v{flex:1;min-width:200px;color:var(--ink);font-weight:600;}

/* WHERE THE PLACEMENT TEST PUTS YOU. The run's most useful output, and until
   v1.93.3 it was not on screen at all. */
.diag-place{
  margin:18px auto 0;max-width:520px;text-align:center;
  padding:16px 18px;border:1px solid var(--edge);border-radius:var(--r-sm);
  background:var(--bg-2);
}
.diag-place-k{
  font-family:var(--font-mono);font-size:10px;letter-spacing:.12em;
  text-transform:uppercase;color:var(--ink-3);
}
.diag-place-v{
  font-family:var(--font-display);font-size:21px;font-weight:600;
  color:var(--ink);margin-top:4px;
}
.diag-place-note{margin:8px 0 12px;}
/* A run that scored nothing. No ring, because there is no number — see
   renderDiagSummary on why a 0% would be a claim rather than a result. */
.diag-none{margin:10px 0 4px;text-align:center;}

/* THE WAY BACK TO THE PLACEMENT TEST (v1.99.38). The Home line's row, reused
   deliberately: this is a link to somewhere, not a card asking to be filled. */
.cur-retake{margin:0 0 18px;}
.cur-retake-note{flex-basis:100%;margin:2px 0 0;font-size:12.5px;}

@media (max-width:640px){
  .cur-card{padding:16px 16px 14px 18px;}
  .cur-sname{min-width:0;}
  .cur-sprog{min-width:0;flex-basis:100%;}
}

/* ============================================================
   GRADED WRITTEN EXERCISES — GRAMMAR (v1.94.3) AND READING (v1.95.3)

   Two surfaces per feature: the Lab's "what I built" card, and the saved list
   plus its exercise card. Nothing here depends on colour alone — every verdict
   and every status carries a word, and the colour is a second signal on top.

   THE `gr-` PREFIX IS SHARED, AND THAT IS DELIBERATE RATHER THAN LAZY. Reading
   Comprehension renders the same components — a status pill, a list row, an
   option button, a verdict box, a spoken-sentence card — because they ARE the
   same components: a graded written exercise looks the same whatever it is
   about. Copying this block under an `rd-` prefix would be seventy lines of
   duplicate rules, which is pattern 10's exact failure and would let the two
   features drift apart visually with nobody noticing. Read the prefix as
   "graded exercise" rather than "grammar".

   Reading adds `rd-` classes only for what has no grammar equivalent: the
   passage itself, and the length chooser. ============================================================ */

/* ---- Grammar Lab: what was built, never the questions ---- */
.gr-made{ border:1px solid var(--edge); border-radius:var(--r); padding:18px; background:var(--bg-2); }
.gr-made-h{ font-family:var(--font-display); font-size:22px; margin:0 0 6px; }
.gr-rule{ color:var(--ink-2); line-height:1.55; margin:0 0 10px; }
.gr-note{ margin-top:8px; min-height:1.2em; }
.gr-err{ color:var(--rec); }

/* ---- My Grammar: the topic list ---- */
.gr-list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:10px; }
.gr-row{ display:flex; align-items:flex-start; justify-content:space-between; gap:14px;
         border:1px solid var(--edge); border-radius:var(--r); padding:14px 16px; background:var(--bg-2); }
.gr-row-main{ min-width:0; }
.gr-row-title{ font-family:var(--font-display); font-size:17px; margin-bottom:2px; }
.gr-row-sub{ margin:0 0 4px; }
.gr-row-meta{ font-family:var(--font-mono); font-size:12px; color:var(--ink-3); }
.gr-row-side{ display:flex; align-items:center; gap:10px; flex-shrink:0; }

/* ---- the status pill. The WORD is the signal; colour is the second one. ---- */
.gr-pill{ display:inline-block; padding:3px 9px; border-radius:999px; font-size:12px;
          font-family:var(--font-mono); border:1px solid var(--edge); white-space:nowrap; }
.gr-new{ color:var(--ink-3); }
.gr-learning{ color:var(--amber); border-color:var(--amber-dim); }
.gr-cleared{ color:var(--teal); border-color:var(--teal-dim); }

/* ---- one topic ---- */
.gr-topic{ display:flex; flex-direction:column; gap:14px; }
.gr-head{ display:flex; align-items:flex-start; justify-content:space-between; gap:14px; }
.gr-title{ font-family:var(--font-display); font-size:20px; margin-bottom:4px; }
.gr-progress{ font-family:var(--font-mono); font-size:12px; color:var(--ink-3); }
.gr-item{ min-height:120px; }
.gr-nav{ flex-wrap:wrap; }

/* ---- the exercise card ---- */
.gr-q{ display:flex; flex-direction:column; gap:12px; }
.gr-prompt{ font-size:19px; line-height:1.5; }
.gr-opts{ display:flex; flex-direction:column; gap:8px; }
.gr-opt{ text-align:left; padding:11px 14px; border:1px solid var(--edge); border-radius:var(--r-sm);
         background:var(--bg-3); color:var(--ink); font:inherit; cursor:pointer;
         transition:border-color var(--dur) var(--ease), background var(--dur) var(--ease); }
.gr-opt:hover:not(:disabled){ border-color:var(--teal-dim); background:var(--bg-2); }
.gr-opt:disabled{ opacity:.6; cursor:default; }
.gr-blank{ gap:8px; }

/* ---- the verdict. A word AND a symbol, never a colour on its own. ---- */
.gr-verdict{ display:none; }
.gr-verdict.show{ display:flex; flex-direction:column; gap:6px; padding:12px 14px;
                  border-radius:var(--r-sm); border:1px solid var(--edge); background:var(--bg-3); }
.gr-verdict.gr-hit{ border-color:var(--teal-dim); }
.gr-verdict.gr-miss{ border-color:var(--amber-dim); }
.gr-verdict.gr-idle{ border-color:var(--edge-2); }
.gr-mark{ font-family:var(--font-mono); font-size:14px; }
.gr-hit .gr-mark{ color:var(--teal); }
.gr-miss .gr-mark{ color:var(--amber); }
.gr-accepted{ font-size:14px; color:var(--ink-2); }
.gr-why{ font-size:14px; color:var(--ink-2); line-height:1.5; }
.gr-again-note{ font-size:12px; color:var(--ink-3); font-family:var(--font-mono); }
.gr-after{ margin-top:4px; }

/* ---- a spoken sentence ---- */
.gr-say{ display:flex; flex-direction:column; gap:10px; }
.gr-say-lbl{ font-family:var(--font-mono); font-size:12px; }
.gr-target{ margin:0; }
.gr-said{ font-family:var(--font-mono); font-size:13px; color:var(--ink-2); }

@media (max-width:640px){
  .gr-row{ flex-direction:column; align-items:stretch; }
  .gr-row-side{ justify-content:space-between; }
  .gr-head{ flex-direction:column; }
}

/* ---- READING ONLY (v1.95.3): the passage, and how long to make it ----

   THE PASSAGE IS THE ONE THING THIS FEATURE HAS THAT GRAMMAR DOES NOT, so it is
   the one thing with its own rules. It is prose to be READ, not a prompt to be
   answered: a measure near 68 characters, real line height, and paragraphs with
   space between them — the passage arrives with its blank lines preserved
   (bodyField keeps newlines where every other field collapses them) and this is
   what makes that visible. */
.rd-passage{ border:1px solid var(--edge); border-radius:var(--r); padding:18px 20px;
             background:var(--bg-2); }
.rd-body{ max-width:68ch; font-size:17px; line-height:1.7; color:var(--ink); }
.rd-body p{ margin:0 0 0.9em; }
.rd-body p:last-child{ margin-bottom:0; }
.rd-meta{ font-family:var(--font-mono); font-size:12px; color:var(--ink-3); margin-top:10px; }
/* The passage stays on screen while the questions are answered — reading
   comprehension with the text hidden is a memory test. */
.rd-split{ display:flex; flex-direction:column; gap:14px; }
.rd-len{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.rd-len-lbl{ font-family:var(--font-mono); font-size:12px; color:var(--ink-3); }
/* The read-aloud half, which does NOT decide whether a passage is understood.
   Muted rather than absent: it is real practice and it is written down as such,
   but it must not read as a second requirement. */
.rd-aside{ border-left:2px solid var(--edge-2); padding-left:12px; }

/* ============================================================
   LISTENING COMPREHENSION (v1.97.3)

   Almost nothing. The list, the pills, the option buttons and the verdict box
   are `gr-`; the transcript is `rd-passage` / `rd-body`, because a transcript
   revealed after the questions is prose to be READ and is exactly the shape a
   passage is. `ln-` exists only for the one thing this feature has that nothing
   else does: a player.
   ============================================================ */
.ln-player{ display:flex; flex-direction:column; gap:10px;
            border:1px solid var(--edge); border-radius:var(--r);
            padding:14px 16px; background:var(--bg-2); }
.ln-play{ min-width:9.5em; }
/* A bar, not a spinner: it says HOW FAR THROUGH, which is the one thing a
   learner wants to know while audio plays and a spinner never says.

   THE TRACK IS --edge, NOT --edge-2, AND THAT WAS A REAL BUG FOUND BY LOOKING
   AT IT. --edge-2 is the "stronger border" tone, and at zero per cent the fill
   is invisible — so the only thing on screen was a solid full-width tan line,
   which reads as a COMPLETED bar rather than an empty one. A learner glancing
   at an untouched set would have been told it was finished. It is the same
   class as the three v1.96.5/v1.96.6 bugs: a style whose meaning is inverted.
   The track is now the hairline tone, well below the fill's contrast, so empty
   reads as empty. */
.ln-bar{ height:6px; border-radius:999px; background:var(--edge); overflow:hidden; }
.ln-bar-fill{ height:100%; background:var(--teal); border-radius:999px;
              transition:width .25s ease; min-width:0; }
.ln-state{ font-family:var(--font-mono); font-size:12px; }
.ln-now{ color:var(--ink-2); }
/* NEVER COLOUR ALONE — the failure text names the utterance and says that
   nothing after it played. The colour is the second signal, not the first. */
.ln-fail{ color:var(--red); }
.ln-voice{ font-size:12px; color:var(--ink-3); }
.ln-hint{ font-size:12px; color:var(--ink-3); }
/* WHAT IS BEING WITHHELD, AND WHY. A screen that simply had no transcript would
   read as a missing feature; this is the sentence that makes it a rule. Muted
   and bordered rather than warned — nothing has gone wrong. */
.ln-sealed{ border-left:2px solid var(--edge-2); padding-left:12px; color:var(--ink-3); }
.ln-script{ border:1px solid var(--edge); border-radius:var(--r); padding:10px 12px; }
.ln-script-h{ cursor:pointer; font-weight:600; color:var(--teal); font-size:14px; }
/* The speaker label, which is DISPLAYED and never spoken. Distinct from the
   words so a reader can see whose line it is, and deliberately not a colour
   that means anything else in this app. */
.ln-who{ font-family:var(--font-mono); font-size:12px; color:var(--ink-3);
         margin-right:6px; }
/* The shelf. Set apart from the learner's own list so "yours" and "ready-made"
   do not read as one pile, and deliberately NOT styled as a promotion: it is
   the free tier's way in, not an upsell. */
.ln-shelf{ margin-top:26px; border-top:1px solid var(--edge); padding-top:18px; }
.ln-shelf-h{ font-size:19px; margin:0 0 4px; }
.ln-shelf-note{ margin:0 0 12px; }
/* ============================================================
   TOEFL PRACTICE (v1.97.8)

   The lists, pills and cards are `gr-`; the stimulus passage is `rd-passage`
   and the transcript block `ln-script`, both borrowed whole; the coach's report
   is `rep-`, which the Conversation Language Report and Writing Practice
   already render — a third copy of the before/after fix layout would be three
   things to fix. `tf-` covers the four things nothing else here has: a clock, a
   section stepper, a live recording line, and a result list that is
   deliberately NOT a grid.
   ============================================================ */
.tf-note{ border-left:2px solid var(--edge-2); padding-left:12px; margin-bottom:16px; }
.tf-checked{ margin-top:18px; font-size:12px; color:var(--ink-3); }

/* The stepper: where you are in a section, as words and position rather than
   as a bar — a bar implies a proportion, and a section is a sequence. */
.tf-steps{ list-style:none; margin:0 0 14px; padding:0; display:flex; flex-wrap:wrap; gap:8px; }
.tf-step{ display:flex; align-items:center; gap:7px; border:1px solid var(--edge);
          border-radius:999px; padding:5px 12px 5px 6px; font-size:13px; color:var(--ink-3); }
.tf-step-n{ display:inline-flex; align-items:center; justify-content:center;
            width:20px; height:20px; border-radius:50%; background:var(--edge);
            font-family:var(--font-mono); font-size:11px; color:var(--ink-2); }
.tf-now{ border-color:var(--teal); color:var(--ink); }
.tf-now .tf-step-n{ background:var(--teal); color:#fff; }
.tf-done{ color:var(--ink-2); }
.tf-done .tf-step-n{ background:var(--ink-3); color:#fff; }
.tf-step-lbl{ }
.tf-step-s{ font-family:var(--font-mono); font-size:11px; color:var(--ink-3); }
.tf-takes{ display:flex; flex-direction:column; gap:10px; }

.tf-task{ border:1px solid var(--edge); border-radius:var(--r); padding:16px 18px;
          background:var(--bg-2); display:flex; flex-direction:column; gap:12px; }
.tf-task-head{ display:flex; align-items:baseline; justify-content:space-between; gap:12px;
               flex-wrap:wrap; }
.tf-task-l{ font-weight:600; font-size:17px; }
.tf-clock-wrap{ display:flex; align-items:baseline; gap:10px; }
/* THE PHASE IS A WORD BESIDE THE NUMBER, always — "Preparing", "Speak now",
   "Time". A bare countdown says how long is left and never what it is timing,
   and the last ten seconds must not be signalled by colour alone. */
.tf-phase{ font-family:var(--font-mono); font-size:12px; color:var(--ink-3); }
.tf-clock{ font-family:var(--font-mono); font-size:22px; font-variant-numeric:tabular-nums;
           color:var(--ink); }
.tf-clock-low{ color:var(--rec); }
.tf-ranout{ border-left:2px solid var(--edge-2); padding-left:12px; margin-top:10px; }

.tf-stim{ display:flex; flex-direction:column; gap:12px; }
.tf-stim-empty{ border-left:2px solid var(--edge-2); padding-left:12px; }
/* The question an independent task asks. It is the whole task, so it is set
   like a prompt rather than like body text. */
.tf-topic{ font-size:19px; line-height:1.55; max-width:60ch; border-left:3px solid var(--teal);
           padding-left:14px; }
.tf-lecture{ display:flex; flex-direction:column; gap:8px; }
/* "Give me a different one" (v1.99.42). Secondary by construction: the task in
   front of the learner is complete and this only swaps it, so it sits under the
   material rather than competing with the control that starts the clock. */
.tf-swap{ margin-top:12px; align-items:center; flex-wrap:wrap; }
.tf-swap-note{ font-size:12.5px; }

.tf-speak, .tf-handoff{ display:flex; flex-direction:column; gap:10px; }
/* The live line while recording. A dot and the words being heard — the same
   reassurance Free Talk gives, which is that something is happening at all. */
.tf-live{ display:flex; align-items:flex-start; gap:8px; border:1px solid var(--rec);
          border-radius:var(--r); padding:10px 12px; font-size:15px; line-height:1.5; }
.tf-live-dot{ width:9px; height:9px; border-radius:50%; background:var(--rec);
              margin-top:6px; flex:0 0 auto; }
.tf-interim{ color:var(--ink-3); }

.tf-take{ border-top:1px solid var(--edge); padding-top:12px; margin-top:6px; }
.tf-take-h{ font-family:var(--font-mono); font-size:12px; }
.tf-take-t{ font-size:16px; line-height:1.6; margin:6px 0 10px; max-width:68ch; }
.tf-reports{ display:flex; flex-direction:column; gap:12px; }
.tf-pick{ margin-top:10px; }

/* THE RESULT LIST IS A LIST AND NEVER A GRID, and that is the whole design.
   Four honest numbers in a two-by-two ARE a score report, so these stack, each
   with its own sentence, and nothing sits beside anything for comparison. */
.tf-summary{ border-top:1px solid var(--edge); margin-top:16px; padding-top:14px; }
.tf-summary-h{ font-size:17px; margin:0 0 10px; }
.tf-res-list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:14px; }
.tf-res{ border-left:2px solid var(--edge-2); padding-left:12px; }
.tf-res-l{ font-family:var(--font-mono); font-size:12px; color:var(--ink-3); }
.tf-res-v{ font-size:18px; color:var(--ink); }
.tf-res-n{ margin-top:2px; }
.tf-disclaimer{ margin-top:16px; border-left:2px solid var(--edge-2); padding-left:12px; }

/* WHAT YOU ANSWERED (v1.99.3) — the takes and reports a finished sitting used
   to carry down the wire and never draw. It sits BELOW the summary and is
   styled like it rather than like a task: this is a record being read back,
   not an exercise being worked, so it borrows .tf-summary's rule above and
   .tf-answer-l takes .tf-res-l's mono label. Each task is stacked, never
   ranged alongside another — the result list's reason, one block along. */
.tf-answers{ border-top:1px solid var(--edge); margin-top:16px; padding-top:14px; }
.tf-answers-h{ font-size:17px; margin:0 0 10px; }
.tf-answer{ margin-bottom:18px; }
.tf-answer-l{ font-family:var(--font-mono); font-size:12px; color:var(--ink-3); margin-bottom:6px; }
/* v1.99.4 — a finished reading or listening task, reopened read-only in place.
   It nests a whole exercise card, so it gets room above it and nothing else:
   the card brings its own frame. */
.tf-review{ margin-top:10px; }
/* NOT A MISS, AND NOT COLOURED LIKE ONE. "You did not answer this" is a fact
   about the clock running out, not a wrong answer, and painting it with
   .gr-miss's red would tell somebody they got something wrong that they were
   never asked. */
.tf-unanswered{ color:var(--ink-3); font-style:italic; }

/* The Lab's summary chips: what was made, never what it says. */
.ln-made-meta{ display:flex; gap:8px; flex-wrap:wrap; margin:4px 0 10px; }
.ln-chip{ font-family:var(--font-mono); font-size:12px; color:var(--ink-2);
          border:1px solid var(--edge); border-radius:999px; padding:3px 10px; }

/* ============================================================
   WRITING PRACTICE (v1.96.4)

   The list, the pills and the cards are `gr-` and the report is `rep-` — both
   already render exactly what this feature needs, and a second copy of either
   would be two things to fix. `wr-` exists only for what has no equivalent
   anywhere else in the app: an editor a learner types into, the live count
   beside it, and the sentence a rewrite is handed.
   ============================================================ */
.wr-brief{ max-width:68ch; font-size:16px; line-height:1.6; color:var(--ink); margin:0 0 4px; }

/* The flawed sentence a rewrite is given. Set apart from the instruction
   because it is the MATERIAL rather than the task — and deliberately not styled
   like a quote from the learner, which is what .rep-said means. */
.wr-source{ border:1px solid var(--edge); border-left:3px solid var(--amber);
            border-radius:var(--r-sm); padding:12px 14px; background:var(--bg-2); }
.wr-source-lbl{ font-family:var(--font-mono); font-size:12px; color:var(--ink-3);
                text-transform:uppercase; letter-spacing:.06em; }
.wr-source-text{ margin:6px 0 0; font-size:17px; line-height:1.5; color:var(--ink); }

/* Said before the first keystroke, not after the work — the v1.70.5 rule. */
.wr-gate{ border-left:2px solid var(--edge-2); padding-left:12px; }

.wr-editor{ display:grid; gap:8px; }
.wr-text{ width:100%; min-height:220px; resize:vertical; font-family:var(--font-body);
          font-size:16px; line-height:1.7; padding:14px 16px; color:var(--ink);
          background:var(--bg-2); border:1px solid var(--edge); border-radius:var(--r-sm); }
.wr-text:focus{ outline:2px solid var(--cyan); outline-offset:1px; }
.wr-editor-foot{ display:flex; gap:12px; flex-wrap:wrap; align-items:baseline;
                 justify-content:space-between; }
.wr-count{ font-family:var(--font-mono); font-size:12px; color:var(--ink-3); }

/* THE LIMIT IS VISIBLE ON THE WAY TO IT, not sprung at the end. Colour is the
   decoration; .wr-why beside it carries the same fact in words, because a
   refusal a learner cannot read is the same as no refusal at all. */
.wr-count.wr-over{ color:var(--red); }
.wr-why{ color:var(--amber); }

.wr-history{ display:grid; gap:14px; margin-top:14px; }
.wr-piece{ display:grid; gap:10px; }
.wr-piece-body{ max-width:68ch; font-size:16px; line-height:1.7; color:var(--ink-2); }
.wr-piece-body p{ margin:0 0 0.9em; }
.wr-piece-body p:last-child{ margin-bottom:0; }

/* A report sits under the piece it is about, indented so two of them on one
   piece read as two readings of the same thing rather than as a thread. */
.wr-report{ border-left:2px solid var(--edge-2); padding-left:14px; }

.wr-kind{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }

/* The step after the rule, on a grammar topic (v1.96.7). Set apart from the
   exercises above it and deliberately quieter than them: it changes nothing
   about the topic or the level, and a block styled like the graded half would
   say otherwise before the copy got a chance to. */
.gr-writing{ display:grid; gap:8px; margin-top:16px; padding:14px 16px;
             border:1px solid var(--edge); border-radius:var(--r-sm); background:var(--bg-2); }
.gr-writing-h{ font-family:var(--font-mono); font-size:12px; color:var(--ink-3);
               text-transform:uppercase; letter-spacing:.06em; }

/* Reading a corrected sentence aloud (v1.96.8). The one place Writing Practice
   reaches the attempts table, so the control sits INSIDE the correction it
   belongs to rather than under the card: the sentence is the drill target, and
   a button further away would have to say which one it meant. */
.wr-say-btn{ margin-top:8px; }
.wr-say-host:empty{ display:none; }
.wr-say-host{ margin-top:10px; }

/* A quote that is EVIDENCE OF SOMETHING DONE RIGHT, and therefore deliberately
   not .rep-said — that one is line-through and means "the wrong thing you
   wrote". Citing a learner's successful use of a pattern in it renders their
   success as a crossed out error, which is what shipped in v1.96.4 and what
   looking at the screen caught. */
.wr-quote{ border-left:2px solid var(--teal); padding-left:12px; margin:6px 0;
           font-size:16px; line-height:1.6; color:var(--ink); }

/* The example rewrite, shown only after something has been handed in. Teal
   rather than the amber of .wr-source: the sentence to FIX is a problem, this
   is a way out of it, and they must not read as the same kind of thing. */
.wr-ref{ border-left-color:var(--teal); }
.wr-ref-note{ margin:8px 0 0; }
.wr-ref-held{ border-left:2px solid var(--edge-2); padding-left:12px; }

/* The optional strands on a level card (v1.94.5, v1.95.6).

   ONE BLOCK, TWO ROWS, ONE DISCLAIMER. With a single strand the note sat on the
   row; with two it would be said twice, which reads as a hedge, and a card
   carrying sounds + connected speech + grammar + reading no longer says which
   two decide anything. So the strands are grouped under a heading that says it
   once, and the rows underneath carry only their counts. */
.cur-extra{ display:grid; gap:8px; padding:12px 14px; border:1px solid var(--edge);
            border-radius:var(--r-sm); background:var(--bg-2); }
.cur-extra-h{ font-family:var(--font-mono); font-size:12px; color:var(--ink-3);
              text-transform:uppercase; letter-spacing:.06em; }
.cur-strand{ display:grid; gap:4px; }
.cur-strand.is-done .cur-applied-v{ color:var(--teal); }

/* .cur-grammar is GONE, not kept. It was the single-strand shape .cur-extra
   replaced, and nothing emits it any more — a rule matching no element is dead
   weight that makes the stylesheet lie about what the app renders.
   tests/css-classes.js cannot catch this direction: it asks whether a class
   USED is DEFINED, never whether a class defined is used. Checked by grep
   before deleting, because that asymmetry means the answer is not free. */
.cur-gtopics{ margin-top:6px; flex-wrap:wrap; }
