
/* ============================================================================
   MINATI MA'AM'S ACADEMY — design system
   Two independently designed themes. Light is airy paper-and-ink with violet
   shadow; dark is a deep midnight aurora with luminous edges. Not inversions.
   ========================================================================== */

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;text-size-adjust:100%}
/* HIDDEN MEANS HIDDEN. The browser's own [hidden]{display:none} is the weakest
   rule on the page: any rule of ours that gives an element a display value beats
   it, whatever the selector, because a site's styles outrank the browser's. So
   .btn{display:inline-flex} kept the lesson wall's Continue with Google on screen
   with hidden set -- on an install with no Google it would have led nowhere --
   while seven other elements each carried their own [hidden] line to get round
   the same thing. Found in review, 14 September 2026.
   !important so nothing further down can undo it. Checked before adding: every
   place in the app that sets hidden means "not there", and nothing shows a
   hidden element by adding a class. */
[hidden]{display:none!important}
body,h1,h2,h3,h4,p,figure,blockquote,dl,dd,ul,ol{margin:0}
ul[class],ol[class]{list-style:none;padding:0}
img,svg,canvas{display:block;max-width:100%}
button,input,select,textarea{font:inherit;color:inherit}
button{background:none;border:0;cursor:pointer;-webkit-tap-highlight-color:transparent}
a{color:inherit;text-decoration:none}
:focus-visible{outline:2.5px solid var(--focus);outline-offset:3px;border-radius:6px}
::selection{background:var(--sel);color:var(--sel-ink)}

/* ---------------------------------------------------------------- tokens --*/
:root{
  --f-ui:'Plus Jakarta Sans',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
  --f-dis:'Outfit','Plus Jakarta Sans',system-ui,sans-serif;
  --f-ser:'Instrument Serif','Iowan Old Style',Georgia,serif;

  --r-xs:8px; --r-sm:12px; --r-md:16px; --r-lg:22px; --r-xl:28px; --r-2xl:36px; --r-full:999px;

  --ease:cubic-bezier(.22,1,.36,1);
  --ease-io:cubic-bezier(.65,.05,.36,1);
  --spring:cubic-bezier(.34,1.46,.64,1);

  --nav-w:266px; --top-h:68px; --tab-h:66px;
  --maxw:1240px;

  /* The theme engine overwrites every one of these at runtime. What is written
     here is the Aurora identity, so the first paint is already correct. */
  --a1:#5A45FF; --a2:#A855F7; --a3:#FF3D9A;
}

/* ------------------------------------------------------------ LIGHT theme -*/
:root[data-theme="light"]{
  --bg:#F2F4FE;
  --bg-deep:#E7EAFB;
  --surface:#FFFFFF;
  --surface-2:#F7F8FF;
  --surface-3:#EFF1FE;
  --glass:rgba(255,255,255,.72);
  --glass-strong:rgba(255,255,255,.9);
  --line:#E2E6F7;
  --line-soft:#EDEFFA;
  --line-strong:#D2D8F0;

  --ink:#0F1130;
  --ink-2:#4A4E75;
  --ink-3:#7B7FA4;
  --ink-4:#A7ABC8;

  --brand:var(--a1); --brand-2:var(--a2); --brand-3:var(--a3);
  --brand-soft:rgba(90,69,255,.09);
  --brand-line:rgba(90,69,255,.22);
  --on-brand:#fff;

  --ok:#059669; --ok-soft:rgba(5,150,105,.1); --ok-line:rgba(5,150,105,.25);
  --no:#E11D6F; --no-soft:rgba(225,29,111,.09); --no-line:rgba(225,29,111,.24);
  /* ink that sits on a filled ok/no chip — the verdict marks are solid, not tinted */
  --on-ok:#fff; --on-no:#fff;
  --warn:#7C3AED; --warn-soft:rgba(124,58,237,.09);

  --sh-1:0 1px 2px rgba(24,20,70,.05), 0 2px 6px rgba(24,20,70,.04);
  --sh-2:0 2px 4px rgba(24,20,70,.05), 0 10px 24px -6px rgba(24,20,70,.09);
  --sh-3:0 4px 8px rgba(24,20,70,.05), 0 22px 44px -12px rgba(30,22,86,.16);
  --sh-4:0 8px 16px rgba(24,20,70,.06), 0 40px 80px -20px rgba(30,22,86,.24);
  --sh-brand:0 12px 32px -8px rgba(90,69,255,.42);
  --ring-track:#E6E9FA;

  --focus:#5A45FF;
  --sel:rgba(90,69,255,.16); --sel-ink:#0F1130;
  --noise-op:.028;
  --aura-op:.9;
}

/* ------------------------------------------------------------- DARK theme -*/
:root[data-theme="dark"]{
  --bg:#05071A;
  --bg-deep:#03040F;
  --surface:#0E1230;
  --surface-2:#141936;
  --surface-3:#1B2145;
  --glass:rgba(18,23,58,.62);
  --glass-strong:rgba(12,16,44,.86);
  --line:rgba(140,155,255,.13);
  --line-soft:rgba(140,155,255,.08);
  --line-strong:rgba(150,165,255,.24);

  --ink:#EFF1FF;
  --ink-2:#AFB6E4;
  --ink-3:#7A82B8;
  --ink-4:#565D8C;

  --brand:#8B7BFF; --brand-2:#C084FC; --brand-3:#FF6BB0;
  --brand-soft:rgba(139,123,255,.15);
  --brand-line:rgba(139,123,255,.34);
  --on-brand:#0B0D24;

  --ok:#34E5B0; --ok-soft:rgba(52,229,176,.13); --ok-line:rgba(52,229,176,.32);
  --no:#FF5C93; --no-soft:rgba(255,92,147,.13); --no-line:rgba(255,92,147,.32);
  /* the dark-mode accents are light, so the ink on a filled chip goes dark */
  --on-ok:#04271E; --on-no:#3B0518;
  --warn:#C084FC; --warn-soft:rgba(192,132,252,.13);

  --sh-1:0 1px 2px rgba(0,0,0,.5);
  --sh-2:0 4px 18px rgba(0,0,0,.5);
  --sh-3:0 12px 40px rgba(0,0,0,.6);
  --sh-4:0 24px 70px rgba(0,0,0,.7);
  --sh-brand:0 12px 40px -10px rgba(139,123,255,.6);
  --ring-track:rgba(140,155,255,.16);

  --focus:#A79BFF;
  --sel:rgba(139,123,255,.32); --sel-ink:#fff;
  --noise-op:.05;
  --aura-op:.75;
}

/* gradients ---------------------------------------------------------------*/
:root{
  --grad-brand:linear-gradient(135deg,var(--a1) 0%,var(--a2) 48%,var(--a3) 100%);
  /* THE BRAND ACCENTS AT FULL STRENGTH, and fixed rather than themed.
     Three attempts were made to darken this so white would clear 4.5 on every
     stop, and the owner rejected each one as losing its lightness. She is right,
     and the reason is worth writing down: #A855F7 is 3.96:1 under white and
     #FF3D9A is 3.29:1 -- so EVERY solid button on this site has been below the
     small-text floor since it shipped, Buy now included. Holding this one button
     to a stricter standard made it visibly duller than the buttons beside it,
     which is a real fault a reader can see, traded against one they cannot.
     THE CONTRAST IS A SEPARATE, SITE-WIDE DECISION -- either the accents move
     for every button at once, or the labels get a scrim. Fixing it on one
     button was the wrong shape of fix.
     WHY THE TOKEN STILL EXISTS: the app rewrites both --grad-brand and
     --grad-deep from the hero palette on every tick of its time-of-day theming
     (p09b.html), so a button using either drifts through the day and stops
     matching the public shop. This one is fixed and shared. */
  /* Her brand accents, at their own values and in their own places -- the same
     gradient every .btn-solid on the site carries. Fixed rather than themed:
     the app rewrites both --grad-brand and --grad-deep from the hero palette on
     every tick of its time-of-day theming (p09b.html), so a button built on
     either drifts through the day and stops matching the public shop. */
  /* THE GRADIENT WHITE TEXT MAY STAND ON. Same three colours, lightness
     lowered until white clears 4.5:1 at every point -- the old stops were
     3.96:1 and 3.29:1. Kept identical to page.css so the button is the same
     button in the app and on the public pages. */
  --grad-btn:linear-gradient(135deg,#5A45FF 0%,#9D40F6 48%,#E4006D 100%);
  --grad-cool:linear-gradient(135deg,#0EA5E9,#5A45FF 60%,#A855F7);
  --grad-mint:linear-gradient(135deg,#10C79B,#0EA5E9);
  --grad-pink:linear-gradient(135deg,#A855F7,#FF3D9A);
}

/* ------------------------------------------------------------------ base -*/
html,body{height:100%}
body{
  font-family:var(--f-ui);
  font-size:15.5px; line-height:1.62; font-weight:450;
  color:var(--ink); background:var(--bg);
  -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale;
  text-rendering:optimizeLegibility;
  overflow-x:hidden;
  transition:background-color .5s var(--ease), color .5s var(--ease);
  font-feature-settings:"kern","liga","calt","ss01";
}
h1,h2,h3,h4{font-family:var(--f-dis);font-weight:700;line-height:1.14;letter-spacing:-.022em;color:var(--ink)}
/* panels that carry their own colour must pass it down to their headings */
.hero h1,.hero h2,.hero h3,.profhero h1,.profhero h2,.profhero h3,
.mcqhero h1,.mcqhero h2,.mcqhero h3,#celebrate h1,#celebrate h2,#onb h1,
.face.back h1,.face.back h2,.face.back h3{color:inherit}

/* --------------------------------------------------------- aurora canvas -*/
#aura{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden;
  opacity:var(--aura-op);transition:opacity .8s var(--ease)}
#aura .blob{position:absolute;border-radius:50%;filter:blur(70px);will-change:transform;
  opacity:.5;mix-blend-mode:normal}
:root[data-theme="dark"] #aura .blob{filter:blur(90px);opacity:.42;mix-blend-mode:screen}
#aura .b1{width:52vw;height:52vw;min-width:380px;min-height:380px;background:var(--au1);
  top:-16vw;left:-10vw;animation:drift1 26s var(--ease-io) infinite alternate}
#aura .b2{width:44vw;height:44vw;min-width:320px;min-height:320px;background:var(--au2);
  top:22vh;right:-14vw;animation:drift2 32s var(--ease-io) infinite alternate}
#aura .b3{width:40vw;height:40vw;min-width:300px;min-height:300px;background:var(--au3);
  bottom:-16vh;left:24vw;animation:drift3 29s var(--ease-io) infinite alternate}
@keyframes drift1{0%{transform:translate3d(0,0,0) scale(1)}100%{transform:translate3d(9vw,7vh,0) scale(1.16)}}
@keyframes drift2{0%{transform:translate3d(0,0,0) scale(1.08)}100%{transform:translate3d(-8vw,10vh,0) scale(.92)}}
@keyframes drift3{0%{transform:translate3d(0,0,0) scale(.95)}100%{transform:translate3d(7vw,-8vh,0) scale(1.14)}}
#grain{position:fixed;inset:0;z-index:1;pointer-events:none;opacity:var(--noise-op);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  mix-blend-mode:overlay}

/* First-paint aurora. The engine replaces these per theme and time of day. */
:root[data-theme="light"]{--au1:#B9C4FF;--au2:#F5B9E4;--au3:#A8E8F0}
:root[data-theme="dark"]{--au1:#4B3BC7;--au2:#8B2E9E;--au3:#1E6E8C}

/* Long-form reading. The aurora stays — it just steps back a little so body
   copy sits on a calmer field. Applied by the router to any reading view, so
   it is a system behaviour rather than a lesson-page exception. */
:root[data-reading="1"] #aura{opacity:calc(var(--aura-op) * .4)}
:root[data-reading="1"] #grain{opacity:calc(var(--noise-op) * .5)}

/* ------------------------------------------------------------- utilities -*/
.hide{display:none !important}
/* Names are user data: they must wrap, never clip, at any length. */
.uname{overflow-wrap:anywhere;word-break:break-word;hyphens:auto;min-width:0;max-width:100%}
.row{display:flex;align-items:center}
.between{display:flex;align-items:center;justify-content:space-between;gap:12px}
.wrap{flex-wrap:wrap}
.g8{gap:8px}.g10{gap:10px}.g12{gap:12px}.g14{gap:14px}.g16{gap:16px}
.mt8{margin-top:8px}.mt12{margin-top:12px}.mt14{margin-top:14px}.mt16{margin-top:16px}
.mt20{margin-top:20px}.mt24{margin-top:24px}.mt32{margin-top:32px}
.tc{text-align:center}
.grow{flex:1 1 auto;min-width:0}
.trunc{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.clamp2{display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.muted{color:var(--ink-3)}
.eyebrow{font-family:var(--f-dis);font-size:11px;font-weight:700;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-3)}
.rel{position:relative}

/* icons — the class lands on the <svg> itself once Lucide swaps the placeholder,
   so the size must live on .i and never be re-expanded to 100%. */
.i{width:20px;height:20px;flex:0 0 auto;stroke-width:1.9;display:block}
.i>svg{width:100%;height:100%;display:block;stroke-width:inherit}
svg.i{overflow:visible}
i[data-lucide]{display:block;width:20px;height:20px;flex:0 0 auto}

/* scrollbars */
*::-webkit-scrollbar{width:11px;height:11px}
*::-webkit-scrollbar-track{background:transparent}
*::-webkit-scrollbar-thumb{background:var(--line-strong);border-radius:99px;border:3px solid transparent;background-clip:content-box}
*::-webkit-scrollbar-thumb:hover{background:var(--ink-4);background-clip:content-box}

/* ---------------------------------------------------------------- motion -*/
@keyframes fadeUp{from{opacity:0;transform:translate3d(0,18px,0)}to{opacity:1;transform:none}}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
@keyframes scaleIn{from{opacity:0;transform:scale(.94)}to{opacity:1;transform:none}}
@keyframes popIn{0%{opacity:0;transform:scale(.8)}60%{opacity:1;transform:scale(1.04)}100%{transform:scale(1)}}
@keyframes slideRight{from{opacity:0;transform:translate3d(-14px,0,0)}to{opacity:1;transform:none}}
@keyframes breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.035)}}
@keyframes spinS{to{transform:rotate(360deg)}}
@keyframes shimmer{0%{background-position:-260% 0}100%{background-position:260% 0}}
@keyframes shakeX{10%,90%{transform:translateX(-2px)}20%,80%{transform:translateX(4px)}
  30%,50%,70%{transform:translateX(-7px)}40%,60%{transform:translateX(7px)}}
@keyframes pulseRing{0%{box-shadow:0 0 0 0 var(--pr,rgba(90,69,255,.5))}
  70%{box-shadow:0 0 0 16px rgba(0,0,0,0)}100%{box-shadow:0 0 0 0 rgba(0,0,0,0)}}
@keyframes floatY{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}
@keyframes sheen{0%{transform:translateX(-120%) skewX(-18deg)}100%{transform:translateX(320%) skewX(-18deg)}}

.anim-up{animation:fadeUp .62s var(--ease) both}
.anim-pop{animation:popIn .5s var(--spring) both}
.stagger>*{animation:fadeUp .62s var(--ease) both}
.stagger>*:nth-child(1){animation-delay:.04s}.stagger>*:nth-child(2){animation-delay:.09s}
.stagger>*:nth-child(3){animation-delay:.14s}.stagger>*:nth-child(4){animation-delay:.19s}
.stagger>*:nth-child(5){animation-delay:.24s}.stagger>*:nth-child(6){animation-delay:.29s}
.stagger>*:nth-child(7){animation-delay:.34s}.stagger>*:nth-child(8){animation-delay:.39s}
.stagger>*:nth-child(9){animation-delay:.44s}.stagger>*:nth-child(n+10){animation-delay:.49s}

:root[data-motion="off"] *,:root[data-motion="off"] *::before,:root[data-motion="off"] *::after{
  animation-duration:.01ms !important;animation-iteration-count:1 !important;
  transition-duration:.01ms !important;scroll-behavior:auto !important}
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;
    transition-duration:.01ms !important}
}


/* ============================================================================
   SHELL — desktop rail, tablet pill bar, mobile dock. Three real layouts.
   ========================================================================== */
#app{position:relative;z-index:2;min-height:100vh;min-height:100dvh;opacity:0}
#app.ready{opacity:1;transition:opacity .5s var(--ease)}

/* ------------------------------------------------------------------ brand -*/
.brand{display:flex;align-items:center;gap:11px;min-width:0}
.brand-mark{width:38px;height:38px;flex:0 0 auto;border-radius:12px;position:relative;
  background:var(--grad-brand);box-shadow:var(--sh-brand);display:grid;place-items:center;
  transition:transform .5s var(--spring)}
.brand-mark::after{content:"";position:absolute;inset:0;border-radius:inherit;
  background:linear-gradient(160deg,rgba(255,255,255,.4),transparent 55%);pointer-events:none}
.brand-mark svg{width:22px;height:22px;position:relative;z-index:1}
.brand:hover .brand-mark{transform:rotate(-7deg) scale(1.06)}
.brand-mark.beat{animation:breathe 4.5s var(--ease-io) infinite}
.brand-txt{display:flex;flex-direction:column;line-height:1.02;min-width:0}
.brand-txt .l1{font-family:var(--f-dis);font-weight:600;font-size:13.5px;letter-spacing:.005em;
  color:var(--ink-2);white-space:nowrap}
.brand-txt .l2{font-family:var(--f-dis);font-weight:800;font-size:19px;letter-spacing:-.028em;
  background:var(--grad-brand);-webkit-background-clip:text;background-clip:text;
  color:transparent;-webkit-text-fill-color:transparent;white-space:nowrap;
  padding-right:.12em;margin-right:-.12em}

/* ------------------------------------------------------------- desktop rail */
#rail{position:fixed;left:0;top:0;bottom:0;width:var(--nav-w);z-index:60;display:none;
  flex-direction:column;padding:22px 16px 18px;gap:18px;
  background:var(--glass);backdrop-filter:blur(26px) saturate(160%);
  -webkit-backdrop-filter:blur(26px) saturate(160%);
  border-right:1px solid var(--line)}
:root[data-theme="dark"] #rail{background:linear-gradient(180deg,rgba(14,18,48,.82),rgba(8,11,32,.86))}
#rail .brand{padding:4px 8px 2px}
.nav{display:flex;flex-direction:column;gap:3px;flex:1 1 auto;min-height:0;overflow-y:auto}
.nav-lbl{padding:14px 12px 6px}
.nav-item{display:flex;align-items:center;gap:12px;padding:11px 13px;border-radius:14px;
  color:var(--ink-2);font-weight:600;font-size:14.5px;position:relative;
  transition:color .2s var(--ease),background .2s var(--ease),transform .3s var(--spring);
  cursor:pointer;text-align:left;width:100%}
.nav-item .i{color:var(--ink-3);transition:color .2s,transform .35s var(--spring)}
.nav-item:hover{color:var(--ink);background:var(--surface-2)}
.nav-item:hover .i{color:var(--brand);transform:translateY(-1.5px) scale(1.06)}
.nav-item[aria-current="page"]{color:var(--ink);background:var(--brand-soft);font-weight:700}
.nav-item[aria-current="page"] .i{color:var(--brand)}
.nav-item[aria-current="page"]::before{content:"";position:absolute;left:-16px;top:50%;
  transform:translateY(-50%);width:4px;height:22px;border-radius:0 4px 4px 0;background:var(--grad-brand)}

.rail-card{border-radius:var(--r-lg);padding:14px;background:var(--surface);
  border:1px solid var(--line);box-shadow:var(--sh-1);position:relative;overflow:hidden}
:root[data-theme="dark"] .rail-card{background:linear-gradient(155deg,var(--surface-2),var(--surface))}
.rail-foot{display:flex;flex-direction:column;gap:9px;padding:0 6px}
.foot-link{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--ink-3);font-weight:600;
  transition:color .2s}
.foot-link:hover{color:var(--brand)}
.foot-link .i{width:14px;height:14px}

/* -------------------------------------------------------------- top bars --*/
#topbar{position:sticky;top:0;z-index:55;height:var(--top-h);display:flex;align-items:center;
  gap:14px;padding:0 clamp(14px,3vw,28px);
  background:var(--glass);backdrop-filter:blur(24px) saturate(170%);
  -webkit-backdrop-filter:blur(24px) saturate(170%);
  border-bottom:1px solid var(--line);transition:box-shadow .3s,background .5s}
#topbar.scrolled{box-shadow:var(--sh-2)}
:root[data-theme="dark"] #topbar{background:rgba(7,10,28,.76)}

.pagetitle{font-family:var(--f-dis);font-weight:700;font-size:17px;letter-spacing:-.02em;
  display:flex;align-items:center;gap:9px;min-width:0}
.pagetitle .i{color:var(--brand)}

/* chips in the bar */
.chip{display:inline-flex;align-items:center;gap:6px;height:34px;padding:0 12px;border-radius:99px;
  background:var(--surface-2);border:1px solid var(--line);font-size:13px;font-weight:700;
  font-family:var(--f-dis);color:var(--ink-2);white-space:nowrap;
  transition:transform .3s var(--spring),border-color .2s,background .2s,box-shadow .3s}
.chip .i{width:15px;height:15px}
.chip.streak .i{color:var(--brand-3)}
.chip.xp .i{color:var(--brand)}
.chip:hover{transform:translateY(-1.5px);border-color:var(--brand-line);box-shadow:var(--sh-1)}
.chip.pop{animation:popIn .5s var(--spring)}
.chip b{color:var(--ink);font-weight:800}

/* the universal theme toggle — same top-right slot in every layout */
.iconbtn{width:38px;height:38px;border-radius:12px;display:grid;place-items:center;
  color:var(--ink-2);background:var(--surface-2);border:1px solid var(--line);
  transition:transform .35s var(--spring),color .2s,background .2s,border-color .2s,box-shadow .3s;position:relative}
.iconbtn:hover{color:var(--brand);border-color:var(--brand-line);transform:translateY(-2px);box-shadow:var(--sh-1)}
.iconbtn:active{transform:translateY(0) scale(.94)}
.iconbtn.ghost{background:transparent;border-color:transparent}
.iconbtn.ghost:hover{background:var(--surface-2);border-color:var(--line)}
#themeBtn{overflow:hidden}
#themeBtn .tw{display:grid;place-items:center;transition:transform .55s var(--spring),opacity .3s}
#themeBtn .tw.out{transform:rotate(-100deg) scale(.3);opacity:0}

.avatar{width:38px;height:38px;border-radius:12px;display:grid;place-items:center;flex:0 0 auto;
  font-family:var(--f-dis);font-weight:800;font-size:15px;color:#fff;letter-spacing:-.02em;
  background:var(--grad-brand);box-shadow:var(--sh-brand);cursor:pointer;
  transition:transform .35s var(--spring)}
.avatar:hover{transform:scale(1.07) rotate(-4deg)}
.avatar.sm{width:30px;height:30px;font-size:12px;border-radius:10px}
.avatar.lg{width:76px;height:76px;font-size:29px;border-radius:24px}
.avatar.xl{width:104px;height:104px;font-size:40px;border-radius:32px}

/* -------------------------------------------------------- tablet pill nav -*/
#pillnav{display:none;gap:4px;padding:5px;border-radius:99px;background:var(--surface-2);
  border:1px solid var(--line);margin:0 auto}
#pillnav .pill{display:flex;align-items:center;gap:7px;padding:8px 15px;border-radius:99px;
  font-weight:650;font-size:13.5px;color:var(--ink-3);font-family:var(--f-dis);
  transition:color .22s,background .3s var(--ease),transform .3s var(--spring);white-space:nowrap}
#pillnav .pill .i{width:17px;height:17px}
#pillnav .pill:hover{color:var(--ink);transform:translateY(-1px)}
#pillnav .pill[aria-current="page"]{color:var(--on-brand);background:var(--grad-brand);
  box-shadow:var(--sh-brand)}

/* ---------------------------------------------------------- mobile dock --*/
#dock{position:fixed;left:0;right:0;bottom:0;z-index:60;display:none;
  padding:8px 10px calc(8px + env(safe-area-inset-bottom));
  background:var(--glass-strong);backdrop-filter:blur(26px) saturate(180%);
  -webkit-backdrop-filter:blur(26px) saturate(180%);
  border-top:1px solid var(--line)}
:root[data-theme="dark"] #dock{background:rgba(6,9,26,.9)}
#dock .dockrow{display:grid;grid-template-columns:repeat(5,1fr);gap:2px;max-width:520px;margin:0 auto}
.dockbtn{display:flex;flex-direction:column;align-items:center;gap:3px;padding:7px 2px 5px;
  border-radius:14px;color:var(--ink-3);position:relative;transition:color .22s}
.dockbtn .ico{width:30px;height:26px;display:grid;place-items:center;border-radius:9px;position:relative;
  transition:transform .4s var(--spring)}
.dockbtn .i{width:21px;height:21px;stroke-width:1.85;transition:stroke-width .2s}
.dockbtn span{font-size:10.5px;font-weight:700;font-family:var(--f-dis);letter-spacing:.005em}
.dockbtn[aria-current="page"]{color:var(--brand)}
.dockbtn[aria-current="page"] .i{stroke-width:2.4}
.dockbtn[aria-current="page"] .ico{transform:translateY(-3px)}
.dockbtn[aria-current="page"]::after{content:"";position:absolute;top:2px;left:50%;transform:translateX(-50%);
  width:26px;height:26px;border-radius:9px;background:var(--brand-soft);z-index:-1}
.dockbtn:active .ico{transform:scale(.88)}

/* ------------------------------------------------------------- main area -*/
#main{position:relative;min-height:100vh;min-height:100dvh;display:flex;flex-direction:column}
#view{flex:1 1 auto;padding:clamp(16px,2.4vw,30px) clamp(14px,3vw,28px) 40px;
  max-width:var(--maxw);width:100%;margin:0 auto}
.viewhead{margin-bottom:20px}
.viewhead h1{font-size:clamp(26px,4vw,38px);letter-spacing:-.035em}
.viewhead p{color:var(--ink-3);font-size:14.5px;margin-top:5px;max-width:62ch}

/* footer */
.sitefoot{margin-top:auto;padding:26px clamp(14px,3vw,28px) 30px;border-top:1px solid var(--line-soft)}
.sitefoot .in{max-width:var(--maxw);margin:0 auto;display:flex;align-items:center;
  justify-content:space-between;gap:16px;flex-wrap:wrap}
.sitefoot .lk{display:flex;gap:18px;flex-wrap:wrap}
.sitefoot a{display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:650;
  color:var(--ink-3);transition:color .2s,transform .3s var(--spring)}
.sitefoot a:hover{color:var(--brand);transform:translateY(-1px)}
.sitefoot .i{width:15px;height:15px}
.sitefoot .cr{font-size:12.5px;color:var(--ink-4)}

/* ============================ RESPONSIVE ================================= */
/* The wordmark never abbreviates and never wraps, so anything sharing the bar
   with it has to yield. Below the rail breakpoint the brand is on screen, and
   the page title would be the second full-length phrase in the same row — which
   is exactly where "Minati Ma'am's Academy" and "Lesson 1 · English Blunders"
   ran into each other. The dock and the pill nav both already name the section,
   so the title belongs only to the layout where the brand is hidden. */
@media (max-width:1099.98px){ #pageTitle{display:none} }

/* MOBILE (default, <760) — dock at the bottom, brand always full in the bar */
@media (max-width:759.98px){
  :root{--top-h:60px}
  #dock{display:block}
  /* Room for the dock AND for Pipo, who floats above it. Without the second
     part the last control on a screen - the quiz Next question, most often -
     could come to rest under him with nothing left to scroll. 84px is his 60px
     button plus the 12px he stands off the dock, plus a finger's margin. */
  #main{padding-bottom:calc(var(--dock-h,calc(var(--tab-h) + env(safe-area-inset-bottom))) + 84px)}
  #topbar{padding:0 12px;gap:8px}
  #barChips .chip{height:30px;padding:0 9px;font-size:12px;gap:4px}
  #barChips .chip .i{width:13px;height:13px}
  #topbar .iconbtn,#topbar .avatar{width:34px;height:34px;border-radius:11px}
  .hero .row.g10.wrap>.btn-lg{width:100%}
  .brand-txt .l1{font-size:11.5px}
  .brand-txt .l2{font-size:16.5px}
  .brand-mark{width:34px;height:34px;border-radius:11px}
  .brand-mark svg{width:19px;height:19px}
  #view{padding:14px 14px 26px}
  .viewhead h1{font-size:27px}
  .sitefoot{padding:22px 14px 24px}
  .sitefoot .in{flex-direction:column;align-items:flex-start;gap:12px}
  body{font-size:15px}
}
/* the bar now carries a third control, so the chips give way before the
   wordmark or anything you can tap does */
@media (max-width:479.98px){ #barChips .chip.xp{display:none} }
/* very narrow — stack the wordmark but never abbreviate it */
@media (max-width:359.98px){
  .brand-txt .l1{font-size:10.5px}
  .brand-txt .l2{font-size:15px}
  .brand-mark{width:30px;height:30px}
  #barChips{display:none}
}
/* the companion floats over this corner — keep the footer links clear of it */
@media (min-width:560px){ .sitefoot .in{padding-right:88px} }
/* TABLET (760–1099) — centred pill navigation.
   Five pills plus the full wordmark plus three controls is the whole budget, so
   the chips step out here; streak and XP are on Home and Profile in full. */
@media (min-width:760px) and (max-width:1099.98px){
  #pillnav{display:flex}
  /* Through the variable, not around it. This said height:72px directly while
     --top-h still read 68, and --top-h is what every sticky row underneath
     offsets itself by - so in this one band they each stopped 4px too high and
     let a sliver of text show through. */
  :root{--top-h:72px}
  #topbar{padding:0 18px;gap:10px}
  #view{padding:22px 22px 34px}
  .brand-txt .l1{font-size:12.5px}
  .brand-txt .l2{font-size:18px}
  #barChips{display:none}
  #pillnav .pill{padding:8px 12px;font-size:13px}
  /* The pill group is the centrepiece of this layout, so it is centred on the
     BAR — not on whatever gap the wordmark and the controls happen to leave.
     Its auto margins could never do that: the flexible spacer beside it absorbs
     the free space first, which left the group sitting ~100–170px left of
     centre. Taking it out of flow centres it outright, and because the pills
     are symmetrically padded both variants — icon-only below 940, labelled
     above — land dead centre by the same rule. */
  #pillnav{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin:0}
}
/* the lower half of the tablet range cannot fit five labelled pills beside the
   wordmark, so they go icon-only rather than overflowing the bar */
@media (min-width:760px) and (max-width:939.98px){
  #pillnav .pill{padding:9px 12px;justify-content:center}
  #pillnav .pill span{display:none}
}
/* DESKTOP (>=1100) — persistent rail */
@media (min-width:1100px){
  #rail{display:flex}
  #main{margin-left:var(--nav-w)}
  #topbar{padding:0 32px}
  #topbar .brand{display:none}
  #view{padding:26px 32px 46px}
  .sitefoot{padding:30px 32px 34px}
}
@media (min-width:1500px){ :root{--maxw:1340px} }


/* ============================================================================
   COMPONENTS — surfaces, buttons, hero, rings, nudges, badges
   ========================================================================== */

/* ------------------------------------------------------------------ cards -*/
.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-xl);
  padding:20px;box-shadow:var(--sh-1);position:relative;
  transition:transform .42s var(--ease),box-shadow .42s var(--ease),border-color .3s}
:root[data-theme="dark"] .card{background:linear-gradient(158deg,rgba(28,34,74,.72),rgba(13,17,44,.86));
  box-shadow:var(--sh-2),inset 0 1px 0 rgba(255,255,255,.045)}
.card.hoverable{cursor:pointer}
.card.hoverable:hover{transform:translateY(-4px);box-shadow:var(--sh-3);border-color:var(--brand-line)}
.card.hoverable:active{transform:translateY(-1px) scale(.995)}
.card.flat{box-shadow:none}
.card-h{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}
.card-h h3{font-size:15.5px;letter-spacing:-.018em;display:flex;align-items:center;gap:8px}
.card-h h3 .i{width:17px;height:17px;color:var(--brand)}

/* cursor-tracked sheen — the surface reacts to where you are */
.lit{position:relative;overflow:hidden;isolation:isolate}
.lit::before{content:"";position:absolute;inset:-1px;z-index:0;pointer-events:none;opacity:0;
  background:radial-gradient(340px circle at var(--mx,50%) var(--my,50%),
    color-mix(in srgb,var(--brand) 15%,transparent),transparent 62%);
  transition:opacity .45s var(--ease)}
.lit:hover::before{opacity:1}
.lit>*{position:relative;z-index:1}

/* ---------------------------------------------------------------- buttons -*/
.btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;height:46px;
  padding:0 22px;border-radius:15px;font-family:var(--f-dis);font-weight:700;font-size:14.5px;
  letter-spacing:-.006em;position:relative;overflow:hidden;white-space:nowrap;
  transition:transform .32s var(--spring),box-shadow .32s var(--ease),
    background .25s,color .25s,border-color .25s,opacity .25s}
.btn .i{width:18px;height:18px}
.btn:active{transform:scale(.96)}
.btn:disabled{opacity:.44;pointer-events:none}
.btn-p{background:var(--grad-brand);color:#fff;box-shadow:var(--sh-brand)}
.btn-p::after{content:"";position:absolute;top:0;bottom:0;width:34%;
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.34),transparent);
  transform:translateX(-120%) skewX(-18deg);pointer-events:none}
.btn-p:hover{transform:translateY(-2.5px);box-shadow:0 18px 40px -10px color-mix(in srgb,var(--a1) 60%,transparent)}
.btn-p:hover::after{animation:sheen .85s var(--ease)}
.btn-s{background:var(--surface);color:var(--ink);border:1px solid var(--line-strong);box-shadow:var(--sh-1)}
.btn-s:hover{border-color:var(--brand-line);color:var(--brand);transform:translateY(-2px);box-shadow:var(--sh-2)}
.btn-g{background:var(--brand-soft);color:var(--brand);border:1px solid var(--brand-line)}
.btn-g:hover{background:color-mix(in srgb,var(--brand) 16%,transparent);transform:translateY(-2px)}
.btn-q{color:var(--ink-2)}
.btn-q:hover{color:var(--brand);background:var(--surface-2)}
.btn-d{background:var(--no-soft);color:var(--no);border:1px solid var(--no-line)}
.btn-d:hover{background:var(--no);color:#fff;transform:translateY(-2px)}
/* buttons that sit on a hero/gradient panel — they follow the active theme
   because they take their ink from the hero's own mid-stop */
.btn-onhero{background:#fff;color:var(--h2);box-shadow:0 14px 34px -10px rgba(0,0,0,.35)}
.btn-onhero:hover{transform:translateY(-2.5px);box-shadow:0 20px 44px -12px rgba(0,0,0,.42)}
.btn-onhero-ghost{background:rgba(255,255,255,.15);color:#fff;border:1px solid rgba(255,255,255,.3);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
.btn-onhero-ghost:hover{background:rgba(255,255,255,.24);transform:translateY(-2.5px)}
.btn-sm{height:37px;padding:0 15px;font-size:13px;border-radius:12px;gap:7px}
.btn-sm .i{width:15px;height:15px}
.btn-lg{height:54px;padding:0 28px;font-size:16px;border-radius:17px}
.btn-block{width:100%}

/* ----------------------------------------------------------------- pills --*/
.tag{display:inline-flex;align-items:center;gap:6px;height:27px;padding:0 11px;border-radius:99px;
  font-size:11.5px;font-weight:750;font-family:var(--f-dis);letter-spacing:.01em;
  background:var(--surface-3);color:var(--ink-2);border:1px solid transparent;white-space:nowrap}
.tag .i{width:13px;height:13px}
.tag.brand{background:var(--brand-soft);color:var(--brand);border-color:var(--brand-line)}
.tag.ok{background:var(--ok-soft);color:var(--ok);border-color:var(--ok-line)}
.tag.no{background:var(--no-soft);color:var(--no);border-color:var(--no-line)}
.tag.warn{background:var(--warn-soft);color:var(--warn)}
/* a page-level back link, sitting above the hero it belongs to. It is the exact
   counterpart of the lesson reader's Next control and wears the same clothes:
   .btn.btn-s.btn-sm sized identically, arrow leading instead of trailing
   because this one goes back. Same shape, same height, same step-down on a
   narrow phone — one navigation pill, used at both ends of the journey. */
.crumb{height:38px;padding:0 14px;gap:6px;white-space:nowrap;margin-bottom:18px}
.crumb .i{width:16px;height:16px;transition:transform .35s var(--spring)}
.crumb:hover .i{transform:translateX(-3px)}
@media (max-width:400px){
  .crumb{height:32px;padding:0 11px;font-size:12.5px;gap:5px}
  .crumb .i{width:14px;height:14px}
}
.tag.glass{background:rgba(255,255,255,.16);color:#fff;border-color:rgba(255,255,255,.26);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}

.segbar{display:inline-flex;gap:3px;padding:4px;border-radius:14px;background:var(--surface-2);
  border:1px solid var(--line);overflow-x:auto;max-width:100%}
.segbar button{padding:7px 14px;border-radius:11px;font-size:13px;font-weight:700;font-family:var(--f-dis);
  color:var(--ink-3);white-space:nowrap;transition:color .2s,background .3s var(--ease)}
.segbar button:hover{color:var(--ink)}
.segbar button[aria-selected="true"]{background:var(--surface);color:var(--brand);box-shadow:var(--sh-1)}
:root[data-theme="dark"] .segbar button[aria-selected="true"]{background:var(--surface-3);color:var(--brand)}
/* THE GIFT TICK, on the one screen where a stray click costs money. The whole
   row is the target rather than a 14px box, and the chosen state is readable
   from across it: this is the difference between buying a course for yourself
   and buying it for somebody else, decided in one press.
   A class rather than :has(input:checked), for the reason written beside the
   unlock strip: a browser without :has() would show no state at all, and the
   state is the whole point. The script toggles it. */
/* THE CURSOR AND THE HOVER BELONG TO THE FORM THAT IS A CONTROL, and only
   to it. This block has two shapes: a <label> wrapping a real checkbox, and --
   on an order that can only be a gift -- a plain <div> stating that, with no
   tick to clear. Both wore the pointer cursor and the border that lights up,
   which are the two things that tell somebody a row can be pressed, so the
   statement invited a press that did nothing and the only thing that worked
   was the small link at its end. Found by review, 21 September 2026. */
.giftpick{display:flex;align-items:center;gap:11px;padding:11px 13px;
  border-radius:13px;background:var(--surface-2);border:1px solid var(--line);
  text-align:left;transition:background .2s,border-color .2s}
label.giftpick{cursor:pointer}
label.giftpick:hover{border-color:var(--brand-line)}
.giftpick input{width:18px;height:18px;flex:0 0 auto;accent-color:var(--brand);cursor:pointer}
.giftpick .gp-t{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;min-width:0}
.giftpick .gp-t b{font-family:var(--f-dis);font-size:13.5px;letter-spacing:-.01em;color:var(--ink)}
.giftpick .gp-t span{font-size:12.4px;color:var(--ink-3)}
.giftpick.on{background:var(--brand-soft);border-color:var(--brand-line)}
.giftpick.on .gp-t b{color:var(--brand)}
/* A SENTENCE THAT IS ALSO A BUTTON. The way out of something, offered beside
   the thing it is the way out of: the checkout's "Remove the gift" sits at the
   end of the block that says the order is one, where a third real button would
   be the fourth thing on the screen asking to be pressed.
   Quiet rather than brand-coloured, because it lives on a brand-soft panel
   where a brand link is both loud and hard to read. It was two rules a few
   lines apart, one overriding most of the other, after the grey sentence it
   was first written for stopped existing. */
.linkish{font:inherit;font-size:12.4px;font-weight:700;color:var(--ink-2);
  text-decoration:underline;text-underline-offset:3px;white-space:nowrap;
  cursor:pointer;background:none;border:0;padding:0}
.linkish:hover{color:var(--brand)}
.giftpick .gp-out{margin-left:auto;flex:0 0 auto}
/* TWO HALVES THAT FILL THE WIDTH — the Courses screen's Store and My Library.
   The bar above is an inline pill that hugs three short filter words, which is
   right over a list of lessons and wrong for a pair of tabs that name the two
   halves of a screen: they were asked to "cover full Width of the screen area".
   A modifier rather than a change to .segbar, so the lesson filter on the
   course page keeps the shape it has.
   The chosen half is FILLED, not lifted. A white surface is what an active
   segment wears everywhere else in this stylesheet, and it is the one thing
   rejected outright in the mock-ups — the strip keeps its own surface and the
   half you are on carries the brand. The dark rule is repeated because the one
   directly above it is more specific than a plain .segbar.split rule and would
   otherwise put the grey surface back on the selected half at night. */
.segbar.split{display:flex;width:100%}
.segbar.split button{flex:1 1 0;min-width:0;text-align:center}
.segbar.split button[aria-selected="true"],
:root[data-theme="dark"] .segbar.split button[aria-selected="true"]{
  background:var(--grad-brand);color:#fff;box-shadow:var(--sh-brand)}

/* ------------------------------------------------------------ progress ---*/
.bar{height:8px;border-radius:99px;background:var(--ring-track);overflow:hidden;position:relative}
.bar>i{display:block;height:100%;border-radius:99px;background:var(--grad-brand);
  transition:width .9s var(--ease);position:relative}
.bar>i::after{content:"";position:absolute;inset:0;
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.45),transparent);
  background-size:200% 100%;animation:shimmer 2.6s linear infinite}
.bar.thin{height:5px}
.bar.thick{height:12px}

.ring{position:relative;display:grid;place-items:center;flex:0 0 auto}
.ring svg{transform:rotate(-90deg);overflow:visible}
.ring .trk{fill:none;stroke:var(--ring-track)}
.ring .val{fill:none;stroke:url(#ringGrad);stroke-linecap:round;
  transition:stroke-dashoffset 1.1s var(--ease)}
.ring .mid{position:absolute;inset:0;display:grid;place-items:center;text-align:center;line-height:1}
.ring .mid b{font-family:var(--f-dis);font-weight:800;letter-spacing:-.04em}

/* ------------------------------------------------------------------ hero -*/
.hero{position:relative;border-radius:var(--r-2xl);overflow:hidden;isolation:isolate;
  padding:clamp(22px,3.4vw,42px);color:#fff;
  background:linear-gradient(135deg,var(--h1),var(--h2) 52%,var(--h3));
  box-shadow:0 26px 60px -22px rgba(20,14,70,.5)}
/* First-paint hero. Replaced per theme and time of day by the engine. */
:root[data-theme="light"]{--h1:#4B37E8;--h2:#8B3FE0;--h3:#E0389A}
:root[data-theme="dark"]{--h1:#241C6B;--h2:#4C1D8A;--h3:#7A1F63}
.hero::before{content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  background:radial-gradient(560px circle at var(--mx,72%) var(--my,18%),rgba(255,255,255,.2),transparent 60%);
  transition:background .3s linear}
.hero::after{content:"";position:absolute;inset:0;z-index:0;pointer-events:none;opacity:.5;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
  mix-blend-mode:overlay}
.hero>*{position:relative;z-index:2}
.hero canvas.dust{position:absolute;inset:0;z-index:1;pointer-events:none;opacity:.55}
.hero .tod{display:inline-flex;align-items:center;gap:7px;height:30px;padding:0 13px;border-radius:99px;
  background:rgba(255,255,255,.15);border:1px solid rgba(255,255,255,.25);
  backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
  font-size:11.5px;font-weight:750;font-family:var(--f-dis);letter-spacing:.05em;text-transform:uppercase}
.hero .tod .i{width:14px;height:14px}
.hero .greet{font-family:var(--f-ser);font-size:clamp(32px,6.2vw,62px);line-height:1.04;
  letter-spacing:-.024em;margin-top:16px;text-wrap:balance;
  overflow-wrap:anywhere;word-break:break-word;hyphens:auto;max-width:20ch}
/* The italic serif overhangs its inline box, so a gradient fill clips the last
   glyph. Padding restores it for names of any length. */
.hero .greet em{font-style:italic;display:inline-block;max-width:100%;
  background:linear-gradient(100deg,#fff,#FFD9F2 45%,#BFE9FF);
  -webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
  padding-right:.16em;margin-right:-.1em;padding-bottom:.08em;margin-bottom:-.08em;
  overflow-wrap:anywhere;word-break:break-word}
@media (min-width:1100px){.hero .greet{max-width:22ch}}
.hero .sub{font-size:clamp(14.5px,1.6vw,17px);line-height:1.55;margin-top:12px;max-width:52ch;
  color:rgba(255,255,255,.9);font-weight:450;min-height:1.55em}
.hero .sub .cursor{display:inline-block;width:2px;height:1em;vertical-align:-.14em;margin-left:2px;
  background:rgba(255,255,255,.9);animation:blink 1.05s steps(2) infinite}
@keyframes blink{50%{opacity:0}}

.heroStats{display:flex;gap:10px;margin-top:26px;flex-wrap:wrap}
.hstat{display:flex;align-items:center;gap:11px;padding:11px 15px 11px 12px;border-radius:17px;
  background:rgba(255,255,255,.13);border:1px solid rgba(255,255,255,.2);
  backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
  transition:transform .4s var(--spring),background .3s}
.hstat:hover{transform:translateY(-3px);background:rgba(255,255,255,.2)}
.hstat .ic{width:34px;height:34px;border-radius:11px;display:grid;place-items:center;
  background:rgba(255,255,255,.18)}
.hstat .ic .i{width:17px;height:17px}
.hstat .tx{line-height:1.15}
.hstat .tx b{display:block;font-family:var(--f-dis);font-weight:800;font-size:18px;letter-spacing:-.03em}
.hstat .tx span{font-size:10.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
  color:rgba(255,255,255,.72)}

/* ---------------------------------------------------------------- nudges -*/
.nudge{position:relative;display:flex;gap:14px;align-items:flex-start;padding:16px 46px 16px 16px;
  border-radius:var(--r-lg);background:var(--surface);border:1px solid var(--line);
  box-shadow:var(--sh-1);overflow:hidden}
:root[data-theme="dark"] .nudge{background:linear-gradient(150deg,rgba(30,36,80,.7),rgba(14,18,46,.85))}
.nudge::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--grad-brand)}
.nudge .nic{width:40px;height:40px;flex:0 0 auto;border-radius:13px;display:grid;place-items:center;
  background:var(--brand-soft);color:var(--brand)}
.nudge .nic .i{width:19px;height:19px}
.nudge h4{font-size:14.5px;letter-spacing:-.015em}
.nudge p{font-size:13.5px;color:var(--ink-2);margin-top:3px;line-height:1.55}
.nudge .nact{margin-top:10px;display:flex;gap:8px;flex-wrap:wrap}
.nudge .x{position:absolute;top:9px;right:9px;width:29px;height:29px;border-radius:9px;display:grid;
  place-items:center;color:var(--ink-4);transition:color .2s,background .2s,transform .3s var(--spring)}
.nudge .x:hover{color:var(--ink);background:var(--surface-3);transform:rotate(90deg)}
.nudge .x .i{width:15px;height:15px}
.nudge.leaving{animation:nudgeOut .45s var(--ease) forwards}
@keyframes nudgeOut{to{opacity:0;transform:translateX(26px) scale(.96);
  margin-bottom:-14px;max-height:0;padding-top:0;padding-bottom:0}}

/* ----------------------------------------------------------------- badges -*/
.badge{display:flex;flex-direction:column;align-items:center;gap:8px;padding:15px 8px;
  border-radius:var(--r-lg);text-align:center;transition:transform .4s var(--spring);position:relative}
.badge:hover{transform:translateY(-4px)}
.badge .medal{width:56px;height:56px;border-radius:19px;display:grid;place-items:center;position:relative;
  background:var(--grad-brand);box-shadow:var(--sh-brand);color:#fff}
.badge .medal::after{content:"";position:absolute;inset:0;border-radius:inherit;
  background:linear-gradient(155deg,rgba(255,255,255,.42),transparent 52%)}
.badge .medal .i{width:25px;height:25px;position:relative;z-index:1}
.badge.locked .medal{background:var(--surface-3);color:var(--ink-4);box-shadow:none;
  border:1.5px dashed var(--line-strong)}
.badge.locked{opacity:.75}
.badge .bn{font-size:11.5px;font-weight:750;font-family:var(--f-dis);line-height:1.25;letter-spacing:-.01em}
.badge.just .medal{animation:popIn .7s var(--spring)}

/* --------------------------------------------------------------- heatmap -*/
.heat{display:grid;grid-template-columns:repeat(auto-fill,minmax(13px,1fr));gap:4px}
.heat i{aspect-ratio:1;border-radius:4px;background:var(--ring-track);transition:transform .3s var(--spring)}
.heat i[data-l="1"]{background:color-mix(in srgb,var(--brand) 28%,var(--ring-track))}
.heat i[data-l="2"]{background:color-mix(in srgb,var(--brand) 52%,var(--ring-track))}
.heat i[data-l="3"]{background:color-mix(in srgb,var(--brand) 76%,var(--ring-track))}
.heat i[data-l="4"]{background:var(--brand);box-shadow:0 0 10px -2px var(--brand)}
.heat i:hover{transform:scale(1.35)}

.weekrow{display:grid;grid-template-columns:repeat(7,1fr);gap:7px}
.daycell{display:flex;flex-direction:column;align-items:center;gap:7px}
.daycell .dot{width:100%;aspect-ratio:1;max-width:44px;border-radius:13px;display:grid;place-items:center;
  background:var(--surface-3);color:var(--ink-4);font-family:var(--f-dis);font-weight:800;font-size:13px;
  transition:transform .4s var(--spring),background .3s}
.daycell .dot .i{width:17px;height:17px}
.daycell.on .dot{background:var(--grad-brand);color:#fff;box-shadow:var(--sh-brand)}
.daycell.today .dot{outline:2.5px solid var(--brand);outline-offset:2.5px}
.daycell .dl{font-size:10.5px;font-weight:750;color:var(--ink-3);font-family:var(--f-dis);
  text-transform:uppercase;letter-spacing:.06em}
.daycell:hover .dot{transform:translateY(-3px)}

/* --------------------------------------------------------------- toaster -*/
#toaster{position:fixed;z-index:400;left:50%;transform:translateX(-50%);
  top:calc(var(--top-h) + 14px);display:flex;flex-direction:column;gap:9px;
  align-items:center;pointer-events:none;width:min(430px,calc(100vw - 24px))}
.toast{display:flex;align-items:center;gap:11px;padding:11px 17px 11px 12px;border-radius:99px;
  background:var(--glass-strong);backdrop-filter:blur(20px) saturate(180%);
  -webkit-backdrop-filter:blur(20px) saturate(180%);
  border:1px solid var(--line-strong);box-shadow:var(--sh-3);pointer-events:auto;
  animation:toastIn .55s var(--spring) both;max-width:100%}
:root[data-theme="dark"] .toast{background:rgba(20,26,62,.92)}
.toast.out{animation:toastOut .35s var(--ease) forwards}
@keyframes toastIn{from{opacity:0;transform:translateY(-16px) scale(.92)}to{opacity:1;transform:none}}
@keyframes toastOut{to{opacity:0;transform:translateY(-12px) scale(.95)}}
.toast .tic{width:30px;height:30px;border-radius:10px;display:grid;place-items:center;flex:0 0 auto;
  background:var(--grad-brand);color:#fff}
.toast .tic .i{width:16px;height:16px}
.toast.ok .tic{background:var(--ok)}
.toast.no .tic{background:var(--no)}
.toast .tt{font-size:13.5px;font-weight:700;font-family:var(--f-dis);letter-spacing:-.01em}
.toast .ts{font-size:12px;color:var(--ink-3);margin-top:1px}

/* ---------------------------------------------------------------- modals -*/
.scrim{position:fixed;inset:0;z-index:300;background:rgba(8,7,28,.5);
  backdrop-filter:blur(9px);-webkit-backdrop-filter:blur(9px);
  display:grid;place-items:center;padding:18px;animation:fadeIn .3s var(--ease) both;overflow-y:auto}
:root[data-theme="dark"] .scrim{background:rgba(2,3,12,.72)}
.scrim.out{animation:fadeIn .25s var(--ease) reverse both}
/* max-height + its own scroll, rather than leaving a tall modal to be
   scrolled as part of the whole page. Settings — the one modal long enough to
   need this — uses .scrolly below instead, which keeps a proper header
   rather than relying on this alone; this is the fallback for anything else
   that happens to run tall. */
.modal{width:min(540px,100%);max-height:88vh;overflow-y:auto;background:var(--surface);
  border:1px solid var(--line);border-radius:var(--r-2xl);box-shadow:var(--sh-4);padding:26px;
  position:relative;animation:modalIn .5s var(--spring) both;margin:auto}
:root[data-theme="dark"] .modal{background:linear-gradient(160deg,#171D42,#0D1130)}
@keyframes modalIn{from{opacity:0;transform:translateY(22px) scale(.96)}to{opacity:1;transform:none}}
.scrim.out .modal{animation:modalIn .25s var(--ease) reverse both}
.modal.wide{width:min(760px,100%)}
/* Settings alone needs this — it carries the same two-column .dash2 split the
   page itself used to give it, and .wide's 760px squeezes a segbar and a
   voice picker that had roughly twice that room before. */
.modal.xwide{width:min(1000px,100%)}
.modal.slim{width:min(430px,100%)}
.modal .mx{position:absolute;top:14px;right:14px;width:34px;height:34px;border-radius:11px;
  display:grid;place-items:center;color:var(--ink-3);
  transition:background .25s,color .25s,transform .25s}
.modal .mx:hover{background:var(--surface-3);color:var(--ink);transform:rotate(90deg)}
.modal h2{font-size:22px;letter-spacing:-.028em}
.modal .mbody{margin-top:12px;color:var(--ink-2);font-size:14.5px;line-height:1.62}
.modal .mfoot{margin-top:22px;display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap}

/* ------------------------------------------------------- rows of actions --
   Two places show a set of equal actions — the store card's footer and the
   preview modal's — and both had the same fault: the buttons were laid out at
   their natural widths, so the row fitted or it did not depending on how long
   the words happened to be. On a 1440 desktop the card's three needed 344px in
   a 316px row, which put "What's inside" on a line of its own looking like an
   afterthought rather than a third option.

   flex:1 1 0 with min-width:0 divides the row instead of measuring it, so the
   three are always equal, always on one line, and always fill the width —
   which is the same at 320px as it is at 1920.

   The icons come off in this row only. They are what made the difference
   between fitting and not: an icon and its gap is about twenty pixels, and
   three of those is the shortfall exactly. The words are the thing being read
   here, and "Add to cart" needs no picture of a trolley to be understood. */
/* wrap, not nowrap: the free-trial button declares flex-basis 100% so it takes
   a line of its own by design. The two above it still divide their row equally,
   so nothing here can produce the ragged edge nowrap was guarding against. */
.actrow{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap}
.actrow>.btn{flex:1 1 0;min-width:0;padding:0 9px;gap:0;
  font-size:12.6px;white-space:nowrap;overflow:hidden}
.actrow>.btn .i,.actrow>.btn svg{display:none}
/* Below about 360 the row still holds but the longest label runs out of room by
   a few pixels, so the type and the padding come in to meet it. Measured at
   320, which is the narrowest screen still in use. */
@media (max-width:400px){
  .actrow>.btn{padding:0 4px;font-size:11.4px;letter-spacing:-.014em}
}
/* .scrolly — a modal built as a fixed header over a scrolling body, the same
   shape Pipo's own chat panel (.pipohead + .pipomsgs) already uses elsewhere
   in the app. Modal.show({scrolly:true}) moves the title and the close button
   together into .mhead, a non-scrolling row with its own bottom border, and
   everything else into .mbody-scroll beneath it — so closing never requires
   scrolling back up to reach either of them, and the row reads as one piece
   rather than a button floating apart from what it belongs to. */
.modal.scrolly{display:flex;flex-direction:column;padding:0;overflow:hidden}
.modal.scrolly .mhead{flex:0 0 auto;display:flex;align-items:center;gap:14px;
  padding:20px 24px;border-bottom:1px solid var(--line-soft)}
.modal.scrolly .mhead h2{flex:1 1 auto;min-width:0;margin:0}
.modal.scrolly .mx{position:static;flex:0 0 auto}
.modal.scrolly .mbody-scroll{flex:1 1 auto;overflow-y:auto;padding:22px 24px 26px}
@media (max-width:559.98px){
  .scrim{padding:0;align-items:flex-end}
  .modal{width:100%;border-radius:var(--r-2xl) var(--r-2xl) 0 0;padding:22px 18px calc(24px + env(safe-area-inset-bottom));
    animation:sheetIn .48s var(--spring) both;max-height:92dvh;overflow-y:auto}
  @keyframes sheetIn{from{transform:translateY(100%)}to{transform:none}}
  .scrim.out .modal{animation:sheetIn .28s var(--ease) reverse both}
  .modal::before{content:"";display:block;width:40px;height:4px;border-radius:99px;background:var(--line-strong);
    margin:-6px auto 14px}
  .modal .mfoot{flex-direction:column-reverse}
  .modal .mfoot .btn{width:100%}
  /* ...except a row of equal actions, which stays a row. Stacking three
     choices into three full-width bars makes the last one look like the
     answer, and it is not — they are alternatives. */
  .modal .mfoot.actrow{flex-direction:row}
  .modal .mfoot.actrow .btn{width:auto}
  .modal.scrolly{padding:0;max-height:92dvh}
  .modal.scrolly::before{margin:8px auto 0;flex:0 0 auto}
  .modal.scrolly .mhead{padding:12px 18px 16px}
  .modal.scrolly .mbody-scroll{padding:18px 18px calc(24px + env(safe-area-inset-bottom))}
}

/* --------------------------------------------------------------- inputs --*/
.field{display:flex;flex-direction:column;gap:7px}
.field label{font-size:12.5px;font-weight:750;font-family:var(--f-dis);color:var(--ink-2);
  letter-spacing:.005em}
.inp{height:46px;padding:0 15px;border-radius:14px;background:var(--surface-2);
  border:1.5px solid var(--line);color:var(--ink);font-size:14.5px;font-weight:500;width:100%;
  transition:border-color .25s,background .25s,box-shadow .25s}
.inp:hover{border-color:var(--line-strong)}
/* One place for "that did not work" inside a modal. Every account flow can
   fail for a reason only the server knows — an address already in use, a wrong
   current password, a code that has expired — and each of those has to be
   readable where the student is looking, not in a toast behind the modal. */
.merr{margin-top:12px;padding:11px 13px;border-radius:13px;background:var(--no-soft);
  border:1px solid var(--no-line);color:var(--no);font-size:13.4px;line-height:1.5;
  font-weight:600}
.merr[hidden]{display:none}

/* --------------------------------------------------------------- free trial */
/* A row of its own, always. The two actions that cost money share the row above;
   this one spans the width beneath them, at every screen size, because a layout
   that is two rows here and three there is the thing that looks broken.

   The colour is the positive one — free is good news, not the option left over
   after the two that charge. The second line carries "no sign-up needed"
   because that is the objection somebody actually has: free usually means an
   email address and a password first, and saying otherwise removes the excuse
   more effectively than any adjective. */
/* THE BRAND var(--grad-brand)IENT AS AN OUTLINE, not as a fill.

   It was green. The only green in this palette is --ok, which means "your
   answer was correct" - so a green button was borrowing the marking language to
   sell something, and that is why it read as foreign however it was arranged.
   Nothing here is green now. The gradient this whole product is built on draws
   the edge and the words, and the inside stays the surface it sits on: lighter
   than the two buttons above it, and unmistakably the same family.

   Two backgrounds do the border: the surface colour clipped to the padding box
   over the gradient clipped to the border box. That is the only way to put a
   gradient on a border, and it needs no image and no extra element.

   It is finished WITHOUT a mouse, which the old one was not - it spent its
   whole effect on hover, so on a phone it was a flat pale rectangle. The edge
   and the lettering are there at rest; hover fills it in. */
.btn-free{flex:1 1 100% !important;width:100%;
  height:auto !important;min-height:48px;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:1px !important;padding:8px 16px !important;text-align:center;
  color:#fff;border:0;
  position:relative;overflow:hidden;isolation:isolate;
  /* SOLID, AND THE LOUDEST THING ON THE CARD. It was a near-white outline
     between a white Add to cart and a solid Buy now -- the palest button on the
     card carrying the best offer on it. The weights are swapped: this one is
     filled and Buy now is outlined below.
     A DEEPER GRADIENT THAN --grad-brand, and that is not taste. White on #A855F7 is
     3.96:1 and on #FF3D9A 3.29:1 -- both under the 4.5 a 14px label needs, and
     Buy now has been carrying that fault all along. #9E42F6 and #E4006D are the
     same two colours taken down until white clears 4.6 at every stop; only the
     pink moves much, and it is still plainly the brand pink. */
  /* AN ORDINARY SOLID BUTTON, AND THEN SOMETHING MOVES ACROSS IT. The gradient
     used to slide inside the button and the shadow used to breathe, so the
     button itself was never still -- it read as a special effect rather than as
     a button. It sits exactly like Buy now or Sign in now: one static gradient,
     one steady shadow. The only thing that moves is the shine crossing it,
     which is a layer on top and changes nothing underneath. */
  /* THE ORIGINAL GRADIENT, ONE STEP LESS BRIGHT. A flat 10% veil over it, which
     is the only way to take brightness off a fill without touching what it is
     made of: every hue keeps its own value and the three stay in exactly the
     relationship they had. Lightness, saturation and stop positions were each
     tried on the colours themselves and each one changed the gradient into a
     different gradient.
     ON TOP OF --grad-btn, NOT INSTEAD OF IT, so the token stays the site's own
     and Sign in still reads from the same definition. */
  /* NO VEIL. The 10% black over the old stops was how "less bright" was
     delivered; the new stops already are that much deeper, and measured
     against what the owner approved this sits 1.2% off it while keeping
     both would be 21.6% darker. */
  background:var(--grad-btn);
  box-shadow:var(--sh-brand)}
/* AND SOMETHING CROSSES IT, left to right, over and over. White now, because
   the fill is saturated -- the tinted stripe that showed up on a near-white
   button is invisible on this one. Narrow, with long feathered edges, so at any
   instant it is over a fraction of the button and gone in half a second. */
.btn-free::after{content:'';position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:linear-gradient(100deg,
    transparent 38%,
    rgba(255,255,255,.30) 47%,
    rgba(255,255,255,.42) 50%,
    rgba(255,255,255,.30) 53%,
    transparent 62%);
  transform:translateX(-115%);
  animation:freePass 2.8s cubic-bezier(.42,0,.3,1) infinite}
@keyframes freePass{
  0%,6%   {transform:translateX(-115%)}
  60%,100%{transform:translateX(115%)}
}
/* Anybody who has asked the system not to move things gets a button that does
   not move: the gradient sits where it starts and nothing crosses it. */
/* Anybody who has asked the system not to move things gets a plain solid
   button: the shine does not run and nothing else ever moved. */
@media (prefers-reduced-motion:reduce){
  .btn-free::after{animation:none;opacity:0}
}
.btn-free b{font-family:var(--f-dis);font-weight:800;font-size:15px;
  letter-spacing:-.012em;line-height:1.25;color:#fff}
/* The objection, answered on the button: free usually means an email address
   and a password first. It was --ink-3 on a white fill, about 3.7:1, which made
   the whole offer the faintest thing on the button making it. On the fill it is
   white at .92 -- 4.6:1 against the palest stop underneath. One line at every
   width, so two cards side by side are never different heights. */
.btn-free small{font-size:11.6px;font-weight:600;line-height:1.3;
  /* FULL WHITE, not .92 of it. Measured on the fill: .92 comes out 4.05:1
     against the palest stop, and this is 11.6px text, which needs 4.5. The
     eight per cent bought nothing and cost the line its legibility. */
  color:#fff;
  max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.btn-free:hover{transform:translateY(-2px);
  box-shadow:0 14px 34px -8px rgba(90,69,255,.55)}
@media (prefers-reduced-motion:reduce){ .btn-free:hover{transform:none} }

/* BUY NOW STEPS BACK SO START FREE TRIAL CAN LEAD. On this card the free entry
   is the offer worth shouting, and two solid buttons in one row shout over each
   other -- so this one is outlined in the brand colour instead of filled with
   it. Three weights in one family: Add to cart plain, Buy now outlined, Start
   Free Trial filled.
   BRAND TEXT ON WHITE IS 5.63:1, better than the 3.96 white-on-gradient it had.
   The swap costs it nothing in legibility; it only stops it competing.
   Scoped to the card's action row -- .btn-solid is the site's primary button
   everywhere else and is not touched. */
/* A REAL COLOURED OUTLINE, not --brand-line. That token is brand at 22% --
   #DAD6FF on a white button, 1.39:1, which is an edge you have to look for.
   Full brand is 5.63:1 and matches the button's own lettering, so the outline
   and the words read as one deliberate object rather than a box that happens to
   be near some purple text. It is also what keeps this distinct from Add to
   cart beside it, which keeps the grey --line-strong. */
/* .btn-p, NOT .btn-solid. The app and the public pages name their buttons
   differently -- primary is .btn-p here and .btn-solid there, secondary is
   .btn-s here and .btn-ghost there -- so the rule copied across from page.css
   matched nothing and Buy now stayed filled. Two filled buttons on one card,
   and the app's was the one at 3.29:1.
   This is the one place the two files cannot be identical, and it is worth
   saying out loud: identical rules are not the same as identical results when
   the markup underneath uses different names. */
/* AND .lc-buy, WHICH IS THE SAME BUTTON ON THE LIBRARY CARD. It was written
   out by hand there as a plain .btn-s, so a trial card showed a white Buy
   now directly above a store card showing the brand one -- two spellings of
   one button on a single screen. The owner asked for them to match on
   6 September 2026; joining the selector is what stops them parting again.
   Open free preview is .btn-p too and is deliberately NOT in here: it stays
   the filled gradient, because it is the one thing the card is offering. */
.actrow .btn-p,.lc-buy{background:var(--brand-soft);color:var(--brand);
  border:1.5px solid var(--brand);box-shadow:none}
/* .13, not .17. Brand text on a 17% tint is 4.37:1 -- a hover state is
   still text somebody reads, and it must not fall under 4.5 the moment a
   pointer touches it. */
/* MORE THAN SUBTLE, WITHOUT BECOMING A PURPLE SLAB. A 13% tint was a
   hover you had to look for; filling it solid brand was the other
   extreme and she said so -- "buy now hover is now too purple".
   What changes instead is the EDGE and the LIFT: the tint goes to 15%,
   which is as far as it can go with brand lettering on it (17% is
   4.37:1), the border doubles, and a brand shadow puts it above the
   card. Three things move at once, so it is unmistakable, and none of
   them is more colour. */
.actrow .btn-p:hover,.lc-buy:hover{background:rgba(90,69,255,.15);
  border-color:var(--a1);color:var(--brand);transform:translateY(-2px);
  /* an inset ring rather than a thicker border: these buttons are
     border-box, so growing the border eats 1.5px of content on each
     side and the label shifts under the pointer. A shadow thickens the
     edge and moves nothing. */
  box-shadow:inset 0 0 0 1.5px var(--a1),0 8px 20px -8px rgba(90,69,255,.55)}

/* .btn-sm shrinks the shared button to 38px; this one carries two lines, so it
   keeps its own floor rather than being squeezed under its own text.

   51, NOT 46, AND THE BUNDLE CARD IS IN THE SELECTOR. 46 was a floor the
   button had already cleared -- its two lines measure 51 -- so the number
   said one thing and the screen showed another, and nothing could be made
   equal to it by naming it. It is the real height now.

   The bundle is the one card with no trial to offer, so What's inside takes
   the bottom row in its place (.ccard.bundle in p04 says why). It was doing
   that at 37px against every neighbour's 51, which left its action row 14px
   short and its last button hanging above the line the rest of the shelf
   ends on -- the owner asked for them to be the same size on
   6 September 2026. One declaration for both, so they cannot part.

   :nth-child(3) BECAUSE THE ROW IS NOT ALWAYS THREE BUTTONS. Off sale the
   bundle loses Buy now, and What's inside is then the second of two -- see
   the p04 rule, which stops claiming a row of its own in that case. Sizing
   it like a trial button there would make it 51 next to a 37, which is the
   mismatch this rule exists to remove, pointing the other way. */
.btn-free.btn-sm,
.ccard.bundle .actrow>.btn:last-child:nth-child(3){min-height:51px}
.btn-free.btn-sm{font-size:inherit}

/* --------------------------------------------------------------- in cart --
   A STATE, NOT AN ACTION, and it was painted like an action: soft violet beside
   the solid violet of Buy now, two strengths of one colour arguing. Once the
   course is in the cart there is nothing left to do here, so it goes quiet and
   the eye carries on to Checkout. Its own class rather than a repaint of
   .btn-g, which the certificate link and the read-aloud toggles also wear. */
.btn.incart{background:var(--surface-2);border:1px solid var(--line);
  color:var(--ink-3);box-shadow:none}
.btn.incart:hover{background:var(--surface-3);color:var(--ink-2);
  border-color:var(--line-strong);transform:none;box-shadow:none}

/* ---------------------------------------------------------------- mini cart */
/* The same bar the public store raises, so the shop behaves the same on both
   sides of the sign-in. It is only ever present when something is in the cart:
   an empty bar is a permanent strip of nothing across the bottom of a phone. */
.minicart{position:fixed;left:0;right:0;bottom:0;z-index:70;
  background:var(--glass);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);
  border-top:1px solid var(--line-strong);box-shadow:0 -10px 34px -14px rgba(24,20,70,.3);
  transform:translateY(115%);transition:transform .34s var(--spring);
  padding-bottom:env(safe-area-inset-bottom,0)}
.minicart.up{transform:none}
.mc-in{max-width:1180px;margin:0 auto;display:flex;align-items:center;gap:12px;
  padding:11px clamp(14px,4vw,28px)}
.mc-list{flex:1 1 auto;display:flex;align-items:center;gap:11px;min-width:0;text-align:left}
.mc-n{flex:0 0 auto;width:30px;height:30px;border-radius:10px;display:grid;place-items:center;
  background:var(--grad-brand);color:#fff;font-family:var(--f-dis);font-weight:800;font-size:14px;
  box-shadow:var(--sh-brand)}
.mc-t{min-width:0;display:block}
.mc-t b{display:block;font-family:var(--f-dis);font-weight:800;font-size:17px;
  letter-spacing:-.03em;line-height:1.1}
.mc-t i{display:block;font-style:normal;color:var(--ink-3);font-size:12.4px;margin-top:1px}
.mc-go{flex:0 0 auto}
/* The dock sits at the bottom on a phone, so the bar rides above it rather than
   over it. */
/* --dock-h is the dock's measured height, written by syncBottomChrome(). The
   fallback is the nominal calculation, which is a few pixels short - it only
   applies for the one frame before the measurement lands. */
@media (max-width:759.98px){
  .minicart{bottom:var(--dock-h,calc(var(--tab-h) + env(safe-area-inset-bottom)));
    /* AND IT HAS FURTHER TO FALL, BY EXACTLY THAT MUCH.

       translateY(115%) is what hides this bar, and 115% of a 65px bar is 74.5
       -- enough to clear the bottom of the window when it sits on it, and
       four hundredths of a pixel short of the 75 it is lifted by here. So on
       a phone the cart stayed on screen on Home, Quiz, Flashcards and
       Profile, fully visible, on every tab it is deliberately kept off.
       Measured at 390x900: top 835 of a 900px window, 64.8px of it showing.

       The script was right all along -- paintMiniCart() takes .up off
       everywhere but the shelf; the bar simply never left the screen when it
       did. It falls by its own height plus the lift now, so the sum cannot be
       short whatever the dock measures. .minicart.up wins on specificity, so
       raising it is untouched. */
    transform:translateY(calc(115% + var(--dock-h,calc(var(--tab-h) + env(safe-area-inset-bottom)))))}
}
/* IT STOPS WHERE THE RAIL STOPS. The bar itself is unchanged - same glass, same
   single top edge, same shelf sitting on the bottom of the screen - it simply no
   longer starts at the left edge of the window. It was spanning the whole width
   at z-index 70 over a rail at 60, so the cart was drawn across the bottom of
   the navigation, which is not its to cover. */
@media (min-width:1100px){ .minicart{left:var(--nav-w)} }

/* A student's own picture, once there is one. The initial stays underneath as
   the markup it always was — this only covers it — so a picture that fails to
   load, or a student who has never had one, is the screen that shipped before
   any of this existed. */
.avatar.hasimg{background-size:cover;background-position:center;color:transparent;
  text-shadow:none}

/* ------------------------------------------------------------- the cropper */
/* A square stage with a round window cut into it. The picture underneath is a
   plain positioned <img> the drag handler moves; the ring is drawn on top with
   one enormous box-shadow, which dims everything outside the circle without a
   second element and without an SVG mask. */
.cropstage{position:relative;width:100%;max-width:280px;aspect-ratio:1;margin:16px auto 0;
  border-radius:18px;overflow:hidden;background:var(--surface-3);cursor:grab;
  touch-action:none;user-select:none}
.cropstage.grabbing{cursor:grabbing}
.cropstage img{position:absolute;max-width:none;pointer-events:none;
  -webkit-user-drag:none}
.cropring{position:absolute;inset:0;border-radius:50%;pointer-events:none;
  box-shadow:0 0 0 999px rgba(6,8,30,.55);border:2px solid rgba(255,255,255,.85)}
.inp:focus{outline:none;border-color:var(--brand);background:var(--surface);
  box-shadow:0 0 0 4px var(--brand-soft)}
.inp::placeholder{color:var(--ink-4)}
select.inp{appearance:none;cursor:pointer;padding-right:38px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%237B7FA4' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 13px center}
textarea.inp{height:auto;padding:12px 15px;resize:vertical;line-height:1.6}

/* ------------------------------------------------------------ checkout --
   Three small things the payment modal needs and nothing else uses. Kept with
   the other form styles rather than inlined, so the modal is built from the
   same tokens as every other surface in the app. */
.hintline{font-size:12.3px;color:var(--ink-3);line-height:1.5;margin-top:6px}
.payerr{margin-top:14px;padding:11px 13px;border-radius:14px;text-align:left;
  background:var(--no-soft);border:1px solid var(--no-line);color:var(--no);
  font-size:13.4px;font-weight:600;line-height:1.5}
.payerr[hidden]{display:none}
/* The button keeps its size while it works, so the modal does not jump. */
.mini-spin{width:15px;height:15px;flex:0 0 auto;border-radius:50%;
  border:2px solid rgba(255,255,255,.35);border-top-color:#fff;
  animation:spinS .7s linear infinite;margin-right:9px;display:inline-block;
  vertical-align:-2px}

.switch{position:relative;width:50px;height:29px;border-radius:99px;background:var(--ring-track);
  border:1px solid var(--line);flex:0 0 auto;cursor:pointer;transition:background .35s var(--ease)}
.switch::after{content:"";position:absolute;top:2px;left:2px;width:23px;height:23px;border-radius:50%;
  background:#fff;box-shadow:0 2px 6px rgba(0,0,0,.22);transition:transform .38s var(--spring)}
.switch[aria-checked="true"]{background:var(--grad-brand);border-color:transparent}
.switch[aria-checked="true"]::after{transform:translateX(21px)}
.switch:active::after{width:27px}

input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:6px;border-radius:99px;
  background:linear-gradient(90deg,var(--brand) var(--p,50%),var(--ring-track) var(--p,50%));
  outline:none;cursor:pointer}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:20px;height:20px;border-radius:50%;
  background:#fff;border:3px solid var(--brand);cursor:pointer;box-shadow:var(--sh-2);
  transition:transform .28s var(--spring)}
input[type=range]::-webkit-slider-thumb:hover{transform:scale(1.22)}
input[type=range]::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background:#fff;
  border:3px solid var(--brand);cursor:pointer;box-shadow:var(--sh-2)}

.setrow{display:flex;align-items:center;gap:14px;padding:14px 2px;border-bottom:1px solid var(--line-soft)}
.setrow:last-child{border-bottom:0}
.setrow .sic{width:38px;height:38px;flex:0 0 auto;border-radius:12px;display:grid;place-items:center;
  background:var(--surface-3);color:var(--ink-2)}
.setrow .sic .i{width:18px;height:18px}
.setrow .st{flex:1 1 auto;min-width:0}
.setrow .st b{display:block;font-size:14px;font-weight:700;font-family:var(--f-dis);letter-spacing:-.012em}
.setrow .st span{display:block;font-size:12.5px;color:var(--ink-3);margin-top:2px;line-height:1.5}

/* theme picker — each option previews the identity it will apply.
   minmax(0,1fr), not plain 1fr: a bare 1fr track is really minmax(auto,1fr),
   so its floor is the widest min-content of whatever lands in that column —
   here, Marine and Orchid happening to fall in column 2 with a couple of the
   longer subtitles among the five. Below ~640px that floor exceeded the
   column's actual share of the grid, and the grid does not clip its own
   overflow, so those two cards were rendered wider than the track and spilled
   past the grid's own right edge. minmax(0,…) removes the content-based floor
   so the track — and the card filling it — shrinks with the grid instead. */
.themegrid{display:grid;gap:10px;grid-template-columns:minmax(0,1fr)}
@media (min-width:520px) and (max-width:1099.98px){.themegrid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (min-width:1240px){.themegrid{grid-template-columns:repeat(2,minmax(0,1fr))}}
.themecard{display:flex;align-items:center;gap:13px;padding:11px;border-radius:16px;text-align:left;
  background:var(--surface-2);border:1.5px solid var(--line);width:100%;
  transition:transform .35s var(--spring),border-color .22s,background .22s,box-shadow .3s}
.themecard:hover{transform:translateY(-2px);border-color:var(--brand-line);box-shadow:var(--sh-1)}
.themecard.on{border-color:var(--brand);background:var(--brand-soft)}
.tc-art{width:58px;height:44px;flex:0 0 auto;border-radius:12px;position:relative;overflow:hidden;
  display:flex;align-items:flex-end;gap:3px;padding:6px;box-shadow:var(--sh-1)}
.tc-art i{width:9px;height:9px;border-radius:50%;box-shadow:0 0 0 1.5px rgba(255,255,255,.5)}
.tc-tx{flex:1 1 auto;min-width:0}
.tc-tx b{display:block;font-family:var(--f-dis);font-weight:750;font-size:13.8px;letter-spacing:-.014em}
.tc-tx span{display:block;font-size:11.8px;color:var(--ink-3);margin-top:1px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tc-check{width:22px;height:22px;flex:0 0 auto;border-radius:50%;display:grid;place-items:center;
  background:var(--brand);color:#fff;opacity:0;transform:scale(.5);
  transition:all .32s var(--spring)}
.tc-check .i{width:13px;height:13px;stroke-width:3}
.themecard.on .tc-check{opacity:1;transform:none}


/* ============================================================================
   ONBOARDING — an entrance, not a form
   ========================================================================== */
/* IT SCROLLS WHEN IT DOES NOT FIT, and only then. It used to clip: on a small
   phone with its text set larger -- 320x568 at 130%, 360x640 at 150% -- the
   bottom of the welcome, which is where Continue with Google sits, was cut off
   with no way to reach it. Landscape phones were cut off the same way. When it
   fits, nothing changes and no scrollbar appears.
   The aura and the dust are fixed to the screen rather than placed inside #onb:
   the aura hangs 20% past every edge, and inside a scrolling box that overhang
   would itself be something to scroll to. */
#onb{position:fixed;inset:0;z-index:500;display:grid;place-items:center;padding:24px;
  overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;
  background:linear-gradient(150deg,#0A0A26,
    color-mix(in srgb,var(--h1) 82%,#000) 40%,
    color-mix(in srgb,var(--h3) 62%,#12082A) 74%, #0B1140)}
#onb .ob-aura{position:fixed;inset:-20%;z-index:0}
#onb .ob-aura i{position:absolute;border-radius:50%;filter:blur(80px);opacity:.55;mix-blend-mode:screen}
#onb .ob-aura i:nth-child(1){width:60vmax;height:60vmax;background:var(--a1);top:-18vmax;left:-14vmax;
  animation:drift1 18s var(--ease-io) infinite alternate}
#onb .ob-aura i:nth-child(2){width:48vmax;height:48vmax;background:var(--a3);bottom:-14vmax;right:-10vmax;
  animation:drift2 22s var(--ease-io) infinite alternate}
#onb .ob-aura i:nth-child(3){width:42vmax;height:42vmax;background:var(--a2);top:34%;left:38%;
  animation:drift3 20s var(--ease-io) infinite alternate}
#onb canvas{position:fixed;inset:0;z-index:1;pointer-events:none}
#onb .ob-in{position:relative;z-index:2;width:min(560px,100%);text-align:center;color:#fff}
#onb .ob-mark{width:82px;height:82px;margin:0 auto;border-radius:26px;display:grid;place-items:center;
  background:linear-gradient(140deg,rgba(255,255,255,.22),rgba(255,255,255,.06));
  border:1px solid rgba(255,255,255,.28);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
  box-shadow:0 22px 60px -18px rgba(0,0,0,.6);animation:popIn .9s var(--spring) both}
#onb .ob-mark svg{width:44px;height:44px}
#onb .ob-mark path.dr{stroke-dasharray:120;stroke-dashoffset:120;animation:draw 1.5s var(--ease) .3s forwards}
#onb .ob-mark circle.dr{opacity:0;animation:fadeIn .5s var(--ease) 1.4s forwards}
@keyframes draw{to{stroke-dashoffset:0}}
#onb .ob-brand{margin-top:22px;font-family:var(--f-dis);font-weight:500;font-size:14px;
  letter-spacing:.34em;text-transform:uppercase;color:rgba(255,255,255,.7);
  animation:fadeUp .9s var(--ease) .5s both}
#onb h1{font-family:var(--f-ser);font-size:clamp(38px,8.4vw,72px);line-height:1;letter-spacing:-.03em;
  margin-top:12px;animation:fadeUp 1s var(--ease) .68s both;text-wrap:balance}
#onb h1 em{font-style:italic;background:linear-gradient(100deg,#FFD9F2,#BFE9FF 60%,#fff);
  -webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent}
/* THE PANEL BOTH WAYS IN SIT ON, 28 September 2026 (mock-up version 7): one quiet
   frosted card in the same glass as the logo tile above it, so the two choices
   read as one finished thing rather than loose pieces on the gradient. It takes
   the entrance the form used to have; the form inside it no longer animates on
   its own. */
#onb .ob-opts{margin:34px auto 0;width:min(384px,100%);text-align:left;padding:26px 24px 22px;
  border-radius:26px;background:linear-gradient(160deg,rgba(255,255,255,.09),rgba(255,255,255,.04));
  border:1px solid rgba(255,255,255,.14);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.10),0 34px 80px -44px rgba(0,0,0,.7);
  animation:fadeUp 1s var(--ease) .86s both}
#onb .ob-form{margin:0}
#onb .ob-wrap{position:relative;margin:0}
/* A SIZE SMALLER THAN IT WAS -- 26px at most, against 32 -- so the name line no
   longer out-shouts Continue with Google above it. Otherwise the original line,
   as Ronnie asked: "below the OR , keep the original design". */
#onb input{width:100%;height:58px;background:transparent;border:0;border-bottom:2px solid rgba(255,255,255,.26);
  color:#fff;font-family:var(--f-dis);font-size:clamp(21px,4.2vw,26px);font-weight:600;text-align:center;
  letter-spacing:-.02em;transition:border-color .3s;padding:0 52px 0 12px}
#onb input:focus{outline:none;border-bottom-color:transparent}
#onb input::placeholder{color:rgba(255,255,255,.32);font-weight:400;font-size:var(--ob-ph,1em)}
#onb .ob-underline{position:absolute;left:0;right:0;bottom:0;height:2px;overflow:hidden;border-radius:2px}
#onb .ob-underline i{display:block;height:100%;width:0;background:linear-gradient(90deg,#8B5CF6,#FF3D9A,#22D3EE);
  transition:width .55s var(--ease)}
/* THE ARROW IS ON THE SCREEN FROM THE START, dimmed, and brightens once there is
   a letter to send. It used to be invisible until then -- which was fine while
   the name was the only way through, and stopped being fine the day Continue with
   Google went under it: on an untouched screen Google would have been the only
   control there was, and so the obvious thing to press, for somebody who only
   meant to type their name. Owner, 13 September 2026: "people who are going to
   enter just the name will think why will I have to click on continue with
   google, because the next button doesn't reveal unless someone types 1 letter."
   Pressing it with nothing typed is harmless: the submit handler puts the cursor
   in the box and stops. (Google has been above the name since 28 September 2026,
   and the reason holds as it was: the arrow must not look like nothing.) */
#onb .ob-go{position:absolute;right:2px;bottom:8px;width:40px;height:40px;border-radius:13px;
  display:grid;place-items:center;background:#fff;color:#2A1470;opacity:.42;transform:none;
  transition:all .45s var(--spring);pointer-events:auto;box-shadow:0 10px 26px -6px rgba(0,0,0,.5)}
#onb .ob-go.on{opacity:1}
#onb .ob-go:hover{transform:translateX(3px) scale(1.06)}
#onb .ob-hint{margin-top:12px;font-size:12.5px;color:rgba(255,255,255,.6);display:flex;
  align-items:center;justify-content:center;gap:7px}
#onb .ob-hint .i{width:15px;height:15px;flex:none;opacity:.85}

/* THE FIRST DOOR -- Continue with Google. It was the other door until 28 September
   2026, a slim pill under the name; since then it comes first (see the note in
   p07). What follows about the mark and its disc still holds.

   THE MARK SITS ON A WHITE DISC, and the disc is not decoration. Google's rules
   forbid their four-colour G on any background other than white, their dark or
   their light grey -- and this screen is a purple gradient. On white it is on
   a background they allow. (Checked against developers.google.com/identity/
   branding-guidelines on 13 September 2026. What remains outside their rules is
   that this pill is not one of their three button styles; the owner chose the
   look knowing that.)

   Every number here was measured off a render, not guessed:
     22px disc around a 13.5px letter -- the disc first came out at 26px, which made
     the whole mark half as big again as the logo it replaced. 13.5px is the size
     the letter itself was drawn at before; Google's file fills its box edge to
     edge, so the box is smaller to keep the letter the same.
     GOOGLE'S OWN ARTWORK, not the copy the sign-in page and the lesson wall use.
     That copy leaves a hairline gap where the green meets the blue -- the owner
     saw it as the G "kinda broken" -- and Google's file overlaps the two instead,
     so nothing can open up at any size. From gstatic.com/firebasejs/ui/2.0.0/
     images/auth/google.svg, "Copyright (c) 2016 Google Inc."
     THE DISC IS PART OF THE DRAWING, not the span's background. As a CSS
     background with an SVG inside it, the browser rounded the disc and the
     letter to the pixel grid separately -- and since this screen is centred, the
     disc lands at a different fraction of a pixel on every screen height. The
     same code painted the letter a pixel low on one screen and centred on the
     next, with identical geometry both times; the owner saw the bad one twice
     while every render here looked right. In one SVG, rounding moves both
     together. Measured at four screen heights, landing on four different
     fractions, at 1x, 1.5x, 2x and 3x: the letter's weight within 0.2% of the
     middle every time, 0.7% at 1x.
     translate(+0.33, -0.40) past edge-centred -- centred by where the letter's
     WEIGHT sits, not by its edges. A G is a heavy curve with an open side; its
     weight lies 0.33px left of and 0.40px below its own middle at this size.
     Taken from a 480px render of the vector on white, where no pixel rounding
     can reach it -- the in-page measurements that preceded it turned out to be
     measuring the rounding, not the letter.
     display:block on the svg -- inline, it sat on the text baseline inside the
     span and was pushed up by the descender space under it.
   The words were lifted 1.5px and underlined while this was a link; the pill that
   followed, and the button that replaced it on 28 September 2026, have neither. */
/* THE BUTTON, 28 September 2026. The see-through look of the pill before it, which
   Ronnie asked to keep ("I want google button background same as current"), in
   the rounded-rectangle shape of the first mock-up, at a size between the two he
   saw: "earlier was too large and now is too small". 50px tall across the panel.
   The words are Google's own, "Continue with Google"; the "Or" of 14 September
   went with the pill, because this is no longer the alternative to anything.
   No backdrop blur: the gradient under it has no detail to blur, and it made the
   browser redo the blur over the moving aura and dust on every frame. */
/* AT LEAST 50px, NOT 50px. With a phone's text set very large the words wrap,
   and a fixed height let the second line spill out over the border and into the
   line under it; the pill before this grew into a rounded box instead. The 6px
   top and bottom only matter then -- one line sits in the 50px as it did.
   Review of 29 September 2026. */
#onb .ob-g{position:relative;display:flex;align-items:center;justify-content:center;gap:12px;
  width:100%;min-height:50px;padding:6px 18px;border-radius:15px;background:rgba(255,255,255,.10);color:#fff;
  border:1px solid rgba(255,255,255,.34);font-size:15px;font-weight:650;text-decoration:none;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.14),0 16px 34px -18px rgba(0,0,0,.6)}
#onb .ob-g:hover{background:rgba(255,255,255,.18);border-color:rgba(255,255,255,.6)}
#onb .ob-g-ring{display:inline-grid;place-items:center;width:22px;height:22px;flex:none;line-height:0}
#onb .ob-g-ring svg{display:block}
#onb .ob-g-t{display:inline-block;line-height:1.2;text-align:left;transform:translateY(-.5px)}
/* ONE LINE UNDER IT: what Google does on the left, the quiet way in for every other
   address on the right ("subtle option for people who want to enter non google
   emails"). They were two lines, and "packed"; side by side they are one. */
#onb .ob-row{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:11px;
  padding:0 4px;font-size:12.5px;line-height:1.3;color:rgba(255,255,255,.6);flex-wrap:wrap;row-gap:6px}
/* WHERE THE TWO DO NOT FIT ON ONE LINE -- phones under 360px wide, or larger text --
   the link moves to a line of its own, still on the right, rather than folding
   "Saved in the cloud" in two against it. Measured: at 320 and 344px it folded.
   Review of 28 September 2026. */
#onb .ob-note{display:flex;align-items:center;gap:7px;white-space:nowrap}
#onb .ob-note .i{width:15px;height:15px;flex:none;opacity:.85}
/* WITH NO GOOGLE ABOVE IT -- an install without it -- the line is the first thing
   in the panel and needs no gap to separate it from a button. Review of
   29 September 2026. */
#onb .ob-g[hidden]+.ob-row{margin-top:0}
#onb .ob-alt{display:inline-flex;align-items:center;gap:3px;white-space:nowrap;font-weight:600;margin-left:auto;
  color:rgba(255,255,255,.88);text-decoration:none}
#onb .ob-alt:hover{color:#fff}
/* A THUMB-SIZED TARGET ON A SMALL LINK, as the pill before it had. The words are
   about 16px tall, well under the 44-48px a phone's touch guidelines ask for;
   this invisible layer makes the pressable area 44px tall without changing
   what is seen. Uneven on purpose: 10px up, because Google's button is 11px
   above and must keep its own edge, and 18px down, where the "or" is 24px
   away. Review of 29 September 2026. */
#onb .ob-alt{position:relative}
#onb .ob-alt::before{content:"";position:absolute;inset:-10px -8px -18px}
#onb .ob-alt .i{width:14px;height:14px}
/* "or continue as a guest", between the two ways in, with lines that fade out at
   their ends. A small sentence, not the spaced capitals "OR" had: the longer words
   in capitals left the lines no room on a phone, and as a sentence they fit one
   line down to 320px (129px of text in a 234px row, measured). Ronnie's words and
   choice, drawn both ways, 29 September 2026. */
#onb .ob-or{display:flex;align-items:center;gap:12px;margin:24px 0 4px;font-size:12.5px;
  color:rgba(255,255,255,.58)}
#onb .ob-or::before,#onb .ob-or::after{content:"";flex:1;height:1px}
#onb .ob-or::before{background:linear-gradient(90deg,rgba(255,255,255,0),rgba(255,255,255,.24))}
#onb .ob-or::after{background:linear-gradient(90deg,rgba(255,255,255,.24),rgba(255,255,255,0))}
/* The entry animations use fill-mode "both", which holds opacity:1 and beats a
   plain declaration — so the form has to be told to stop animating before it
   can clear the stage for the welcome. */
#onb.stage2 .ob-in>*:not(.ob-final){
  animation:none !important;opacity:0;transform:translateY(-18px) scale(.985);
  transition:opacity .45s var(--ease),transform .6s var(--ease);pointer-events:none}
#onb .ob-final{position:fixed;inset:0;display:grid;place-items:center;padding:26px;
  opacity:0;pointer-events:none;text-align:center}
/* Arrive quickly — the hold happens after this lands, not during it. */
#onb.stage2 .ob-final{opacity:1;animation:fadeUp .62s var(--ease) .14s both}
#onb .ob-final .fn{font-family:var(--f-ser);font-size:clamp(34px,7.4vw,64px);line-height:1.06;
  letter-spacing:-.03em;text-wrap:balance;max-width:16ch;margin-inline:auto;
  overflow-wrap:anywhere;word-break:break-word}
#onb .ob-final .fn em{font-style:italic;display:inline-block;
  background:linear-gradient(100deg,#FFD9F2,#BFE9FF 60%,#fff);
  -webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
  padding-right:.16em;margin-right:-.1em;padding-bottom:.08em;margin-bottom:-.08em}
#onb .ob-final .fs{font-size:15.5px;color:rgba(255,255,255,.74);margin-top:16px;
  display:flex;align-items:center;justify-content:center;gap:9px}
#onb .ob-final .fs i{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.7);
  animation:tdot 1.2s infinite}
#onb .ob-final .fs i:nth-child(2){animation-delay:.16s}
#onb .ob-final .fs i:nth-child(3){animation-delay:.32s}
#onb.done{animation:obOut 1.05s var(--ease) forwards}
@keyframes obOut{0%{opacity:1;transform:scale(1)}55%{opacity:1;transform:scale(1.03)}
  100%{opacity:0;transform:scale(1.14);visibility:hidden}}

/* ============================================================================
   DASHBOARD
   ========================================================================== */
.dashgrid{display:grid;gap:16px;margin-top:16px}
@media (min-width:760px){.dashgrid{gap:18px;grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (min-width:1100px){.dashgrid{grid-template-columns:minmax(0,1.62fr) minmax(0,1fr);gap:20px}}
.nudgestack{display:flex;flex-direction:column;gap:11px;margin-top:16px}

.contcard{position:relative;overflow:hidden;padding:0}
.contcard .art{height:104px;position:relative;background:var(--grad-cool);overflow:hidden}
.contcard .art::after{content:"";position:absolute;inset:0;
  background:radial-gradient(300px circle at 78% 20%,rgba(255,255,255,.3),transparent 60%)}
.contcard .art .lnum{position:absolute;right:16px;bottom:-14px;font-family:var(--f-dis);font-weight:800;
  font-size:82px;color:rgba(255,255,255,.16);letter-spacing:-.06em;line-height:1}
.contcard .art .tag{position:absolute;left:16px;top:16px}
.contcard .bd{padding:18px}
.sentpair{display:flex;flex-direction:column;gap:8px}
/* The verdict marks run 10% down the brightness scale from the shared ok/no
   accents. Redefining the two tokens on .sline itself means the chip fill and
   the strike-through move together and every sentence pair matches, wherever it
   appears — while quiz feedback, toasts and tags keep the accents at full. */
.sline{--ok:#058760; --no:#CB1A64;
  display:flex;align-items:flex-start;gap:9px;font-size:14.5px;line-height:1.5}
:root[data-theme="dark"] .sline{--ok:#2FCE9E; --no:#E65384}
/* The cross and the tick carry the whole point of this course, so they are solid
   chips rather than 9%-alpha tints, and the wrong sentence stays readable ink
   rather than dropping to the muted tertiary grey. One rule set, so every place
   a sentence pair appears — lesson hero, lesson prose, the dashboard's
   "pick up where you left off" card, the quiz prompt — reads the same. */
.sline .mk{width:21px;height:21px;flex:0 0 auto;border-radius:7px;display:grid;place-items:center;
  margin-top:1px;box-shadow:0 1px 3px rgba(24,20,70,.16)}
.sline .mk .i{width:13px;height:13px;stroke-width:3}
.sline.bad .mk{background:var(--no);color:var(--on-no)}
.sline.bad .stx{color:var(--ink-2);font-weight:500;text-decoration:line-through;
  text-decoration-color:var(--no);text-decoration-thickness:1.5px;text-underline-offset:2px}
.sline.good .mk{background:var(--ok);color:var(--on-ok)}
.sline.good .stx{color:var(--ink);font-weight:650}
.sline.tick .mk{background:var(--brand);color:var(--on-brand)}
.sline.tick .stx{color:var(--ink)}
.sline.arrow .mk{background:var(--surface-3);color:var(--ink-2);box-shadow:none}

.mini{display:flex;align-items:center;gap:12px;padding:12px;border-radius:16px;
  transition:background .25s,transform .35s var(--spring);cursor:pointer;width:100%;text-align:left}
.mini:hover{background:var(--surface-2);transform:translateX(3px)}
.mini .mn{width:38px;height:38px;flex:0 0 auto;border-radius:12px;display:grid;place-items:center;
  font-family:var(--f-dis);font-weight:800;font-size:13.5px;background:var(--surface-3);color:var(--ink-2)}
.mini .mn.on{background:var(--grad-brand);color:#fff}
.mini .mt{flex:1 1 auto;min-width:0}
.mini .mt b{display:block;font-size:13.8px;font-weight:700;letter-spacing:-.012em;font-family:var(--f-dis);
  max-width:100%}
.mini .mt span{display:block;font-size:12.5px;color:var(--ink-3);margin-top:1px}
.mini .mt .bar{display:block;width:100%}
.mini .mn .i{width:17px;height:17px}
.mini>.i{color:var(--ink-4);transition:transform .3s var(--spring),color .2s}
.mini:hover>.i{color:var(--brand);transform:translateX(3px)}

.insight{display:flex;gap:12px;align-items:flex-start;padding:12px 0;border-bottom:1px solid var(--line-soft)}
.insight:last-child{border-bottom:0}
.insight .iic{width:32px;height:32px;flex:0 0 auto;border-radius:10px;display:grid;place-items:center;
  background:var(--brand-soft);color:var(--brand)}
.insight .iic .i{width:16px;height:16px}
.insight b{font-size:13.5px;font-family:var(--f-dis);letter-spacing:-.012em}
.insight p{font-size:12.8px;color:var(--ink-3);margin-top:2px;line-height:1.5}

.qa{display:grid;grid-template-columns:repeat(2,1fr);gap:11px}
.qabtn{display:flex;flex-direction:column;gap:9px;padding:15px;border-radius:18px;text-align:left;
  background:var(--surface-2);border:1px solid var(--line);
  transition:transform .38s var(--spring),box-shadow .35s,border-color .25s,background .25s}
.qabtn:hover{transform:translateY(-4px);box-shadow:var(--sh-2);border-color:var(--brand-line);
  background:var(--surface)}
.qabtn .qic{width:38px;height:38px;border-radius:13px;display:grid;place-items:center;color:#fff}
.qabtn .qic .i{width:19px;height:19px}
.qabtn b{font-size:13.8px;font-family:var(--f-dis);letter-spacing:-.014em}
.qabtn span{font-size:12px;color:var(--ink-3);line-height:1.42}

/* ============================================================================
   COURSES
   ========================================================================== */
/* ------------------------------------------------------------ my library --*/
/* The courses already paid for, and the switch between them. Deliberately
   compact: a library is a short list of things a student already has, and
   giving each one a full catalogue card is exactly what pushed the Store off
   the first screen. These say where you are, then get out of the way. */
/* the same track sizes as .catalogue below it, so a library card and a store
   card are exactly as wide as each other at every width and the two sections
   read as one shelf rather than two grids that happen to be stacked */
/* 16px was enough when a card opened with its own quiet padding: the heading
   had 36px of nothing under it before anything was drawn. A card wearing the
   Active band starts with a saturated colour at its very top edge, so the same
   16px put "My Library" and "ACTIVE" within a few pixels of each other with a
   block of brand in between - two labels fighting over one small space. The
   grid takes the air instead, so it is right whichever card is active and
   whether any of them is. */
.libgrid{display:grid;gap:16px;margin-top:26px}
/* AND THE COLUMN COUNT IS SET BY WHAT A CARD HOLDS, NOT BY THE SCREEN.

   These were 640 and 1100, chosen from the screen alone, and at 1100 they
   produced a card 241px wide -- NARROWER THAN A PHONE, because a phone is one
   column at full width. Nobody noticed while the card carried two buttons.
   With three it is the tightest layout in the app, and the owner asked for
   one row at every width on 5 September 2026.

   The three buttons need 313px at the store card's spacing and the card 42 of
   padding, so a column under about 355 cannot hold them whatever the buttons
   do. Measured at every breakpoint rather than derived from one: 830 gives a
   386px card with 31 to spare, 1500 gives 375 with 20. Below those the count
   drops and the cards get bigger. .catalogue below carries the same two
   numbers, because the comment above says the two shelves are one.

   SO A 1440 LAPTOP SHOWS TWO A ROW WHERE IT SHOWED THREE, and that is the
   trade, stated rather than buried: three columns at 1440 leaves 313px inside
   a card for a row that needs 313, and one pixel is not a layout. The cards
   are larger and the trial card is readable. */
@media (min-width:830px){.libgrid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (min-width:1500px){.libgrid{grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}}
/* Art and title share the top row, progress spans the next, Open and the Active
   marker take the last. Every card in the library is built and painted
   identically — which one is active is reported by the marker alone, never by
   the card. The sheen is a background layer rather than an overlay so it stays
   under the content without any stacking work. */
.libcard{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr);
  column-gap:17px;row-gap:17px;align-items:center;padding:20px;border-radius:var(--r-lg);
  overflow:hidden;border:1px solid var(--line);box-shadow:var(--sh-1);
  background:radial-gradient(340px circle at 94% -12%,var(--brand-soft),transparent 62%),var(--surface);
  transition:transform .4s var(--spring),box-shadow .35s,border-color .3s}
:root[data-theme="dark"] .libcard{
  background:radial-gradient(340px circle at 94% -12%,var(--brand-soft),transparent 62%),
             linear-gradient(150deg,rgba(30,36,80,.62),rgba(14,18,46,.82))}
.libcard:hover{transform:translateY(-3px);box-shadow:var(--sh-2);border-color:var(--brand-line)}
.libcard .lc-art{width:74px;height:74px;border-radius:20px;overflow:hidden;position:relative;
  box-shadow:var(--sh-2);transition:transform .45s var(--spring)}
.libcard .lc-art canvas{position:absolute;inset:0;width:100%;height:100%}
/* a glazed edge over the generated art — it sits above the canvas, which an
   inset shadow on the tile itself could not */
.libcard .lc-art::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  box-shadow:inset 0 1px 1px rgba(255,255,255,.42),inset 0 0 0 1px rgba(255,255,255,.16)}
.libcard:hover .lc-art{transform:scale(1.05)}
/* the card is a grid, so its columns are blockified for free — but the text
   column's own children are not, and .bar would collapse to nothing as an
   inline box */
.libcard .lc-main{display:block;min-width:0}
/* the whole course name, wrapped rather than cut — this is the label the
   student is choosing by, and half of it is not enough to choose on */
.libcard .lc-t{font-family:var(--f-dis);font-weight:750;font-size:16px;
  line-height:1.28;letter-spacing:-.022em;color:var(--ink)}
.libcard .lc-lv{display:block;font-size:12.2px;font-weight:650;color:var(--ink-3);margin-top:6px}
/* WHAT A TRIAL OPENS FREE, on a line of its own under the level and the
   lesson count. The pill is the app's own .tag.brand; only its box changes.
   display:flex, not the inline-flex a .tag carries: a vertical margin on an
   inline-level box does not move the line it sits on, so a margin utility
   measured a gap of zero and the pill sat hard against the line above.
   width:max-content keeps it hugging its two words rather than stretching.
   KNOWN, AND ACCEPTED: this makes the text column taller than the 74px art
   tile beside it, and the row centres the two against each other -- so the
   title and the level line rise by about 14px on a trial card. */
.libcard .lc-main>.tag{display:flex;width:max-content;margin-top:9px}
.lc-prog{grid-column:1/-1;display:block}
.lc-pn{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:9px;
  font-size:12px;font-weight:700;color:var(--ink-3)}
.lc-pn b{font-family:var(--f-dis);font-size:13.2px;font-weight:800;letter-spacing:-.02em;color:var(--ink-2)}
.libcard .bar{display:block;width:100%}
/* Open takes whatever width the row has left rather than sitting pinned to the
   right edge — stranded against the corner it read as an afterthought, and on
   the card with no marker beside it there was nothing to balance against. It
   still always finishes flush right; it just starts where the content does. */
/* ONE ROW, EVERY WORD, EVERY WIDTH.

   Adding Add to cart to a trial card made three buttons where there had been
   two, and .lc-side is a flex row. The first attempt gave the card a layout of
   its own -- the two buy buttons stretched to equal halves, Open forced onto a
   row beneath -- and the owner settled it in one line: one row, all three, full
   wording, nothing cut, on every layout. Everything here is that sentence.

   A row stays a row because of nowrap, not because its items are told not to
   shrink. flex-shrink cannot prevent wrapping: the browser puts items on a line
   at their content width and starts a new line when the next will not fit,
   whatever the shrink factor says. wrap was also why the card read two
   different ways on two machines: the grid went to three columns at 1100px,
   which made a DESKTOP card narrower than a phone one -- a phone being a
   single column at full width. The breakpoints above are set from what the
   card holds now, so that cannot happen again. */
.lc-side{grid-column:1/-1;display:flex;align-items:center;gap:8px;flex-wrap:nowrap}
/* AND IT IS BUILT TO THE STORE CARD'S NUMBERS, NOT TO ITS OWN.

   The owner's words were "no design consistency", and she was right: this row
   had a gap, a padding and a type size all of its own, so a trial card and the
   store card underneath it on the same screen were two different designs of the
   same three buttons. .actrow in p03 already settled every one of those
   questions for a card's row of actions. They are copied here deliberately --
   flex:1 1 0 so the buttons divide the row instead of hugging their words,
   8px between them, 9px inside them, 12.6px type, and the same 400px breakpoint
   where the padding and the letter-spacing come in to meet a phone.

   TWO DIFFERENCES, BOTH ASKED FOR. The glyphs go from Add to cart and Buy now,
   which is what .actrow does to every button in its row and for the reason it
   states there -- "the words are the thing being read here, and 'Add to cart'
   needs no picture of a trolley to be understood" -- but the arrow on Open free
   preview stays, because that is the one thing the card is offering.

   AND min-width:max-content, WHICH .actrow DOES NOT HAVE. It uses min-width:0
   and lets a label clip; the owner ruled that out here. .btn sets
   overflow:hidden, and a flex item whose overflow is not visible has an
   automatic minimum size of ZERO -- which is how Open free preview quietly
   shrank to 90px against its natural 122 on a 320 phone and lost its last word,
   with text-overflow:clip meaning not even an ellipsis said so. max-content
   makes the words the floor: spacing gives, wording never does. */
.lc-side>.btn{flex:1 1 0;min-width:max-content;padding:0 9px;font-size:12.6px}
/* EXCEPT THAT OPEN IS NOT ONE OF THREE EQUAL THINGS. Equal shares made the
   free way in the same size as the two ways to pay, and the owner said it
   should be larger -- it is the likeliest tap on a trial card and the only
   one that costs nothing.

   2.4, ARRIVED AT BY MEASURING RATHER THAN BY TASTE. 1.8 left the label
   looking packed inside its own button; the owner asked for a little more
   and said in the same breath not so much that the other two start looking
   wrong, which is the real constraint.

   flex-basis stays 0 on all three, which is what keeps Add to cart and Buy
   now the SAME size as each other -- basis auto was tried and gives Open its
   words plus a share, but then the two beside it sit at their own natural
   widths and "Buy now" comes out visibly smaller than "Add to cart" on a
   phone. Equal basis, unequal share, measured on the card: 117/117/251 at
   1440, 73/73/159 on a 390 phone, 67/66/141 at 360. */
.lc-side>.lc-open{flex-grow:2.4}
.lc-side .cart-add .i, .lc-side .cart-add svg,
.lc-side .lc-buy .i,   .lc-side .lc-buy svg{display:none}
@media (max-width:400px){
  .lc-side>.btn{padding:0 4px;font-size:11.4px;letter-spacing:-.014em}
}
/* THE ONE WIDTH THAT CANNOT BE MADE TO WORK, said out loud rather than left to
   bleed off the side of the card. Below 360px the card has 250px inside it and
   the three labels alone come to more than that -- there is no spacing left to
   take and the wording is not up for discussion. So a 320 screen (an iPhone 5
   or SE, 2012-2016) gets the two-row fallback: the pair that spends money, then
   Open under it. Every phone sold since is 360 or wider and gets the one row. */
@media (max-width:359px){.lc-side{flex-wrap:wrap;row-gap:8px}
  .lc-side>.lc-open{flex:1 0 100%}}
/* ACTIVE — a status, so it is built like one: a lit dot with its own halo and a
   micro-label in the same uppercase, letter-spaced voice the Coming Soon chip
   already speaks in. Soft-filled rather than gradient, so it reports alongside
   Open without competing with it. */
/* THE HAT. Full-bleed across the top of the card - the negative margins undo
   the card's own 20px padding, and the card's overflow:hidden trims the ends to
   its corner radius, so the band takes the shape of the card rather than sitting
   on it. Left padding matches the card's, so the word starts on the same line
   the artwork and title start on.

   Filled, not tinted. A wash of brand-soft was too quiet to do a hat's job -
   from a shelf's distance it read as a slightly warmer stripe and the card
   still had to be examined. This is the same --grad-brand the primary buttons
   and the current-lesson chip carry, so the loudest thing on the shelf is a
   colour the app already owns rather than a new one invented for a badge. */
.libcard .lc-live{grid-column:1/-1;margin:-20px -20px 8px;height:36px;padding:0 20px;
  border:0;border-radius:0;letter-spacing:.13em;justify-content:flex-start;
  background:var(--grad-brand);color:#fff;
  box-shadow:0 1px 0 rgba(0,0,0,.06)}
/* White on the band, and pulsing with the ring the current lesson already uses,
   so "this is the one the app is working from" is said the same way in both
   places. The global reduced-motion rule stops it for anybody who asked. */
.libcard .lc-live .lc-dot{background:#fff;--pr:rgba(255,255,255,.6);
  box-shadow:0 0 0 3px rgba(255,255,255,.28);animation:pulseRing 2.4s infinite}
.lc-live{flex:0 0 auto;display:inline-flex;align-items:center;gap:9px;height:31px;
  padding:0 14px 0 11px;border-radius:99px;
  background:var(--brand-soft);border:1px solid var(--brand-line);color:var(--brand);
  font-family:var(--f-dis);font-size:10.6px;font-weight:800;letter-spacing:.1em;
  text-transform:uppercase;white-space:nowrap}
.lc-dot{position:relative;width:8px;height:8px;flex:0 0 auto;border-radius:50%;
  background:var(--brand);box-shadow:0 0 0 3px var(--brand-soft)}
/* one slow ring outward — the difference between "this is the one" and "this
   one is live right now". It rides the halo, never the card. */
.lc-dot::after{content:"";position:absolute;inset:0;border-radius:50%;
  border:1.5px solid var(--brand);animation:lcPing 2.6s var(--ease-io) infinite}
@keyframes lcPing{
  0%{transform:scale(1);opacity:.6}
  70%,100%{transform:scale(2.7);opacity:0}
}
:root[data-motion="off"] .lc-dot::after{display:none}
@media (prefers-reduced-motion:reduce){.lc-dot::after{display:none}}


.catalogue{display:grid;gap:16px;margin-top:16px}
/* 830 and 1500, not 640 and 1100, BECAUSE .libgrid MOVED AND THESE TWO ARE
   ONE SHELF. The comment above .libgrid has said since it was written that a
   library card and a store card must be exactly as wide as each other at
   every width, or the page reads as two grids that happen to be stacked. The
   library moved because a trial card now carries three buttons and 1100 gave
   it a 241px card; leaving the store behind would have put My Library two a
   row directly above the Store three a row on the same screen. */
@media (min-width:830px){.catalogue{grid-template-columns:repeat(2,1fr)}}
@media (min-width:1500px){.catalogue{grid-template-columns:repeat(3,1fr);gap:18px}}

.ccard{position:relative;overflow:hidden;border-radius:var(--r-2xl);border:1px solid var(--line);
  background:var(--surface);box-shadow:var(--sh-1);
  transition:transform .45s var(--ease),box-shadow .45s var(--ease),border-color .3s}
:root[data-theme="dark"] .ccard{background:linear-gradient(158deg,rgba(28,34,74,.7),rgba(13,17,44,.88))}
.ccard:hover{transform:translateY(-5px);box-shadow:var(--sh-3);border-color:var(--brand-line)}
/* The hat keeps the banner's own 3:1 so no part of a course picture is ever
   cropped away. A fixed 128px against a card 273-482 wide took up to 14.5%
   off each side, and three of the six drawn banners lost the first letter of
   their own title on a narrow phone. The public page.css carries the same
   pair of rules, because these two cards are meant to be one card.
   @supports: without aspect-ratio the fixed height must stand, or the hat
   collapses. */
.ccard .cart{height:128px;position:relative;overflow:hidden}
@supports (aspect-ratio: 3 / 1){ .ccard .cart{height:auto;aspect-ratio:3 / 1} }
.ccard .cart canvas{position:absolute;inset:0;width:100%;height:100%}
/* THE COURSE PICTURE, over the generated art. It is inserted before the
   glow, so the light still falls across the photograph the way it fell
   across the gradient, and the state chip still sits above both. If the
   file is not there the <img> removes itself and the canvas below is
   what it always was -- so no list of which courses have pictures has to
   be kept anywhere. object-fit:cover matches the public page, which
   paints the same file as a background-size:cover. */
.ccard .cart .cpic{position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;object-position:center}
.ccard .cart .glow{position:absolute;inset:0;
  background:radial-gradient(320px circle at 22% 24%,rgba(255,255,255,.3),transparent 62%)}
.ccard .cbody{padding:18px}
.ccard h3{font-size:17.5px;letter-spacing:-.026em}
/* TWO LINES, ALWAYS. min-height floors a short blurb; it does nothing to a
   long one, so a three-line blurb made its card 23px taller than the card
   beside it and the grid stretched every other card in the row to match.
   The clamp caps it at the same two lines the floor guarantees, which is
   what page.css does on the public shop's copy of this card. */
.ccard .cdesc{font-size:13.2px;color:var(--ink-3);margin-top:6px;line-height:1.55;
  min-height:2.9em;display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;
  -webkit-box-orient:vertical;overflow:hidden}
/* A BUNDLE'S LAST BUTTON TAKES A ROW OF ITS OWN. A course card runs to two
   rows because Start Free Trial claims one; a bundle cannot be tried, so
   its three buttons sat on a single row and the card came out 58px shorter
   than every card beside it.

   ONLY WHEN THERE ARE THREE. Off sale the row loses Buy now, and this rule
   was still pushing the last of the remaining two onto its own line -- which
   left a DISABLED Add to cart stretched 504px across a row by itself, above a
   full-width What's inside. Two rows of one button each, and the one at the
   top greyed out. With the count in the selector the pair simply shares a row,
   which is the shape the card beside it has in the same state. Found by
   putting the bundle off sale and looking, 6 September 2026. */
.ccard.bundle .actrow > .btn:last-child:nth-child(3){flex:1 1 100%}
.ccard .cmeta{display:flex;gap:14px;margin-top:14px;flex-wrap:wrap}
.ccard .cmeta i{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:650;
  color:var(--ink-3);font-style:normal}
.ccard .cmeta .i{width:14px;height:14px}
/* THE OLD PRICE, quieter than the new one and beside it. --ink-2 rather
   than --ink-3: a struck-through price is the number a saving is measured
   against, so it has to stay readable rather than become decoration.
   page.css draws the same pair on the public shop's card, and the two
   cards are meant to be one card. */
.ccard .cwas{color:var(--ink-2);font-weight:650;margin-right:1px;
  text-decoration-color:var(--ink-2);text-decoration-thickness:1.5px}
/* A course on sale. It used to be dimmed hard - saturate(.42) - which read as
   unavailable, and beside the word "Available" it would have contradicted
   itself. Held back only enough that an owned course still looks different. */
.ccard.avail{opacity:1}
.ccard.avail .cart{filter:saturate(.86)}
.ccard.avail:hover .cart{filter:saturate(1)}
/* A course still in production. It is not dimmed the way a locked-but-buyable
   course is — there is nothing to unlock yet, and greying out the very thing
   the Store exists to advertise would defeat the point. It stays fully lit and
   simply says when, so the art keeps doing its job. */
.ccard.soon{opacity:1}
.ccard.soon .cart{filter:none}
.statechip{position:absolute;top:14px;right:14px;display:inline-flex;align-items:center;gap:6px;
  height:29px;padding:0 12px;border-radius:99px;background:rgba(10,8,32,.5);color:#fff;
  border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
  font-size:11px;font-weight:750;font-family:var(--f-dis);letter-spacing:.06em;text-transform:uppercase}
.statechip .i{width:13px;height:13px}

/* -------------------------------------------------------------------- cart */
/* The cart is a bar control, so it borrows .iconbtn wholesale and adds nothing
   but a count — and the count only exists when there is something to count. */
#cartBtn{flex:0 0 auto}
.cartbadge{position:absolute;top:-6px;right:-6px;min-width:19px;height:19px;padding:0 5px;
  border-radius:99px;display:grid;place-items:center;pointer-events:none;
  background:var(--grad-brand);color:#fff;font-family:var(--f-dis);
  font-size:10.5px;line-height:1;font-weight:800;font-variant-numeric:tabular-nums;
  box-shadow:var(--sh-brand);border:2px solid var(--bg)}
.cartbadge.empty{display:none}
.cartbadge.pop{animation:popIn .45s var(--spring)}

.cartlist{display:flex;flex-direction:column;gap:9px;margin-top:14px}
.cartrow{display:flex;align-items:center;gap:12px;padding:12px;border-radius:16px;
  background:var(--surface-2);border:1px solid var(--line)}
.cartrow.leaving{animation:nudgeOut .3s var(--ease) forwards}
.cartrow .cr-ic{width:40px;height:40px;flex:0 0 auto;border-radius:13px;display:grid;place-items:center;
  background:var(--grad-brand);color:#fff;box-shadow:var(--sh-brand)}
.cartrow .cr-ic .i{width:19px;height:19px}
.cartrow b{display:block;font-family:var(--f-dis);font-size:14px;letter-spacing:-.016em;line-height:1.3}
.cartrow>div>span{display:block;font-size:12.2px;color:var(--ink-3);margin-top:2px}
.cartrow .cr-p{font-family:var(--f-dis);font-weight:800;font-size:14.5px;white-space:nowrap}
/* A COURSE A BUNDLE IN THIS CART ALREADY PAYS FOR. Shown rather than removed:
   the price is struck through and the row says which bundle covers it, so the
   total below adds up in front of the customer. Not dimmed to the point of
   looking unavailable — it is not unavailable, it is included. */
.cartrow.covered{background:var(--surface);border-style:dashed}
.cartrow.covered .cr-ic{background:var(--surface-3);color:var(--ink-3);box-shadow:none}
.cartrow .cr-p.was{text-decoration:line-through;color:var(--ink-4);font-weight:650}
.cartrow .cr-x{width:32px;height:32px;flex:0 0 auto;border-radius:10px;display:grid;place-items:center;
  color:var(--ink-4);transition:all .22s}
.cartrow .cr-x:hover{background:var(--no-soft);color:var(--no)}
.cartrow .cr-x .i{width:15px;height:15px}
.carttotal{display:flex;align-items:center;justify-content:space-between;margin-top:14px;padding-top:14px;
  border-top:1px solid var(--line)}
.carttotal span{font-size:13px;color:var(--ink-3);font-weight:650}
.carttotal b{font-family:var(--f-dis);font-weight:800;font-size:24px;letter-spacing:-.03em}

/* course preview sheet */
.prevhead{position:relative;height:150px;margin:-26px -26px 0;overflow:hidden;
  border-radius:var(--r-2xl) var(--r-2xl) 0 0}
.prevhead canvas{position:absolute;inset:0;width:100%;height:100%}
.prevhead .pvin{position:absolute;inset:auto 22px 18px;color:#fff;z-index:1}
.prevhead .pvin h2{color:#fff;font-size:clamp(20px,3.6vw,26px);text-shadow:0 2px 18px rgba(0,0,0,.35)}
@media (max-width:559.98px){ .prevhead{margin:-22px -18px 0;height:128px} }

.partacc{border:1px solid var(--line);border-radius:var(--r-lg);overflow:hidden;background:var(--surface)}
:root[data-theme="dark"] .partacc{background:rgba(20,25,58,.55)}
.parthead{display:flex;align-items:center;gap:13px;padding:15px 17px;width:100%;text-align:left;
  transition:background .25s}
.parthead:hover{background:var(--surface-2)}
.parthead .pn{width:40px;height:40px;border-radius:13px;display:grid;place-items:center;flex:0 0 auto;
  background:var(--surface-3);font-family:var(--f-dis);font-weight:800;font-size:12.5px;color:var(--ink-2)}
.parthead .pn.done{background:var(--grad-mint);color:#fff}
.parthead .pt{flex:1 1 auto;min-width:0}
.parthead .pt b{display:block;font-size:14.5px;font-family:var(--f-dis);letter-spacing:-.016em}
.parthead .pt span{display:block;font-size:12.2px;color:var(--ink-3);margin-top:2px}
.parthead .cv{transition:transform .38s var(--spring);color:var(--ink-4)}
.parthead[aria-expanded="true"] .cv{transform:rotate(180deg)}
.partbody{display:grid;grid-template-rows:0fr;transition:grid-template-rows .45s var(--ease)}
.partbody>div{overflow:hidden}
.partacc[data-open="1"] .partbody{grid-template-rows:1fr}
.lrow{display:flex;align-items:center;gap:13px;padding:12px 17px 12px 15px;width:100%;text-align:left;
  border-top:1px solid var(--line-soft);transition:background .22s,padding-left .3s var(--ease)}
.lrow:hover{background:var(--surface-2);padding-left:20px}
.lrow .ls{width:30px;height:30px;flex:0 0 auto;border-radius:10px;display:grid;place-items:center;
  background:var(--surface-3);color:var(--ink-3);font-family:var(--f-dis);font-weight:800;font-size:12px}
.lrow .ls.done{background:var(--ok-soft);color:var(--ok)}
/* The finished tick in the row's right-hand group. It was an inline style on
   a hand-written <i data-lucide> placeholder; the markup went to ic() like
   every other icon in the file, and its one colour comes here with it. */
.lrow .lg .lr-done{color:var(--ok)}
.lrow .ls.cur{background:var(--grad-brand);color:#fff;animation:pulseRing 2.4s infinite}
.lrow .ls .i{width:15px;height:15px}
.lrow .lt{flex:1 1 auto;min-width:0}
.lrow .lt b{display:block;font-size:13.6px;font-weight:650;letter-spacing:-.008em;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.lrow .lt span{display:block;font-size:11.6px;color:var(--ink-4);margin-top:1px;font-weight:700;
  font-family:var(--f-dis);letter-spacing:.04em;text-transform:uppercase}
.lrow .lg{display:flex;align-items:center;gap:6px;flex:0 0 auto}
.lrow .lg .i{width:15px;height:15px;color:var(--ink-4)}

/* ============================================================================
   LESSON READER
   ========================================================================== */
/* ---------------------------------------------------------------------------
   READING SURFACE ("paper")
   The same card language used everywhere, tuned for long-form text: a calmer
   field behind the words, a measured line length and a firmer ink. Used by any
   reading view so the treatment is systemic, not a lesson-only exception.
   ------------------------------------------------------------------------- */
.paper{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-2xl);
  box-shadow:var(--sh-2);padding:clamp(20px,3.4vw,40px);position:relative}
:root[data-theme="dark"] .paper{
  background:linear-gradient(168deg,color-mix(in srgb,var(--surface-2) 82%,transparent),
    color-mix(in srgb,var(--surface) 94%,transparent));
  box-shadow:var(--sh-2),inset 0 1px 0 rgba(255,255,255,.045)}
.paper+.paper{margin-top:16px}

.lessongrid{display:grid;gap:22px;align-items:start}
.lessonwrap{max-width:100%;min-width:0}
@media (min-width:1180px){
  .lessongrid{grid-template-columns:minmax(0,1fr) 244px;gap:30px}
  .lessonwrap{max-width:none}
}
@media (max-width:1179.98px){ .lessongrid{grid-template-columns:minmax(0,1fr)} }
@media (min-width:760px) and (max-width:1179.98px){ .lessonwrap{max-width:760px;margin:0 auto} }

/* In This Lesson --------------------------------------------------------- */
.toc{display:none;position:sticky;top:calc(var(--top-h) + 22px);
  max-height:calc(100dvh - var(--top-h) - 46px);overflow-y:auto;padding:2px}
@media (min-width:1180px){ .toc{display:block} }
.toc .tochead{font-family:var(--f-dis);font-size:11px;font-weight:800;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-3);display:flex;align-items:center;gap:8px;
  padding:0 12px 12px}
.toc .tochead .i{width:14px;height:14px;color:var(--brand)}
.toc ol{list-style:none;padding:0;margin:0;position:relative}
.toc ol::before{content:"";position:absolute;left:15px;top:6px;bottom:6px;width:2px;
  border-radius:2px;background:var(--line)}
.toc li{position:relative}
.toc a{display:flex;gap:11px;align-items:flex-start;padding:8px 12px;border-radius:12px;
  font-size:12.9px;line-height:1.42;font-weight:600;color:var(--ink-3);
  transition:color .22s,background .25s var(--ease)}
.toc a .dot{width:8px;height:8px;flex:0 0 auto;border-radius:50%;margin-top:5px;margin-left:3px;
  background:var(--surface);border:2px solid var(--line-strong);position:relative;z-index:1;
  transition:all .3s var(--spring)}
.toc a:hover{color:var(--ink);background:var(--surface-2)}
.toc a[aria-current="true"]{color:var(--ink);font-weight:750;background:var(--brand-soft)}
.toc a[aria-current="true"] .dot{background:var(--brand);border-color:var(--brand);
  transform:scale(1.32);box-shadow:0 0 0 4px var(--brand-soft)}
.toc .tocfoot{margin-top:14px;padding:12px;border-radius:14px;background:var(--surface-2);
  border:1px solid var(--line)}
.toc .tocfoot .bar{margin-top:9px}
.toc .tocfoot b{font-family:var(--f-dis);font-size:12px;letter-spacing:-.01em}
.toc .tocfoot span{font-size:11.5px;color:var(--ink-3)}

.lessonbar{position:sticky;top:var(--top-h);z-index:40;margin:-8px -4px 14px;padding:9px 4px;
  background:linear-gradient(var(--bg) 72%,transparent);display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.lessonbar .lbtitle{font-family:var(--f-dis);font-weight:750;font-size:13.5px;letter-spacing:-.01em;
  color:var(--ink-2);display:flex;align-items:center;gap:8px;min-width:0}
/* previous / next, mirroring the pair at the foot of the lesson. The ends of the
   course keep the slot occupied rather than letting the row shift sideways.
   These live in the lesson's title heading (.lname), at its right edge — the
   title is the one thing on the page that is unambiguously about this lesson,
   so the way to the next one belongs beside it rather than in the toolbar,
   which is about what you can do to the lesson you are already on. */
.lnav{display:flex;align-items:center;gap:6px;flex:0 0 auto}
/* Hard against the far end of the toolbar, opposite the lesson number - the
   same relationship it had with the title it travelled from. */
.lessonbar .lnav.in-bar{margin-left:auto}
.lnav .off{opacity:.26;pointer-events:none}
/* Previous stays the plain square icon button it always was. Next carries its
   own label — the one lesson-to-lesson action worth naming outright — so it
   gets the compact pill treatment instead, sized to match the icon button's
   height at every width. */
.lnav-next{height:38px;padding:0 14px;gap:6px;white-space:nowrap}
.lnav-next .i{width:16px;height:16px}
@media (max-width:400px){
  .lnav .iconbtn{width:32px;height:32px}
  .lnav-next{height:32px;padding:0 11px;font-size:12.5px;gap:5px}
  .lnav-next .i{width:14px;height:14px}
}
/* named actions — the icon stays, the word removes the guesswork.
   The three of them are one row at every width: below the tablet breakpoint they
   take the bar's second line together and share it evenly, and the labels step
   down to short forms rather than ever wrapping or stacking. */
.lessontools{display:flex;align-items:center;gap:8px;flex-wrap:nowrap;min-width:0}
.ltool{min-width:0}
.ltool .lt-l{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ltool .lt-s{display:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media (max-width:759.98px){
  .lessonbar{row-gap:10px}
  /* The bar wraps to two rows here: back arrow and title on the first, the
     three named actions sharing the second between them. */
  .lbtitle{order:1}
  /* The travelling nav belongs on the first row, at its right-hand end. Without
     an order it inherited 0, the same as the back arrow, and being second in
     the DOM among the order-0 items it landed BETWEEN the arrow and "Lesson 1 /
     50" - to the left of the number it is meant to sit opposite. Sharing the
     title's order puts it after the title, which grows to fill the row and so
     carries it out to the edge. */
  .lnav.in-bar{order:1}
  .lessontools{order:2;flex:1 1 100%}
  .ltool{flex:1 1 0;justify-content:center;padding:0 10px;gap:6px}
  .ltool .lt-l{font-size:12.5px}
}
/* below this the full phrase cannot share a row with two other buttons, so the
   middle action drops to its short label instead of the row breaking apart */
@media (max-width:539.98px){
  .ltool .lt-l.long{display:none}
  .ltool .lt-s{display:inline}
}
@media (max-width:400px){
  .ltool{padding:0 8px;gap:5px}
  .ltool .lt-l,.ltool .lt-s{font-size:12px}
  .ltool .i{width:16px;height:16px}
}
@media (max-width:339.98px){
  .ltool .lt-l,.ltool .lt-s{display:none}
  .ltool{padding:0;gap:0}
}
.readprog{position:fixed;left:0;right:0;top:var(--top-h);height:3px;z-index:45;background:transparent}
.readprog i{display:block;height:100%;width:0;background:var(--grad-brand);transition:width .12s linear}
@media (min-width:1100px){.readprog{left:var(--nav-w)}}

.lhero{overflow:hidden}
.lhero::before{content:"";position:absolute;top:0;left:0;right:0;height:4px;background:var(--grad-brand)}
.lhero .lnum{font-family:var(--f-dis);font-size:11.5px;font-weight:800;letter-spacing:.15em;
  text-transform:uppercase;color:var(--brand)}
/* A course may give each lesson a name of its own — "Present Perfect
   Continuous" — where another course is happy to be known by the mistake it
   corrects. Rendered only when the course provides one, so a course without
   names looks exactly as it always did. */
/* The lesson's name is the page's title, so it sits above the first card
   rather than inside it. Only the name: the eyebrow and the unit chip stay on
   the card, where they introduce the example beneath them. */
.lname{font-family:var(--f-dis);font-weight:800;
  font-size:clamp(21px,3.4vw,27px);letter-spacing:-.026em;line-height:1.18;
  color:var(--ink);text-wrap:balance;
  /* A header, not another content card. Tinted rather than white so it does not
     read as a second surface stacked on the first, and carrying an icon and the
     lesson's place in the course so it has a reason to be a card at all — the
     name alone in a plain box looked like an empty one. */
  display:flex;align-items:center;gap:14px;
  margin:0 0 14px;padding:15px 20px;
  border-radius:var(--r-xl);
  /* The quiet inset surface rather than a branded one. The card below it is
     already doing the shouting with its gradient edge, and two saturated blocks
     stacked read as two competing headings instead of a heading and its
     content. */
  background:var(--surface-2);
  border:1px solid var(--line)}
.lname .ln-ic{flex:0 0 auto;width:38px;height:38px;border-radius:12px;
  display:grid;place-items:center;color:#fff;background:var(--grad-brand);
  box-shadow:0 8px 18px -8px rgba(90,69,255,.6)}
.lname .ln-ic .i{width:19px;height:19px}
.lname .ln-t{flex:1 1 auto;min-width:0}
@media (max-width:559.98px){
  .lname{gap:11px;padding:12px 14px;margin-bottom:12px}
  .lname .ln-ic{width:32px;height:32px;border-radius:10px}
  .lname .ln-ic .i{width:16px;height:16px}
}
/* .ln-c and .lunit lived here: a chip beside the lesson title that repeated the
   unit number. Removed when the lesson bar was simplified — the heading already
   carried the number and the toolbar carried it again, so the chip said the
   same thing a third time. Nothing has rendered either class since. */
.lhero .pair{margin-top:16px;display:flex;flex-direction:column;gap:12px}
.lhero .pair .sline{font-size:clamp(16px,2.3vw,20px);align-items:center}
.lhero .pair .sline .mk{width:27px;height:27px;border-radius:9px}
.lhero .pair .sline .mk .i{width:15px;height:15px}

/* One gap value drives the whole reading column, so the space under the lesson
   hero matches the space between every section below it. */
:root{--sec-gap:16px}
#lbody{margin-top:var(--sec-gap)}
.sec{scroll-margin-top:calc(var(--top-h) + 78px)}
.sec+.sec{margin-top:var(--sec-gap)}
.sec-h{display:flex;align-items:center;gap:12px;margin-bottom:18px;
  padding-bottom:16px;border-bottom:1px solid var(--line-soft)}
.sec-h .sn{width:34px;height:34px;border-radius:12px;display:grid;place-items:center;flex:0 0 auto;
  background:var(--brand-soft);color:var(--brand)}
.sec-h .sn .i{width:17px;height:17px}
.sec-h h2{font-size:19px;letter-spacing:-.026em}
.sec-h .num{margin-left:auto;font-family:var(--f-dis);font-size:11.5px;font-weight:800;
  color:var(--ink-4);letter-spacing:.06em}
/* Reading ink is a step firmer than UI secondary text — the same palette, more
   contrast, because this is the only place with paragraphs to get through. */
.prose{font-size:16.4px;line-height:1.78;color:var(--read-ink);max-width:68ch;
  letter-spacing:-.003em}
:root{--read-ink:#2B2E52}
:root[data-theme="dark"]{--read-ink:#C9CFF2}
.prose>*+*{margin-top:14px}
.prose b{color:var(--ink);font-weight:750}
.prose .ph{font-family:var(--f-dis);font-weight:750;font-size:15px;color:var(--ink);letter-spacing:-.014em;
  margin-top:26px;display:flex;align-items:center;gap:9px}
.prose .ph::before{content:"";width:16px;height:2.5px;border-radius:2px;background:var(--brand);flex:0 0 auto}
.prose ul{display:flex;flex-direction:column;gap:9px;margin-top:12px}
.prose ul li{display:flex;gap:11px;align-items:flex-start}
.prose ul li::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--brand);
  margin-top:.66em;flex:0 0 auto}
.prose .sline{margin-top:9px;font-size:15.6px}
.prose table{width:100%;border-collapse:separate;border-spacing:0;margin-top:14px;font-size:14px;
  border:1px solid var(--line);border-radius:var(--r-md);overflow:hidden}
.prose th{background:var(--surface-3);font-family:var(--f-dis);font-weight:750;font-size:12px;
  letter-spacing:.05em;text-transform:uppercase;color:var(--ink-2);text-align:left;padding:11px 14px}
.prose td{padding:11px 14px;border-top:1px solid var(--line-soft);color:var(--ink)}
.prose tbody tr:nth-child(even) td{background:var(--surface-2)}
.prose .tipbox{border-radius:var(--r-lg);padding:18px;background:var(--brand-soft);
  border:1px solid var(--brand-line);margin-top:14px}
.prose .tipbox .prose{color:var(--ink-2)}

/* the block currently being read aloud */
.readmark{background:var(--brand-soft);border-radius:5px;box-shadow:0 0 0 3px var(--brand-soft)}

/* The reading player that used to float here is gone: nothing covers the lesson
   while it is read. Its equaliser now lives on Pipo (see #pipoBtn .pipowave),
   which is where the sound was always coming from. */


/* ============================================================================
   HUB HERO — the one big card at the top of Quiz and Flashcards. Same shell,
   same rhythm, same brand-gradient treatment everywhere it is used, so the two
   sections read as one product rather than two differently-designed screens.
   ========================================================================== */
.mcqhero{position:relative;overflow:hidden;border-radius:var(--r-2xl);padding:0;
  background:linear-gradient(135deg,var(--a1),var(--a2) 50%,var(--a3));color:#fff;
  box-shadow:0 30px 70px -26px color-mix(in srgb,var(--a1) 70%,transparent);
  transition:transform .5s var(--ease),box-shadow .5s var(--ease)}
.mcqhero:hover{transform:translateY(-6px);box-shadow:0 40px 90px -28px color-mix(in srgb,var(--a1) 78%,transparent)}
.mcqhero canvas{position:absolute;inset:0;width:100%;height:100%;opacity:.6;pointer-events:none}
/* No eyebrow above the title: the padding comes in a step, the title starts at
   the top of its column, and the rhythm underneath tightens by the same
   proportion, so the card reads as designed at this height. */
.mcqhero .in{position:relative;z-index:2;padding:clamp(22px,3.1vw,34px);
  display:grid;gap:22px;align-items:center}
@media (min-width:820px){.mcqhero .in{grid-template-columns:minmax(0,1fr) 260px}}
.mcqhero h2{font-size:clamp(28px,5vw,44px);letter-spacing:-.036em;margin-top:0;line-height:1.03}
.mcqhero p{font-size:15px;color:rgba(255,255,255,.86);margin-top:10px;max-width:46ch;line-height:1.6}
.mcqhero .stats{display:flex;gap:9px;margin-top:18px;flex-wrap:wrap}
.mcqhero .acts{display:flex;gap:10px;margin-top:22px;flex-wrap:wrap}
.mcqhero .btn-w{background:#fff;color:var(--a1);box-shadow:0 14px 34px -10px rgba(0,0,0,.4)}
.mcqhero .btn-w:hover{transform:translateY(-2.5px)}
.mcqhero .btn-o{background:rgba(255,255,255,.14);color:#fff;border:1px solid rgba(255,255,255,.3)}
.mcqhero .btn-o:hover{background:rgba(255,255,255,.24);transform:translateY(-2.5px)}
.mcqhero .viz{display:none}
@media (min-width:820px){.mcqhero .viz{display:grid;place-items:center}}
.mcqviz{width:230px;display:flex;flex-direction:column;gap:10px}
.mcqviz .op{display:flex;align-items:center;gap:11px;padding:12px 14px;border-radius:15px;
  background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.2);font-size:13px;font-weight:650;
  animation:opFloat 3.6s var(--ease-io) infinite}
.mcqviz .op:nth-child(2){animation-delay:.4s}.mcqviz .op:nth-child(3){animation-delay:.8s}
.mcqviz .op:nth-child(4){animation-delay:1.2s}
.mcqviz .op b{width:24px;height:24px;border-radius:8px;display:grid;place-items:center;
  background:rgba(255,255,255,.2);font-family:var(--f-dis);font-size:11.5px;flex:0 0 auto}
.mcqviz .op.sel{background:#fff;color:var(--a1);border-color:#fff;box-shadow:0 10px 26px -8px rgba(0,0,0,.4)}
.mcqviz .op.sel b{background:var(--a1);color:#fff}
@keyframes opFloat{0%,100%{transform:translateX(0)}50%{transform:translateX(6px)}}
/* Flashcards' deck choices. Four equal cards: three scopes plus the lesson
   picker. auto-fill left a ragged empty cell at desktop width, so the count is
   fixed per breakpoint — 1 up, 2 up, then a full 4-across row that squares off
   against the four-stat strip above it and leaves no dead space. */
.deckgrid{display:grid;gap:13px;margin-top:16px;grid-template-columns:1fr}
@media (min-width:600px){.deckgrid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (min-width:1100px){.deckgrid{grid-template-columns:repeat(4,minmax(0,1fr))}}
/* a four-stat strip balances two-up then four-up; the six-column default
   belongs to Profile, which actually has six */
.statgrid.s4{grid-template-columns:repeat(2,1fr)}
@media (min-width:760px){.statgrid.s4{grid-template-columns:repeat(4,1fr)}}

/* Which course am I practising — answerable from every practice screen.

   Three things and no more: the course's own icon, its name, and the word that
   changes it. Everything else this row has carried at one time or another — an
   uppercase label, a progress figure, a filled pill, a dropdown — was another
   element competing for the same 40px, and a thin strip holding five things
   reads as clutter however each one is styled. */
.cswitch{display:flex;align-items:center;gap:10px;padding:8px 15px;
  border-radius:var(--r-full);
  background:var(--surface);border:1px solid var(--line);box-shadow:var(--sh-1);margin:14px 0}
:root[data-theme="dark"] .cswitch{background:var(--surface-2)}
.cswitch .cs-ic{width:22px;height:22px;flex:0 0 auto;display:grid;place-items:center;
  color:var(--brand)}
.cswitch .cs-ic .i{width:18px;height:18px}
.cswitch .cs-name{flex:1 1 auto;min-width:0;font-family:var(--f-dis);font-weight:600;
  font-size:14px;letter-spacing:-.014em;color:var(--ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* A text link, not a button — no fill, no border, no height of its own. Its
   character comes from motion rather than from a shape: the underline warms and
   the chevron springs right, which is the same gesture every .mini row in the
   app makes when you hover it. Borrowed rather than invented, so it feels like
   the rest of the product without adding another filled thing to the row. */
.cswitch .cs-change{flex:0 0 auto;display:inline-flex;align-items:center;gap:5px;
  font-family:var(--f-dis);font-weight:700;font-size:13.5px;letter-spacing:-.012em;
  color:var(--brand);transition:color .2s var(--ease)}
.cswitch .cs-change:hover{color:var(--brand-2)}
.cswitch .cs-change .i{width:15px;height:15px;transition:transform .35s var(--spring)}
.cswitch .cs-change:hover .i{transform:translateX(3px)}
.cswitch .cs-change:active .i{transform:translateX(5px)}
/* "Change course" says what it changes; on a phone the row has no room for the
   second word and "Change" beside a course name is unambiguous anyway */
@media (max-width:559.98px){ .cswitch .cs-change .cs-more{display:none} }

.fmtgrid{display:grid;gap:13px;margin-top:16px;grid-template-columns:repeat(auto-fill,minmax(210px,1fr))}
.fmtcard{position:relative;overflow:hidden;padding:18px;border-radius:var(--r-xl);text-align:left;
  background:var(--surface);border:1px solid var(--line);box-shadow:var(--sh-1);
  transition:transform .42s var(--spring),box-shadow .4s,border-color .3s}
:root[data-theme="dark"] .fmtcard{background:linear-gradient(158deg,rgba(28,34,74,.7),rgba(13,17,44,.86))}
.fmtcard:hover{transform:translateY(-5px);box-shadow:var(--sh-3);border-color:var(--brand-line)}
.fmtcard .fic{width:42px;height:42px;border-radius:14px;display:grid;place-items:center;color:#fff;
  box-shadow:var(--sh-2)}
.fmtcard .fic .i{width:21px;height:21px}
.fmtcard b{display:block;font-size:15px;font-family:var(--f-dis);letter-spacing:-.02em;margin-top:13px}
.fmtcard span{display:block;font-size:12.6px;color:var(--ink-3);margin-top:5px;line-height:1.48}
.fmtcard .tag{display:inline-flex;width:auto;font-size:11.5px;margin-top:12px}
.fmtcard .tag .i{width:13px;height:13px}
.fmtcard .go{position:absolute;right:16px;top:18px;color:var(--ink-4);transition:transform .35s var(--spring),color .2s}
.fmtcard:hover .go{color:var(--brand);transform:translate(3px,-3px)}

/* Choosing a lesson: numbers, nothing else. The grid auto-fills so it is five
   or six across on a phone and ten-plus on a desktop without a breakpoint, and
   it scrolls inside the dialog rather than pushing the footer off screen. */
.lespick{display:grid;grid-template-columns:repeat(auto-fill,minmax(46px,1fr));gap:8px;
  max-height:min(46vh,300px);overflow-y:auto;padding:3px;margin-inline:-3px}
.lp{position:relative;aspect-ratio:1;border-radius:13px;display:grid;place-items:center;
  background:var(--surface-2);border:1.5px solid var(--line);color:var(--ink-2);
  font-family:var(--f-dis);font-weight:750;font-size:15px;letter-spacing:-.02em;
  transition:transform .28s var(--spring),border-color .2s,background .2s,color .2s,box-shadow .3s}
.lp:hover{transform:translateY(-2px);border-color:var(--brand-line);color:var(--brand);
  background:var(--surface);box-shadow:var(--sh-1)}
.lp:active{transform:scale(.94)}
.lp.done{background:var(--ok-soft);border-color:var(--ok-line);color:var(--ok)}
.lp.next{background:var(--grad-brand);border-color:transparent;color:#fff;box-shadow:var(--sh-brand)}
.lp.next:hover{color:#fff}
.lp .lp-t{position:absolute;top:-4px;right:-4px;width:16px;height:16px;border-radius:50%;
  display:grid;place-items:center;background:var(--ok);color:var(--on-ok);border:2px solid var(--surface)}
.lp .lp-t .i{width:9px;height:9px;stroke-width:3.4}
.lpkey{display:flex;gap:16px;justify-content:center;margin-top:12px;font-size:11.5px;
  font-weight:650;color:var(--ink-3)}
.lpkey span{display:inline-flex;align-items:center;gap:6px}
.lpkey i{width:11px;height:11px;border-radius:4px;display:block}
.lpkey .k-next{background:var(--grad-brand)}
.lpkey .k-done{background:var(--ok-soft);border:1.5px solid var(--ok-line)}
@media (max-width:379.98px){ .lespick{grid-template-columns:repeat(auto-fill,minmax(42px,1fr));gap:7px} }

/* the same format vocabulary in a row, for choosing one inside a dialog */
.fmtpick{display:flex;align-items:center;gap:13px;width:100%;padding:12px;border-radius:16px;text-align:left;
  background:var(--surface-2);border:1px solid var(--line);color:var(--ink);
  transition:transform .3s var(--spring),border-color .22s,background .22s,box-shadow .3s}
.fmtpick:hover{transform:translateX(3px);border-color:var(--brand-line);background:var(--surface);box-shadow:var(--sh-1)}
.fmtpick:active{transform:scale(.985)}
.fmtpick .fic{width:40px;height:40px;flex:0 0 auto;border-radius:14px;display:grid;place-items:center;
  color:#fff;box-shadow:var(--sh-2)}
.fmtpick .fic .i{width:20px;height:20px}
.fmtpick .ft{flex:1 1 auto;min-width:0}
.fmtpick .ft b{display:block;font-family:var(--f-dis);font-weight:750;font-size:14.5px;letter-spacing:-.018em}
.fmtpick .ft span{display:block;font-size:12.4px;color:var(--ink-3);line-height:1.45;margin-top:2px}
.fmtpick>.i{color:var(--ink-4);flex:0 0 auto}
.fmtpick .tag{flex:0 0 auto}
.fmtpick .tag .i{width:12px;height:12px}
.fmtpick.lead{background:var(--brand-soft);border-color:var(--brand-line);padding:14px}
.fmtpick.lead .ft b{font-size:16px}
.fmtpicks{display:flex;flex-direction:column;gap:8px}
@media (max-width:479.98px){
  .fmtpick .ft span{display:none}
  .fmtpick.lead .ft span{display:block}
  .fmtpick .tag{display:none}
}

/* runner */
.qrun{max-width:720px;margin:0 auto}
.qtop{display:flex;align-items:center;gap:12px;margin-bottom:18px}
.qtop .qcount{font-family:var(--f-dis);font-weight:800;font-size:13px;color:var(--ink-3);
  letter-spacing:.02em;white-space:nowrap;font-variant-numeric:tabular-nums}
.qcard{border-radius:var(--r-2xl);padding:clamp(20px,3vw,30px);background:var(--surface);
  border:1px solid var(--line);box-shadow:var(--sh-2);animation:qIn .5s var(--ease) both}
:root[data-theme="dark"] .qcard{background:linear-gradient(158deg,rgba(28,34,74,.78),rgba(13,17,44,.9))}
@keyframes qIn{from{opacity:0;transform:translateY(16px) scale(.985)}to{opacity:1;transform:none}}
.qcard.leave{animation:qOut .3s var(--ease) forwards}
@keyframes qOut{to{opacity:0;transform:translateY(-14px) scale(.985)}}
.qprompt{font-size:clamp(17px,2.6vw,22px);font-family:var(--f-dis);font-weight:700;letter-spacing:-.024em;
  line-height:1.35;color:var(--ink)}
.qsrc{font-size:11.5px;font-weight:750;font-family:var(--f-dis);letter-spacing:.09em;text-transform:uppercase;
  color:var(--brand);margin-bottom:10px}
.opts{display:flex;flex-direction:column;gap:10px;margin-top:20px}
.opt{display:flex;align-items:center;gap:13px;padding:14px 15px;border-radius:16px;text-align:left;
  background:var(--surface-2);border:1.5px solid var(--line);font-size:15px;line-height:1.45;
  transition:transform .3s var(--spring),border-color .22s,background .22s,box-shadow .3s;width:100%}
.opt .k{width:29px;height:29px;flex:0 0 auto;border-radius:9px;display:grid;place-items:center;
  background:var(--surface);border:1px solid var(--line);font-family:var(--f-dis);font-weight:800;
  font-size:12.5px;color:var(--ink-3);transition:all .22s;text-transform:uppercase}
.opt .ot{flex:1 1 auto;min-width:0}
.opt .rs{width:22px;height:22px;flex:0 0 auto;opacity:0;transition:opacity .25s}
.opt:not(.dis):hover{border-color:var(--brand-line);background:var(--surface);transform:translateX(4px)}
.opt:not(.dis):hover .k{border-color:var(--brand-line);color:var(--brand)}
.opt.sel{border-color:var(--brand);background:var(--brand-soft)}
.opt.sel .k{background:var(--grad-brand);color:#fff;border-color:transparent}
.opt.ok{border-color:var(--ok);background:var(--ok-soft)}
.opt.ok .k{background:var(--ok);color:#fff;border-color:transparent}
.opt.ok .rs{opacity:1;color:var(--ok)}
.opt.no{border-color:var(--no);background:var(--no-soft);animation:shakeX .5s var(--ease)}
.opt.no .k{background:var(--no);color:#fff;border-color:transparent}
.opt.no .rs{opacity:1;color:var(--no)}
.opt.dis{cursor:default}
.opt.fade{opacity:.5}

.fb{margin-top:18px;border-radius:var(--r-lg);padding:15px;display:flex;gap:12px;align-items:flex-start;
  animation:fadeUp .45s var(--ease) both}
.fb.ok{background:var(--ok-soft);border:1px solid var(--ok-line)}
.fb.no{background:var(--no-soft);border:1px solid var(--no-line)}
.fb .fi{width:32px;height:32px;flex:0 0 auto;border-radius:11px;display:grid;place-items:center;color:#fff}
.fb.ok .fi{background:var(--ok)}
.fb.no .fi{background:var(--no)}
.fb b{font-size:13.8px;font-family:var(--f-dis);letter-spacing:-.014em}
.fb p{font-size:13.6px;color:var(--ink-2);margin-top:4px;line-height:1.58}

/* auto-advance ring on every "next" button ------------------------------- */
.qnext{padding-left:14px}
/* PIPO OWNS THE BOTTOM-RIGHT CORNER OF A PHONE, and this button is the one
   thing on the screen you have to press to carry on - right-aligned, into that
   exact corner, so at some scroll positions his head was over it. #main already
   reserves the height to scroll it clear; this reserves the width so it never
   has to be scrolled at all. 72px is his 60px button plus the 14px he stands
   off the edge. The button keeps its full width: the row has the room. */
@media (max-width:759.98px){ .qnext{margin-right:72px} }
.qtimer{position:relative;width:24px;height:24px;flex:0 0 auto;display:grid;place-items:center;
  border-radius:50%;cursor:pointer;color:#fff;
  transition:transform .3s var(--spring),background .25s}
.qtimer:hover{background:rgba(255,255,255,.18);transform:scale(1.1)}
.qtimer:active{transform:scale(.92)}
.qtimer:focus-visible{outline:2px solid #fff;outline-offset:2px}
.qtimer svg{width:24px;height:24px;transform:rotate(-90deg);overflow:visible}
.qtimer circle{fill:none;stroke-width:3}
.qtimer .trk{stroke:rgba(255,255,255,.32)}
.qtimer .run{stroke:#fff;stroke-linecap:round;stroke-dasharray:94.25;stroke-dashoffset:0;
  animation:qcount 5s linear forwards}
@keyframes qcount{from{stroke-dashoffset:0}to{stroke-dashoffset:94.25}}
/* the ring is a countdown, not decoration — it keeps its real duration */
:root[data-motion="off"] .qtimer .run{animation-duration:5s !important}
@media (prefers-reduced-motion:reduce){ .qtimer .run{animation-duration:5s !important} }
.qtimer.paused .run{animation-play-state:paused;opacity:.4}
/* pause bars while it counts, a play triangle once it is held */
.qtimer .qglyph{position:absolute;width:6px;height:8px;
  border-left:2px solid currentColor;border-right:2px solid currentColor;opacity:.95}
.qtimer.paused .qglyph{width:0;height:0;border:none;margin-left:2px;
  border-left:7px solid currentColor;border-top:4.5px solid transparent;border-bottom:4.5px solid transparent}
@media (max-width:420px){ .qnext .qnext-t{font-size:13.5px} }

/* fill in the blank */
.blankline{font-size:clamp(18px,2.8vw,24px);font-family:var(--f-dis);font-weight:600;line-height:1.75;
  letter-spacing:-.018em;color:var(--ink);display:flex;flex-wrap:wrap;align-items:baseline;gap:6px}
.blankin{display:inline-block;min-width:130px;border:0;border-bottom:2.5px solid var(--brand);
  background:var(--brand-soft);border-radius:8px 8px 0 0;padding:2px 12px;font-family:inherit;
  font-size:inherit;font-weight:700;color:var(--brand);text-align:center;transition:all .25s}
.blankin:focus{outline:none;box-shadow:0 6px 18px -6px var(--brand)}
.blankin.ok{border-color:var(--ok);color:var(--ok);background:var(--ok-soft)}
.blankin.no{border-color:var(--no);color:var(--no);background:var(--no-soft);animation:shakeX .5s}
/* Progressive hint: the shape of the word first, then letters — never a list
   of candidate answers, which would give the exercise away. */
.shapehint{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:18px;
  opacity:0;max-height:0;overflow:hidden;transition:opacity .35s var(--ease),max-height .4s var(--ease)}
.shapehint.on{opacity:1;max-height:70px}
.shapehint .shl{font-size:11px;font-weight:800;font-family:var(--f-dis);letter-spacing:.1em;
  text-transform:uppercase;color:var(--ink-3);margin-right:6px}
.shapehint span:not(.shl){width:26px;height:34px;border-radius:8px;display:grid;place-items:center;
  background:var(--surface-2);border:1.5px solid var(--line);font-family:var(--f-dis);font-weight:800;
  font-size:16px;color:var(--ink-4);transition:all .3s var(--spring)}
.shapehint span.on{background:var(--brand-soft);border-color:var(--brand-line);color:var(--brand)}
.shapehint span.pun{width:16px;background:transparent;border-color:transparent;color:var(--ink-3)}
.shapehint.done span:not(.shl){opacity:.55}

/* sentence builder */
.slots{min-height:78px;display:flex;flex-wrap:wrap;gap:8px;padding:14px;border-radius:var(--r-lg);
  background:var(--surface-2);border:2px dashed var(--line-strong);align-content:flex-start;
  transition:border-color .3s,background .3s}
.slots.filled{border-style:solid;border-color:var(--brand-line);background:var(--brand-soft)}
.slots.ok{border-color:var(--ok);background:var(--ok-soft)}
.slots.no{border-color:var(--no);background:var(--no-soft);animation:shakeX .5s}
.slots:empty::after{content:attr(data-ph);color:var(--ink-4);font-size:13.5px;padding:6px 4px}
.bank{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px;min-height:44px}
.word{max-width:100%;padding:9px 14px;border-radius:12px;background:var(--surface);border:1.5px solid var(--line);
  font-family:var(--f-dis);font-weight:650;font-size:15px;color:var(--ink);cursor:grab;
  box-shadow:var(--sh-1);transition:transform .3s var(--spring),box-shadow .3s,border-color .2s;
  user-select:none;-webkit-user-select:none;touch-action:none;
  overflow-wrap:break-word;word-break:break-word;box-sizing:border-box}
.word:hover{transform:translateY(-3px);border-color:var(--brand-line);box-shadow:var(--sh-2)}
.word:active{cursor:grabbing;transform:scale(.95)}
.word.placed{background:var(--grad-brand);color:#fff;border-color:transparent;box-shadow:var(--sh-brand)}
.word.drag{opacity:.4}
.word.ghost{opacity:0;pointer-events:none}
.word.fly{position:fixed;z-index:200;pointer-events:none;margin:0}
/* narrow phones — the same tiles, sized so a full row of them never has to
   fight the edge of the screen */
@media (max-width:479.98px){
  .slots{padding:11px;gap:6px}
  .bank{gap:6px}
  .word{padding:7px 11px;font-size:13.5px;border-radius:10px}
}

/* true / false */
.tfstage{text-align:center;padding:14px 0}
.tfsent{font-family:var(--f-ser);font-size:clamp(24px,4.6vw,38px);line-height:1.24;letter-spacing:-.02em;
  color:var(--ink);text-wrap:balance;margin:8px auto 0;max-width:20ch}
.tfbtns{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:26px}
.tfbtn{padding:20px 14px;border-radius:var(--r-xl);border:2px solid var(--line);background:var(--surface-2);
  display:flex;flex-direction:column;align-items:center;gap:9px;
  transition:transform .35s var(--spring),border-color .22s,background .22s,box-shadow .3s}
.tfbtn .i{width:28px;height:28px}
.tfbtn b{font-family:var(--f-dis);font-weight:750;font-size:15px;letter-spacing:-.018em}
.tfbtn.t{color:var(--ok)}.tfbtn.f{color:var(--no)}
.tfbtn:not(.dis):hover{transform:translateY(-5px);box-shadow:var(--sh-3);background:var(--surface)}
.tfbtn.t:not(.dis):hover{border-color:var(--ok-line)}
.tfbtn.f:not(.dis):hover{border-color:var(--no-line)}
.tfbtn.picked.t{border-color:var(--ok);background:var(--ok-soft)}
.tfbtn.picked.f{border-color:var(--no);background:var(--no-soft)}
.tfbtn.wrongpick{animation:shakeX .5s}

/* match pairs */
.matchgrid{display:grid;grid-template-columns:1fr;gap:20px;margin-top:6px}
@media (min-width:680px){.matchgrid{grid-template-columns:1fr 1fr}}
.mcol{display:flex;flex-direction:column;gap:9px}
.mcol .clbl{font-size:11px;font-weight:800;font-family:var(--f-dis);letter-spacing:.11em;
  text-transform:uppercase;color:var(--ink-3);margin-bottom:2px}
.mitem{padding:13px 14px;border-radius:14px;text-align:left;background:var(--surface-2);
  border:1.5px solid var(--line);font-size:14px;line-height:1.42;
  transition:transform .3s var(--spring),border-color .22s,background .22s,opacity .3s,box-shadow .3s}
.mitem:not(.dis):hover{transform:translateX(4px);border-color:var(--brand-line);box-shadow:var(--sh-1)}
.mitem.sel{border-color:var(--brand);background:var(--brand-soft);transform:scale(1.015)}
.mitem.done{border-color:var(--ok-line);background:var(--ok-soft);opacity:.62;pointer-events:none}
.mitem.bad{border-color:var(--no);background:var(--no-soft);animation:shakeX .45s}
.mitem .mn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;
  border-radius:6px;background:var(--surface);border:1px solid var(--line);font-size:10.5px;font-weight:800;
  font-family:var(--f-dis);color:var(--ink-3);margin-right:8px;vertical-align:1px}
.mitem.done .mn{background:var(--ok);color:#fff;border-color:transparent}

/* odd one out */
.oddgrid{display:grid;gap:11px;margin-top:18px}
@media (min-width:640px){.oddgrid{grid-template-columns:1fr 1fr}}

/* results */
.resultwrap{max-width:680px;margin:0 auto;text-align:center}
/* The results actions. Above the phone breakpoint this is the centred wrapping
   row it has always been, untouched. Below it, a two-up grid: the primary spans
   the full width and the rest pair off, so four buttons never leave one stranded
   on its own line, and .btn's nowrap label gets room by trimming the padding
   rather than by letting the text clip. */
/* One decision first, four options second. The primary is wide and alone so it
   reads as the way forward; the rest sit under it as a quieter row of equals. */
.qacts{display:flex;flex-direction:column;align-items:center;gap:14px}
.qacts .qact-go{width:100%;max-width:420px}
/* the supporting four are deliberately smaller than the primary — it reinforces
   the hierarchy and it is what lets all four share one row on a desktop */
.qact-rest{display:flex;flex-wrap:wrap;gap:9px;justify-content:center;width:100%}
.qact-rest>.btn{height:42px;padding:0 15px;font-size:13.6px;gap:8px}
.qact-rest>.btn .i{width:16px;height:16px}
@media (max-width:539.98px){
  .qacts{gap:11px}
  .qacts .qact-go{max-width:none;height:50px}
  .qact-rest{display:grid;grid-template-columns:1fr 1fr;gap:9px;align-items:stretch}
  .qact-rest>.btn{width:100%;padding:0 10px;font-size:13.5px;gap:7px;height:44px;min-width:0}
  .qact-rest>.btn .i{width:16px;height:16px}
  /* an odd count would leave a half-width button stranded beside an empty cell */
  .qact-rest>.btn:last-child:nth-child(odd){grid-column:1/-1}
}
@media (max-width:399.98px){
  .qact-rest{grid-template-columns:1fr}
  .qact-rest>.btn{grid-column:1/-1}
}
/* a status line on the results page, not a nag: quiet surface, no accent fill,
   and it stacks rather than squeezes when the row runs out of room */
.lessonnote{display:flex;align-items:center;gap:13px;text-align:left;padding:13px 14px;
  border-radius:16px;background:var(--surface-2);border:1px solid var(--line);
  animation:fadeUp .5s var(--ease) both}
.lessonnote .ni{width:36px;height:36px;flex:0 0 auto;border-radius:12px;display:grid;place-items:center;
  background:var(--brand-soft);color:var(--brand)}
.lessonnote .ni .i{width:18px;height:18px}
.lessonnote b{display:block;font-family:var(--f-dis);font-weight:700;font-size:13.6px;letter-spacing:-.014em}
.lessonnote span.grow,.lessonnote .grow>span{display:block;font-size:12.4px;color:var(--ink-3);
  line-height:1.45;margin-top:2px}
.lessonnote .btn{flex:0 0 auto}
@media (max-width:539.98px){
  .lessonnote{flex-wrap:wrap}
  .lessonnote .grow{min-width:0;flex:1 1 60%}
  .lessonnote .btn{width:100%;justify-content:center}
}
.rvlist{display:flex;flex-direction:column;gap:9px;margin-top:18px;text-align:left}
.rv{display:flex;gap:12px;align-items:flex-start;padding:14px;border-radius:16px;background:var(--surface-2);
  border:1px solid var(--line)}
.rv .rvi{width:26px;height:26px;flex:0 0 auto;border-radius:8px;display:grid;place-items:center;color:#fff}
.rv.ok .rvi{background:var(--ok)}.rv.no .rvi{background:var(--no)}
.rv .rvi .i{width:14px;height:14px}
.rv b{font-size:13.8px;display:block;line-height:1.45}
.rv p{font-size:12.8px;color:var(--ink-3);margin-top:4px;line-height:1.5}
.rv .ans{font-size:12.8px;color:var(--ok);font-weight:700;margin-top:3px}


/* ============================================================================
   FLASHCARDS — the reveal is its own deliberate step
   ========================================================================== */
.fcstage{max-width:600px;margin:0 auto;perspective:1600px;-webkit-perspective:1600px}
.fcdots{display:flex;gap:5px;justify-content:center;margin-bottom:18px;flex-wrap:wrap}
/* The transition only means something because paintDots keeps these elements
   and sets className on them. It used to rebuild the row with innerHTML on
   every card, so each dot was a brand-new element with no previous value to
   move from and the rule did nothing at all. */
.fcdots i{width:22px;height:4px;border-radius:99px;background:var(--ring-track);transition:all .4s var(--ease)}
.fcdots i.on{background:var(--grad-brand);width:30px}
.fcdots i.past{background:var(--ok)}
/* The wrapper handles entering and leaving; the card itself only ever rotates.
   Keeping the two transforms on separate elements is what stops the flip from
   fighting the slide and flashing both faces mid-animation. */
.fcslide{position:relative;will-change:transform,opacity}
.fcslide.in-right{animation:slideInR .42s var(--ease) both}
.fcslide.in-left{animation:slideInL .42s var(--ease) both}
.fcslide.out-left{animation:slideOutL .26s var(--ease-io) both}
@keyframes slideInR{from{opacity:0;transform:translateX(38px) scale(.965)}to{opacity:1;transform:none}}
@keyframes slideInL{from{opacity:0;transform:translateX(-38px) scale(.965)}to{opacity:1;transform:none}}
@keyframes slideOutL{from{opacity:1;transform:none}to{opacity:0;transform:translateX(-34px) scale(.96)}}

.flip{position:relative;width:100%;aspect-ratio:1/1.06;max-height:66vh;
  transform-style:preserve-3d;-webkit-transform-style:preserve-3d;
  transition:transform .8s cubic-bezier(.34,.02,.2,1);cursor:pointer;will-change:transform}
@media (min-width:640px){.flip{aspect-ratio:1.32/1}}
.flip.on{transform:rotateY(180deg)}
.flip:focus-visible{outline:none}
.flip:focus-visible .face.front{outline:2.5px solid var(--focus);outline-offset:3px}
.face{position:absolute;inset:0;backface-visibility:hidden;-webkit-backface-visibility:hidden;
  border-radius:var(--r-2xl);padding:clamp(22px,4vw,34px);display:flex;flex-direction:column;
  box-shadow:var(--sh-3)}
/* No overflow:hidden here, deliberately. Clipping a backface-hidden element
   inside a preserve-3d parent makes WebKit rasterise it into its own surface
   and then compose that surface without re-applying backface-visibility — the
   hidden face comes back as a mirrored ghost under the one you are meant to be
   reading. It reproduces on every iPhone browser (they are all WebKit) and on
   none of the Blink ones, which is exactly the split reported. The rounded
   corners never needed it: a background is clipped by border-radius on its own,
   the sheen inherits the radius, and the only child that could ever outgrow the
   card clips itself instead. */
.face .fbody{overflow:hidden}
/* backface-visibility:hidden reliably stops the hidden face from being PAINTED,
   but hit-testing a 3D-rotated element is a separate code path in every renderer
   and is not consistently excluded the same way — on some engines the hidden
   front face keeps intercepting taps after the flip, which is why "Hear it"
   could sit directly under a real finger and still never fire. Segregating
   pointer-events explicitly removes the ambiguity: whichever face is not the
   one currently facing the viewer is inert, full stop, regardless of how any
   given browser handles backface hit-testing. */
.face.front{pointer-events:auto}
.face.back{pointer-events:none}
.flip.on .face.front{pointer-events:none}
.flip.on .face.back{pointer-events:auto}
/* And the same reasoning applied to painting. Rather than trust that every
   engine honours backface-visibility, the face that is turned away is also
   simply not drawn. The swap is a hard cut, timed to the instant the card
   passes edge-on: the flip runs .8s on cubic-bezier(.34,.02,.2,1), which is
   front-loaded, so 90deg falls at .2594s rather than halfway. At that moment
   the card is exactly perpendicular and one pixel wide, so nothing about the
   cut is visible — and with the turned-away face genuinely absent there is no
   ghost left for any renderer to mirror. Where backface-visibility already
   works this changes nothing; where it does not, it is the whole fix. */
.face{opacity:1;transition:opacity 0s linear .2594s}
.face.back{opacity:0}
.flip.on .face.front{opacity:0}
.flip.on .face.back{opacity:1}
/* Reduced motion collapses transition-duration but says nothing about delay, so
   the rotation would finish instantly while the faces waited a quarter second
   to swap — the exact ghost this is here to prevent. With no animation to stay
   in step with, the cut belongs at zero. */
:root[data-motion="off"] .face{transition-delay:0s}
@media (prefers-reduced-motion:reduce){.face{transition-delay:0s}}
.face.front{background:var(--surface);border:1px solid var(--line)}
:root[data-theme="dark"] .face.front{background:linear-gradient(158deg,#1B2149,#0D1130)}
.face.back{transform:rotateY(180deg);background:linear-gradient(140deg,var(--a1),var(--a2) 55%,var(--a3));
  color:#fff;border:1px solid transparent}
.face.back::after{content:"";position:absolute;inset:0;border-radius:inherit;
  background:radial-gradient(420px circle at 78% 12%,rgba(255,255,255,.24),transparent 60%);pointer-events:none}
.face .fk{font-size:11px;font-weight:800;font-family:var(--f-dis);letter-spacing:.14em;text-transform:uppercase;
  display:flex;align-items:center;gap:7px}
.face.front .fk{color:var(--brand)}
.face.back .fk{color:rgba(255,255,255,.8)}
.face .fk .i{width:14px;height:14px}
.face .fbody{flex:1 1 auto;display:flex;flex-direction:column;justify-content:center;
  gap:14px;position:relative;z-index:1;min-height:0;overflow-y:auto;padding:8px 0}
.face .fq{font-family:var(--f-ser);font-size:clamp(23px,4.4vw,34px);line-height:1.24;letter-spacing:-.02em;
  text-wrap:balance}
.face.front .fq{color:var(--ink)}
.face .fa{font-family:var(--f-ser);font-size:clamp(24px,4.6vw,36px);line-height:1.22;letter-spacing:-.02em}
.face .fnote{font-size:13.6px;line-height:1.6;color:rgba(255,255,255,.86);max-width:52ch}
.face .ffoot{position:relative;z-index:1;display:flex;align-items:center;justify-content:space-between;gap:12px}
.face .hintflip{display:inline-flex;align-items:center;gap:8px;font-size:12.5px;font-weight:700;
  font-family:var(--f-dis);color:var(--ink-3);animation:floatY 2.6s var(--ease-io) infinite}
.face .hintflip .i{width:15px;height:15px}
.fcactions{display:flex;gap:10px;margin-top:18px;justify-content:center;flex-wrap:wrap}
.fcactions .btn{flex:1 1 150px;max-width:220px}
.lockmsg{text-align:center;font-size:12.6px;color:var(--ink-4);margin-top:12px;display:flex;
  align-items:center;justify-content:center;gap:7px}
.lockmsg .i{width:14px;height:14px}

/* ============================================================================
   PROFILE
   ========================================================================== */
.profhero{position:relative;overflow:hidden;border-radius:var(--r-2xl);padding:clamp(22px,3.4vw,34px);
  background:linear-gradient(140deg,var(--h1),var(--h2) 55%,var(--h3));color:#fff;
  box-shadow:0 26px 60px -24px rgba(20,14,70,.55)}
.profhero::after{content:"";position:absolute;inset:0;
  background:radial-gradient(520px circle at var(--mx,80%) var(--my,10%),rgba(255,255,255,.22),transparent 62%)}
.profhero>*{position:relative;z-index:1}
.profhero .ptop{display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.profhero .pname{font-family:var(--f-ser);font-size:clamp(28px,5.4vw,44px);line-height:1.04;letter-spacing:-.028em}
.profhero .ppersona{display:inline-flex;align-items:center;gap:7px;margin-top:9px;height:29px;padding:0 13px;
  border-radius:99px;background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.26);
  font-size:12px;font-weight:750;font-family:var(--f-dis)}
.profhero .ppersona .i{width:14px;height:14px}
.statgrid{display:grid;grid-template-columns:repeat(2,1fr);gap:11px;margin-top:16px}
@media (min-width:560px){.statgrid{grid-template-columns:repeat(3,1fr)}}
@media (min-width:1100px){.statgrid{grid-template-columns:repeat(6,1fr)}}
.stat{padding:15px;border-radius:var(--r-lg);background:var(--surface);border:1px solid var(--line);
  transition:transform .38s var(--spring),box-shadow .3s,border-color .25s}
:root[data-theme="dark"] .stat{background:linear-gradient(158deg,rgba(28,34,74,.66),rgba(13,17,44,.84))}
.stat:hover{transform:translateY(-4px);box-shadow:var(--sh-2);border-color:var(--brand-line)}
.stat .si{width:32px;height:32px;border-radius:11px;display:grid;place-items:center;
  background:var(--brand-soft);color:var(--brand)}
.stat .si .i{width:16px;height:16px}
.stat .sv{font-family:var(--f-dis);font-weight:800;font-size:clamp(21px,3vw,27px);letter-spacing:-.038em;
  margin-top:11px;line-height:1;font-variant-numeric:tabular-nums}
.stat .sl{font-size:11.5px;font-weight:700;color:var(--ink-3);margin-top:5px;letter-spacing:.03em}
.badgegrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(88px,1fr));gap:6px}
.posterrow{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
.posterthumb{position:relative;aspect-ratio:3/4;border-radius:var(--r-lg);overflow:hidden;
  border:1px solid var(--line);box-shadow:var(--sh-1);cursor:pointer;
  transition:transform .42s var(--spring),box-shadow .4s}
.posterthumb:hover{transform:translateY(-5px) rotate(-1.2deg);box-shadow:var(--sh-3)}
.posterthumb canvas{width:100%;height:100%;object-fit:cover}
.posterthumb .pl{position:absolute;left:0;right:0;bottom:0;padding:22px 11px 10px;color:#fff;
  background:linear-gradient(transparent,rgba(6,4,24,.82));font-size:12px;font-weight:750;
  font-family:var(--f-dis);letter-spacing:-.01em}
.posterthumb .lockw{position:absolute;inset:0;display:grid;place-items:center;background:rgba(8,6,26,.62);
  backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);color:#fff;text-align:center;padding:12px}
.posterthumb .lockw span{font-size:11.5px;font-weight:700;line-height:1.4;margin-top:7px;display:block;
  opacity:.9}

.posterview{display:flex;flex-direction:column;align-items:center;gap:16px}
.posterview .frame{width:min(320px,72vw);aspect-ratio:3/4;border-radius:var(--r-lg);overflow:hidden;
  box-shadow:var(--sh-4);animation:popIn .6s var(--spring) both}
.posterview .frame canvas{width:100%;height:100%}

/* ---------------------------------------------------------- certificate --*/
.certwrap{display:grid;gap:20px;margin-top:8px}
@media (min-width:1000px){.certwrap{grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);align-items:start}}
.certframe{border-radius:var(--r-xl);overflow:hidden;box-shadow:var(--sh-4);
  border:1px solid var(--line);background:var(--surface);animation:popIn .6s var(--spring) both}
.certframe canvas{width:100%;height:auto;display:block}
.certside{display:flex;flex-direction:column;gap:16px;min-width:0}

/* ============================================================================
   CELEBRATION
   ========================================================================== */
#celebrate{position:fixed;inset:0;z-index:450;display:grid;place-items:center;padding:22px;
  background:radial-gradient(circle at 50% 40%,
    color-mix(in srgb,var(--h2) 78%,#000 22%), color-mix(in srgb,var(--h1) 26%,#05040F 74%) 72%);
  overflow:hidden}
#celebrate canvas{position:absolute;inset:0;z-index:0}
#celebrate .cin{position:relative;z-index:2;text-align:center;color:#fff;max-width:560px;width:100%}
#celebrate .cring{width:132px;height:132px;margin:0 auto}
#celebrate .cring circle.v{stroke-dasharray:396;stroke-dashoffset:396;
  animation:draw2 1.5s var(--ease) .2s forwards}
@keyframes draw2{to{stroke-dashoffset:0}}
#celebrate .cring .cmid{position:absolute;inset:0;display:grid;place-items:center}
#celebrate .cring .cmid .i{width:52px;height:52px;color:#fff;opacity:0;
  animation:popIn .7s var(--spring) 1.1s forwards}
#celebrate .ck{font-family:var(--f-dis);font-size:11.5px;font-weight:800;letter-spacing:.28em;
  text-transform:uppercase;color:rgba(255,255,255,.66);margin-top:26px;
  animation:fadeUp .8s var(--ease) 1.3s both}
#celebrate h2{font-family:var(--f-ser);font-size:clamp(34px,7.6vw,60px);line-height:1.04;letter-spacing:-.03em;
  margin-top:10px;animation:fadeUp .9s var(--ease) 1.45s both;text-wrap:balance}
#celebrate h2 em{font-style:italic;background:linear-gradient(100deg,#FFD9F2,#BFE9FF);
  -webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent}
#celebrate .cs{font-size:15.5px;color:rgba(255,255,255,.78);margin-top:14px;line-height:1.6;
  animation:fadeUp .9s var(--ease) 1.6s both;max-width:42ch;margin-inline:auto}
#celebrate .cstats{display:flex;gap:11px;justify-content:center;margin-top:26px;flex-wrap:wrap;
  animation:fadeUp .9s var(--ease) 1.75s both}
#celebrate .cstat{padding:13px 18px;border-radius:17px;background:rgba(255,255,255,.11);
  border:1px solid rgba(255,255,255,.2);min-width:96px}
#celebrate .cstat b{display:block;font-family:var(--f-dis);font-weight:800;font-size:24px;letter-spacing:-.035em}
#celebrate .cstat span{font-size:10.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
  color:rgba(255,255,255,.66)}
#celebrate .cacts{display:flex;gap:11px;justify-content:center;margin-top:30px;flex-wrap:wrap;
  animation:fadeUp .9s var(--ease) 1.9s both}
#celebrate .btn-w{background:#fff;color:#2A1470}
#celebrate .btn-o{background:rgba(255,255,255,.12);color:#fff;border:1px solid rgba(255,255,255,.28)}
#celebrate.out{animation:fadeIn .4s var(--ease) reverse both}

/* xp burst */
.xpfly{position:fixed;z-index:420;pointer-events:none;font-family:var(--f-dis);font-weight:800;
  font-size:19px;color:var(--brand);text-shadow:0 2px 12px var(--brand-soft);
  animation:xpFly 1.25s var(--ease) forwards}
@keyframes xpFly{0%{opacity:0;transform:translateY(0) scale(.6)}
  18%{opacity:1;transform:translateY(-10px) scale(1.16)}
  100%{opacity:0;transform:translateY(-84px) scale(.94)}}

/* ============================================================================
   PIPO — the companion
   ========================================================================== */
#pipo{position:fixed;right:18px;bottom:18px;z-index:120;
  bottom:calc(18px + env(safe-area-inset-bottom))}
@media (max-width:759.98px){
  #pipo{right:14px;bottom:calc(var(--dock-h,calc(var(--tab-h) + env(safe-area-inset-bottom))) + 12px)}
}
/* PIPO YIELDS TO THE CART. He floats bottom-right at z-index 120, which is
   exactly where the mini cart puts Checkout - so on a phone the one button that
   takes money sat underneath a cartoon. Sitting him down inside the bar instead
   was tried and dropped: a 60px face in a 65px bar has 2px of air around it and
   reads as wedged in. He stands above it. */
/* When the bar is up he stands on top of it, and --mc-clear already says how
   far up the screen it reaches - so this is one line whether the cart is a bar
   resting on a dock or a card in the corner of a desktop. */
body.has-mc #pipo{bottom:calc(var(--mc-clear,0px) + 14px)}
#pipoBtn{width:60px;height:60px;border-radius:22px;position:relative;display:grid;place-items:center;
  background:linear-gradient(145deg,var(--a1),var(--a2) 55%,var(--a3));box-shadow:var(--sh-brand);
  transition:transform .45s var(--spring),box-shadow .4s;overflow:visible}
#pipoBtn:hover{transform:translateY(-4px) scale(1.06)}
#pipoBtn:active{transform:scale(.93)}
/* Pipo sits still. No breathing, no idle pulse — hover, the celebration hop and
   the blink are all deliberate reactions, and the equaliser badge plus the mouth
   carry "speaking" on their own. */
#pipoBtn.hop{animation:pipoHop .8s var(--spring)}
@keyframes pipoHop{0%,100%{transform:translateY(0) scale(1)}
  28%{transform:translateY(-16px) scale(1.09)}52%{transform:translateY(0) scale(.94)}
  74%{transform:translateY(-6px) scale(1.03)}}
#pipoBtn .aur{position:absolute;inset:-7px;border-radius:26px;background:inherit;filter:blur(13px);
  opacity:.5;z-index:-1}
#pipoBtn svg{width:40px;height:40px;position:relative;z-index:1;overflow:visible}
#pipoBtn .eye{transition:transform .16s linear}
#pipoBtn .lid{transform-origin:center;transform:scaleY(0)}
#pipoBtn.blink .lid{animation:blinkLid .2s ease}
@keyframes blinkLid{0%,100%{transform:scaleY(0)}50%{transform:scaleY(1)}}
#pipoBtn .sparks{position:absolute;inset:-16px;pointer-events:none;opacity:0}
#pipoBtn.celebrate .sparks{opacity:1;animation:fadeIn .2s}
#pipoBtn .sparks i{position:absolute;width:5px;height:5px;border-radius:50%;background:#fff;
  animation:sparkOut .9s var(--ease) forwards}
@keyframes sparkOut{0%{opacity:1;transform:translate(0,0) scale(1)}
  100%{opacity:0;transform:translate(var(--dx),var(--dy)) scale(0)}}
#pipoDot{position:absolute;top:-2px;right:-2px;width:14px;height:14px;border-radius:50%;
  background:var(--a3);border:2.5px solid var(--bg);opacity:0;transform:scale(0);
  transition:all .35s var(--spring)}
#pipoDot.on{opacity:1;transform:scale(1)}

/* ------------------------------------------------------- Pipo's voice ------
   Reading aloud and Tap a word are Pipo doing the talking, not a separate audio
   feature running beside it. The signal is the equaliser badge that used to sit
   in the reading player — the same rhythm, moved to the thing actually making
   the sound — plus the mouth cycling through its talk shapes. No halo, and the
   button itself never scales: the badge and the mouth are the whole signal. */
#pipoBtn #pipoMouth{transition:d .13s linear}

.pipowave{position:absolute;top:-8px;right:-9px;z-index:2;display:none;align-items:center;
  gap:2.5px;height:21px;padding:0 7px;border-radius:99px;
  background:var(--surface);border:1.5px solid var(--line);box-shadow:var(--sh-1)}
:root[data-theme="dark"] .pipowave{background:var(--surface-2)}
.pipowave i{width:2.5px;height:5px;border-radius:2px;background:var(--brand);
  animation:wv .9s ease-in-out infinite}
/* three bars, so the delays spread across the same beat rather than the first
   three of a five-bar stagger — otherwise the wave would look truncated */
.pipowave i:nth-child(2){animation-delay:.18s}.pipowave i:nth-child(3){animation-delay:.36s}
@keyframes wv{0%,100%{height:5px}50%{height:15px}}
#pipoBtn.speaking .pipowave,#pipoBtn.listening .pipowave{display:flex;animation:popIn .3s var(--spring)}
/* listening is the same badge held steady — awake and waiting, not mid-sentence */
#pipoBtn.listening .pipowave i{animation:wvIdle 2.1s var(--ease-io) infinite;animation-delay:0s}
@keyframes wvIdle{0%,100%{height:5px;opacity:.5}50%{height:9px;opacity:1}}
/* one badge in that corner at a time */
#pipoBtn.speaking #pipoDot,#pipoBtn.listening #pipoDot{opacity:0;transform:scale(0)}
:root[data-motion="off"] .pipowave i{animation:none;height:11px}

/* the bubble needs its own containing block — inside #pipo it would be clamped
   to the 60px button and collapse to a sliver */
#pipoBubbleHost{position:fixed;right:92px;bottom:calc(26px + env(safe-area-inset-bottom));
  width:min(272px,calc(100vw - 130px));display:flex;justify-content:flex-end;
  pointer-events:none;z-index:119}
@media (max-width:759.98px){
  #pipoBubbleHost{right:88px;bottom:calc(var(--tab-h) + env(safe-area-inset-bottom) + 20px)}
}
@media (max-width:479.98px){
  #pipoBubbleHost{right:14px;left:auto;width:min(280px,74vw);
    bottom:calc(var(--tab-h) + env(safe-area-inset-bottom) + 84px)}
}
/* WITH THE CART UP, EVERYTHING RISES BY THE HEIGHT OF THE CART. These are all
   measured off the bottom of the screen and the bar is at the bottom of the
   screen, so without this the bubble opened across it - its far end over the
   left of Checkout, at z-index 119 against the bar's 70, swallowing taps meant
   for the till. --mc-clear is the bar's reach and is 0 when it is down, so each
   line below is simply the position it always had plus whatever now sits under
   it. The bubble still opens to Pipo's left; it is only higher. */
body.has-mc #pipoBubbleHost{bottom:calc(var(--mc-clear,0px) + 26px)}
@media (max-width:759.98px){
  body.has-mc #pipoBubbleHost{bottom:calc(var(--mc-clear,0px) + 20px)}
}
@media (max-width:479.98px){
  body.has-mc #pipoBubbleHost{bottom:calc(var(--mc-clear,0px) + 84px)}
}
#pipoBubble{position:relative;max-width:100%;padding:12px 34px 12px 15px;pointer-events:auto;
  border-radius:18px 18px 4px 18px;background:var(--surface);border:1px solid var(--line);
  box-shadow:var(--sh-3);font-size:13.2px;line-height:1.5;color:var(--ink);
  animation:bubIn .5s var(--spring) both;transform-origin:bottom right}
:root[data-theme="dark"] #pipoBubble{background:linear-gradient(150deg,#1D2450,#111535)}
@keyframes bubIn{from{opacity:0;transform:scale(.7) translateX(14px)}to{opacity:1;transform:none}}
#pipoBubble.out{animation:bubIn .3s var(--ease) reverse both}
#pipoBubble .bx{position:absolute;top:5px;right:5px;width:22px;height:22px;border-radius:7px;display:grid;
  place-items:center;color:var(--ink-4);transition:all .2s}
#pipoBubble .bx:hover{background:var(--surface-3);color:var(--ink)}
#pipoBubble .bx .i{width:12px;height:12px}
@media (max-width:479.98px){#pipoBubble{border-radius:18px 18px 18px 4px}}

#pipoPanel{position:fixed;right:18px;bottom:88px;z-index:130;width:min(384px,calc(100vw - 32px));
  height:min(548px,calc(100dvh - 150px));display:flex;flex-direction:column;overflow:hidden;
  border-radius:var(--r-2xl);background:var(--surface);border:1px solid var(--line);
  box-shadow:var(--sh-4);animation:panelIn .5s var(--spring) both;transform-origin:bottom right}
:root[data-theme="dark"] #pipoPanel{background:linear-gradient(165deg,#161C42,#0B0E2A)}
@keyframes panelIn{from{opacity:0;transform:translateY(20px) scale(.9)}to{opacity:1;transform:none}}
#pipoPanel.out{animation:panelIn .28s var(--ease) reverse both}
@media (max-width:559.98px){
  #pipoPanel{right:0;left:0;bottom:0;width:100%;height:min(86dvh,660px);
    border-radius:var(--r-2xl) var(--r-2xl) 0 0;animation:sheetIn .45s var(--spring) both}
  #pipoPanel.out{animation:sheetIn .26s var(--ease) reverse both}
}
.pipohead{display:flex;align-items:center;gap:11px;padding:14px 15px;border-bottom:1px solid var(--line);
  background:var(--surface-2);flex:0 0 auto}
:root[data-theme="dark"] .pipohead{background:rgba(255,255,255,.03)}
.pipohead .pav{width:38px;height:38px;border-radius:13px;display:grid;place-items:center;
  background:linear-gradient(145deg,var(--a1),var(--a3));box-shadow:var(--sh-brand)}
.pipohead .pav svg{width:24px;height:24px}
.pipohead .ph b{display:block;font-family:var(--f-dis);font-weight:750;font-size:14.5px;letter-spacing:-.016em}
.pipohead .ph span{font-size:11.5px;color:var(--ok);font-weight:650;display:flex;
  align-items:center;gap:5px}
.pipohead .ph span::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--ok);
  animation:breathe 2s infinite}
.pipomsgs{flex:1 1 auto;overflow-y:auto;padding:15px;display:flex;flex-direction:column;gap:11px;
  scroll-behavior:smooth}
.pm{max-width:86%;padding:11px 14px;border-radius:17px;font-size:13.6px;line-height:1.56;
  animation:fadeUp .4s var(--ease) both;word-wrap:break-word;overflow-wrap:anywhere}
.pm.bot{align-self:flex-start;background:var(--surface-2);border:1px solid var(--line);
  border-bottom-left-radius:5px;color:var(--ink)}
:root[data-theme="dark"] .pm.bot{background:rgba(255,255,255,.05)}
.pm.me{align-self:flex-end;background:var(--grad-brand);color:#fff;border-bottom-right-radius:5px;
  box-shadow:var(--sh-brand)}
.pm b{font-weight:750}
/* a word Pipo pronounced, with a quiet speaker to replay it */
.pm .sayw{display:inline-flex;align-items:center;gap:5px;padding:2px 4px 2px 8px;border-radius:99px;
  background:var(--brand-soft);border:1px solid var(--brand-line);vertical-align:baseline;
  max-width:100%;line-height:1.35}
.pm .sayw b{font-weight:750;color:var(--brand);overflow-wrap:anywhere}
.pm.me .sayw{background:rgba(255,255,255,.2);border-color:rgba(255,255,255,.3)}
.pm.me .sayw b{color:#fff}
.sayw-b{width:22px;height:22px;flex:0 0 auto;border-radius:50%;display:grid;place-items:center;
  color:var(--brand);background:transparent;
  transition:background .22s,transform .3s var(--spring),color .2s}
.sayw-b .i{width:13px;height:13px}
.sayw-b:hover{background:var(--brand);color:#fff;transform:scale(1.1)}
.sayw-b:active{transform:scale(.9)}
.sayw-b.saying{background:var(--brand);color:#fff;animation:breathe 1.1s var(--ease-io) infinite}
.pm.me .sayw-b{color:#fff}
.pm.me .sayw-b:hover{background:rgba(255,255,255,.3)}

.pm .qref{display:inline-flex;align-items:center;gap:6px;margin-top:9px;padding:6px 11px;border-radius:99px;
  background:var(--brand-soft);color:var(--brand);font-size:11.8px;font-weight:750;font-family:var(--f-dis);
  transition:all .25s}
.pm .qref:hover{background:var(--brand);color:#fff}
.pm .qref .i{width:12px;height:12px}
.pm.typing{display:flex;gap:4px;padding:14px 15px;align-items:center}
.pm.typing i{width:6px;height:6px;border-radius:50%;background:var(--ink-4);animation:tdot 1.1s infinite}
.pm.typing i:nth-child(2){animation-delay:.16s}.pm.typing i:nth-child(3){animation-delay:.32s}
@keyframes tdot{0%,60%,100%{opacity:.3;transform:translateY(0)}30%{opacity:1;transform:translateY(-4px)}}
.pipochips{display:flex;gap:7px;padding:0 15px 11px;flex-wrap:wrap;flex:0 0 auto}
.pipochip{height:31px;padding:0 12px;border-radius:99px;background:var(--surface-2);border:1px solid var(--line);
  font-size:12px;font-weight:700;color:var(--ink-2);font-family:var(--f-dis);
  transition:all .3s var(--spring);white-space:nowrap}
.pipochip:hover{border-color:var(--brand-line);color:var(--brand);transform:translateY(-2px)}
.pipoform{display:flex;gap:9px;padding:12px 14px calc(12px + env(safe-area-inset-bottom));
  border-top:1px solid var(--line);flex:0 0 auto;align-items:flex-end}
.pipoform textarea{flex:1 1 auto;height:42px;max-height:110px;padding:10px 14px;border-radius:14px;
  background:var(--surface-2);border:1.5px solid var(--line);resize:none;font-size:14px;line-height:1.4;
  transition:border-color .25s,box-shadow .25s}
.pipoform textarea:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 3.5px var(--brand-soft)}
.pipoform .send{width:42px;height:42px;flex:0 0 auto;border-radius:14px;display:grid;place-items:center;
  background:var(--grad-brand);color:#fff;box-shadow:var(--sh-brand);
  transition:transform .35s var(--spring),opacity .25s}
.pipoform .send:hover{transform:scale(1.07)}
.pipoform .send:disabled{opacity:.4;pointer-events:none}

/* ---------------------------------------------------------------- empties -*/
.empty{text-align:center;padding:52px 20px}
.empty .ei{width:64px;height:64px;margin:0 auto 16px;border-radius:22px;display:grid;place-items:center;
  background:var(--brand-soft);color:var(--brand)}
.empty .ei .i{width:29px;height:29px}
.empty h3{font-size:18px;letter-spacing:-.024em}
.empty p{font-size:14px;color:var(--ink-3);margin-top:7px;max-width:38ch;margin-inline:auto;line-height:1.6}

/* print fallback for PDF */
@media print{
  #rail,#topbar,#dock,#pipo,#pipoPanel,#aura,#grain,.sitefoot,#toaster{display:none !important}
  #main{margin:0}
  body{background:#fff;color:#000}
}


/* ============================================================================
   DASHBOARD COLUMN SYSTEM — a different arrangement on each device, not one
   layout squeezed through breakpoints.
   ========================================================================== */
.dash2{display:grid;gap:16px;margin-top:16px}
.dcol{display:flex;flex-direction:column;gap:16px;min-width:0}
/* mobile: one honest column, ordered by what matters on a small screen */
@media (max-width:759.98px){
  .dash2{grid-template-columns:1fr}
  .dcol{gap:14px}
}
/* tablet: single stack, but the side rail becomes a two-up card grid */
@media (min-width:760px) and (max-width:1099.98px){
  .dash2{grid-template-columns:1fr;gap:18px}
  .dcol{gap:18px}
  .dash2>.dcol:last-child{display:grid;grid-template-columns:1fr 1fr;align-items:start}
  .dash2>.dcol:last-child>*:nth-child(3){grid-column:span 2}
  #dMain,#pMain{display:grid;grid-template-columns:1fr 1fr;align-items:start}
  #dMain>*:first-child,#pMain>*:first-child{grid-column:span 2}
}
/* desktop: true two-column composition */
@media (min-width:1100px){
  .dash2{grid-template-columns:minmax(0,1.58fr) minmax(0,1fr);gap:20px;align-items:start}
  .dcol{gap:20px}
}

/* icon size helpers used inline */
.i22{width:22px;height:22px;flex:0 0 auto}
.i32{width:32px;height:32px;flex:0 0 auto}
.i-14{width:14px;height:14px;flex:0 0 auto}

/* per-sentence speak affordance */
.sline{position:relative;padding-right:30px}
.sline .stx{flex:1 1 auto;min-width:0}
/* A template a student can open and keep. Shut by default so it reads as one
   quiet line in the lesson rather than a wall of text. */
.tmpl{border:1px solid var(--line);border-radius:14px;margin:14px 0;overflow:hidden;
  background:var(--surface-2,var(--surface))}
.tmpl-h{display:flex;align-items:center;gap:9px;padding:11px 13px}
.tmpl-h .i{width:16px;height:16px;color:var(--brand);flex:0 0 auto}
.tmpl-n{font-weight:600;flex:1 1 auto;min-width:0}
.tmpl-b{padding:0 13px 13px}
.tmpl-t{margin:0;padding:12px;border-radius:10px;background:var(--bg);
  border:1px solid var(--line);white-space:pre-wrap;word-break:break-word;
  font:500 13.5px/1.65 var(--mono,ui-monospace,SFMono-Regular,Menlo,monospace)}
.tmpl-h .tmpl-cp{flex:0 0 auto;margin-left:auto}
/* SAY IT - the block with the microphones. Built to look like the template
   card, because it is the same kind of thing: something the lesson hands you
   rather than something it says. */
.sayb{border:1px solid var(--line);border-radius:14px;margin:14px 0;overflow:hidden;
  background:var(--surface-2,var(--surface))}
.say-b{padding:0 13px 13px}
/* No header above it, so the body carries its own top padding. */
.say-b.bare{padding:13px}
.say-note{margin:0 0 11px;color:var(--ink-3);font-size:13.4px;line-height:1.55}
/* The one line a browser without a microphone gets. Smallest type on the card
   and no colour of its own, because it is an explanation and not a warning -
   nothing here has gone wrong. */
.say-why{display:flex;gap:7px;align-items:flex-start;margin:0 0 11px;
  color:var(--ink-4);font-size:12.4px;line-height:1.5}
.say-why .i{width:14px;height:14px;flex:0 0 auto;margin-top:2px}
/* The way back, after the microphone has been refused. A link rather than a
   button, because it undoes something rather than doing something. */
.say-again{background:none;border:0;padding:0;font:inherit;font-size:13.4px;
  color:var(--brand);font-weight:650;cursor:pointer;text-decoration:underline;
  text-underline-offset:2px}
/* Button and line on one row, at every width. The button is a column the eye
   can follow; the sentence starts where the sentence above it started. */
.say-row{display:flex;align-items:center;gap:11px;padding:8px 0;
  border-top:1px solid var(--line)}
.say-row:first-of-type{border-top:0}
/* A BUTTON WITH A WORD ON IT. It used to be a grey icon square, which sat in
   the same place as the plain speaker mark the no-microphone version shows -
   so the one thing on the card that could be tapped looked exactly like the
   thing that could not. Brand colour and the word Speak, because this is the
   one action the card exists for. */
.say-go{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
  gap:6px;width:82px;height:34px;padding:0;border-radius:10px;white-space:nowrap;
  background:var(--brand-soft);border:1px solid var(--brand);color:var(--brand);
  font:inherit;font-size:12.5px;font-weight:650;letter-spacing:-.01em;
  cursor:pointer;transition:box-shadow .18s,transform .12s}
/* A FIXED WIDTH, so Speak, Listening and Again do not make the column jump
   about as somebody works down the card. Listening is the longest of the
   three and it sets the number. */
.say-go .i{width:15px;height:15px}
.say-go:hover{box-shadow:var(--sh-brand,0 4px 14px rgba(0,0,0,.10))}
.say-go:active{transform:scale(.97)}
/* No microphone here: a quiet mark instead of a button, so the row still reads
   as a line to be said rather than a control that failed to load. */
.say-dot{flex:0 0 auto;width:40px;height:40px;display:grid;place-items:center;
  color:var(--ink-4)}
.say-dot .i{width:16px;height:16px}
.say-row.plain{align-items:center}
.say-t{min-width:0;flex:1 1 auto}
.say-line{display:block;font-weight:600;line-height:1.45;font-size:14px}
.say-heard{display:flex;gap:5px;align-items:baseline;margin-top:3px;
  font-size:12.8px;color:var(--ink-4);line-height:1.5;overflow-wrap:anywhere}
.say-heard:empty{display:none}
.say-heard .i{width:13px;height:13px;flex:0 0 auto;transform:translateY(2px)}
/* Listening: the button pulses so it is obvious the microphone is open, which
   matters more than it looks - nobody should have to wonder. */
/* Listening: it pulses, so there is never a doubt that the microphone is
   open. Smaller than it was, because the button is wider now and a big pulse
   on a wide element reads as the page breathing. */
.say-row.hearing .say-go{animation:sayPulse 1.2s ease-in-out infinite}
@keyframes sayPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.03)}}

/* Narrow screens keep the same shape and simply give the button less room.
   Stacking it under the line was tried and measured: four outlined boxes down
   the page, rows of ninety and a hundred and fourteen pixels, and a card half
   as tall again as the one it replaced. */
@media (max-width:560px){
  .say-go{width:74px;height:32px;font-size:12px}
  .say-go .i{width:14px;height:14px}
  .say-line{font-size:13.6px}
}
/* Green, and the same green as a right answer everywhere else in this
   product. The button carries it too, so the column of buttons doubles as a
   list of what is left to do. */
.say-row.ok .say-go{background:var(--ok-soft);border-color:var(--ok);color:var(--ok)}
.say-row.ok .say-heard{color:var(--ok);font-weight:600}
/* AND RED WHEN IT DID NOT, the owner's decision and the right one. This was
   grey, on the reasoning that the recogniser is generous with accents and
   strict with noise - but a student looking at four lines needs to see which
   ones are still to do, and grey against green reads as not-yet-tried rather
   than try-again. The same --no the quiz uses, so one colour means one thing
   everywhere in the product.

   What keeps it honest is the wording rather than the colour: the line still
   says what was heard and which words were missing, so anybody can see whether
   it was the sentence or the room that failed. */
.say-row.miss .say-go{background:var(--no-soft);border-color:var(--no);color:var(--no)}
.say-row.miss .say-heard{color:var(--no);font-weight:600}
/* A booking line: prose with a link in it, at the end of a section. Not a card
   and not a colour - a box in the middle of a lesson reads as an advert, and
   somebody re-reading lesson nine for the fourth time should not have to see
   one. It is hidden until the server says the session is real. */
.bookline{margin:16px 0 0;color:var(--ink-3);font-size:14.6px;line-height:1.7}
.bookline[hidden]{display:none}
.bookline .bl-go{color:var(--brand);font-weight:700;text-decoration:none;
  border-bottom:1.5px solid var(--brand-line)}
.bookline .bl-go:hover{border-bottom-color:var(--brand)}
.bookline .bl-meta{color:var(--ink-4)}
/* 32px, and not 26 shrunk to 21. It is an extra rather than a control, but a
   thing worth tapping is a thing worth being able to tap. */
.sayline{position:absolute;right:0;top:50%;transform:translateY(-50%) scale(.86);
  width:32px;height:32px;border-radius:10px;display:grid;place-items:center;color:var(--ink-4);
  opacity:0;transition:opacity .22s,color .2s,background .2s,transform .3s var(--spring)}
.sayline .i{width:14px;height:14px}
.sline:hover .sayline,.sline:focus-within .sayline{opacity:1;transform:translateY(-50%) scale(1)}
.sayline:hover{color:var(--brand);background:var(--brand-soft)}
.sline.saying{background:var(--brand-soft);border-radius:8px}
@media (hover:none){ .sayline{opacity:.55;transform:translateY(-50%) scale(1)} }

/* tap-to-pronounce mode */
.lessonwrap.pron .prose,.lessonwrap.pron .lhero .stx{cursor:pointer}
.lessonwrap.pron .prose p:hover,.lessonwrap.pron .prose li:hover,
.lessonwrap.pron .prose .stx:hover{background:var(--brand-soft);border-radius:6px}
.lessonwrap.pron::after{content:"";position:fixed;inset:auto 0 0 0;height:2px;background:var(--grad-brand);
  z-index:30;animation:fadeIn .3s}

/* quiz hub visual balance on tablet */
@media (min-width:760px) and (max-width:1099.98px){
  .fmtgrid{grid-template-columns:repeat(2,1fr)}
}
/* profile settings columns */
@media (min-width:760px) and (max-width:1099.98px){
  #setA,#setB{display:flex}
}

/* reader typography scales with the device, never below comfortable */
@media (min-width:1100px){ .prose{font-size:16.8px;line-height:1.8} }
@media (max-width:759.98px){
  :root{--sec-gap:13px}
  .prose{font-size:16px;line-height:1.75}
  .paper{padding:19px 17px;border-radius:var(--r-xl)}
  .statgrid{gap:9px}
  .stat{padding:13px}
  .qa{gap:9px}
}

/* onboarding on small screens */
@media (max-width:559.98px){
  #onb{padding:20px 18px}
  #onb .ob-mark{width:68px;height:68px;border-radius:22px}
  #onb .ob-mark svg{width:36px;height:36px}
  #onb .ob-brand{font-size:11.5px;letter-spacing:.28em;margin-top:18px}
  #onb .ob-opts{margin-top:28px}
}

/* THE WELCOME SITS A LITTLE ABOVE THE MIDDLE, not on it.
   Owner, on her Galaxy S23 FE, 13 September 2026: "it kind of looks like the
   blank area below is less and top is more blank area." It was first put down
   to 100dvh, and her screenshot does not support that: read again, the content
   sits roughly in the middle of the visible page, with no URL-bar-sized gap
   anywhere. What it does show is the ordinary optical effect -- a block set on
   the middle of a tall screen reads as sitting low, which is why title screens
   are set above centre. Nobody has confirmed this on her phone yet.
   HOW FAR. The bottom padding becomes 20px + 8vh, which moves the middle up by
   half of what it adds. A phone's padding is 20px all round, so there it is 4vh:
   34px on a screen 844px tall. A laptop's is 24px, so there it is 4vh less 2px:
   34px on a screen 900px tall. (This said "4vh, 36px on a laptop" until
   14 September 2026 -- the 20px was the phone's number.)
   "HELLO, <name>" GOES UP BY THE SAME AMOUNT. It is drawn on its own layer,
   .ob-final, fixed to the screen with 26px of padding, so the lift above never
   reached it and the greeting landed lower than the welcome it replaced. Its
   bottom padding is set to move it exactly as far: 26 + 8vh against 26 on a
   phone, 22 + 8vh against 26 on a laptop.
   AT 700px OF HEIGHT AND UP ONLY. On a shorter screen the padding would come
   out of the room the content needs. Placed after the small-screen block above
   so it wins there too.
   AND NOT WHEN THE CONTENT DOES NOT FIT (.ob-nolift, set by startOnboarding()).
   With larger text on a laptop the lift pushed the welcome into scrolling on a
   screen that had room for all of it centred -- 1280x720 at 130%. */
@media (min-height:700px){
  #onb:not(.ob-nolift){padding-bottom:calc(20px + 8vh)}
  #onb:not(.ob-nolift) .ob-final{padding-bottom:calc(22px + 8vh)}
}
@media (min-height:700px) and (max-width:559.98px){
  #onb:not(.ob-nolift) .ob-final{padding-bottom:calc(26px + 8vh)}
}

/* the hero never lets its greeting collide with the stats on short screens */
@media (max-height:620px) and (max-width:759.98px){
  .hero{padding:18px}
  .hero .greet{font-size:30px;margin-top:12px}
  .heroStats{margin-top:18px}
}

/* wide desktop: give the hero more presence */
@media (min-width:1400px){
  .hero{padding:48px}
  .hero .greet{font-size:66px}
}


/* ============================================================================
   BOOT SKELETON
   The split build paints from cache and then waits for the course content to
   arrive. That gap is short, but a blank page for even a few hundred
   milliseconds reads as a broken app, so the shape of the product goes up
   immediately and the real thing replaces it.

   It draws the whole screen, chrome included, because #app is display:none
   until the app starts — the rail and the top bar are not there to be seen yet.
   Deliberately a silhouette and not a spinner: the student sees where things
   will be before they are there.

   The standalone build has nothing to wait for and never shows this.
   ========================================================================== */
#boot{
  position:fixed; inset:0; z-index:3;
  display:flex;
  background:var(--bg);
  animation:bootIn .28s var(--ease) both;
}
#boot[hidden]{display:none}

/* one shimmering surface, reused at every size */
.bk{
  position:relative; overflow:hidden;
  border-radius:var(--r-lg);
  background:var(--surface-2);
  border:1px solid var(--line);
}
.bk::after{
  content:''; position:absolute; inset:0;
  transform:translateX(-100%);
  background:linear-gradient(90deg,transparent,var(--surface-3),transparent);
  animation:bootSheen 1.25s var(--ease) infinite;
}

/* ---- the rail, desktop only ---------------------------------------------- */
.bk-rail{
  flex:0 0 var(--nav-w); display:none;
  flex-direction:column; gap:12px;
  padding:22px 20px;
  border-right:1px solid var(--line);
}
.bk-brand{height:46px; border-radius:var(--r-md)}
.bk-rail .bk-nav{height:44px; border-radius:var(--r-md)}
.bk-rail .bk-spacer{flex:1 1 auto}
.bk-rail .bk-card{height:104px}

/* ---- top bar + content --------------------------------------------------- */
.bk-main{flex:1 1 auto; min-width:0; display:flex; flex-direction:column}
.bk-top{
  height:var(--top-h); flex:0 0 auto;
  display:flex; align-items:center; justify-content:flex-end; gap:10px;
  padding:0 clamp(14px,3vw,28px);
  border-bottom:1px solid var(--line-soft);
}
.bk-top .bk{height:36px; width:62px; border-radius:var(--r-full)}
.bk-top .bk-av{width:38px}

.bk-body{
  flex:1 1 auto; min-height:0;
  display:flex; flex-direction:column; gap:18px;
  padding:24px clamp(14px,3vw,28px);
  max-width:var(--maxw); width:100%;
}
.bk-hero{height:210px; flex:0 0 auto}
.bk-row{display:flex; gap:16px; flex:0 0 auto}
.bk-row .bk{flex:1 1 0; height:120px}
.bk-wide{height:150px; flex:0 0 auto}

/* the dock takes the rail's place on a phone */
.bk-dock{
  position:fixed; left:0; right:0; bottom:0; display:none;
  height:calc(var(--tab-h) + env(safe-area-inset-bottom));
  padding:9px 14px calc(9px + env(safe-area-inset-bottom));
  gap:10px;
  border-top:1px solid var(--line-soft);
  background:var(--surface);
}
.bk-dock .bk{flex:1 1 0; border-radius:var(--r-md)}

@media (min-width:1100px){
  .bk-rail{display:flex}
  .bk-hero{height:240px}
}
@media (max-width:759.98px){
  .bk-body{padding:16px; gap:14px}
  .bk-hero{height:180px}
  .bk-row .bk{height:96px}
  .bk-top .bk{width:52px}
  .bk-dock{display:flex}
}

@keyframes bootIn{from{opacity:0}to{opacity:1}}
@keyframes bootSheen{to{transform:translateX(100%)}}

/* ---- content could not be fetched ---------------------------------------- */
/* Says what happened and offers the one action that helps, rather than leaving
   a half-painted shell up forever. */
#bootErr{
  position:fixed; inset:0; z-index:4;
  display:flex; align-items:center; justify-content:center; padding:24px;
  background:var(--bg);
}
#bootErr[hidden]{display:none}
#bootErr .be-card{
  max-width:420px; text-align:center;
  padding:32px 28px;
  border-radius:var(--r-lg);
  background:var(--surface);
  border:1px solid var(--line);
  box-shadow:var(--sh-2);
}
#bootErr h2{
  font-family:var(--f-dis); font-weight:800; font-size:20px;
  letter-spacing:-.02em; color:var(--ink); margin:0 0 8px;
}
#bootErr p{
  font-size:14.5px; line-height:1.6; color:var(--ink-3); margin:0 0 20px;
}
#bootErr button{
  font-family:var(--f-dis); font-weight:700; font-size:14px;
  padding:11px 22px; border:0; border-radius:var(--r-full);
  color:#fff; background:var(--grad-brand); cursor:pointer;
}

@media (prefers-reduced-motion: reduce){
  #boot{animation:none}
  .bk::after{animation:none; opacity:0}
}


/* ============================================================================
   PREVIEW & UNLOCK
   A course held on preview opens its first lessons and shows the rest, shut.
   Everything here is inert unless that is the case — nothing in this file
   applies to a course the student owns outright.

   Two shapes for the one action, because one shape cannot do both jobs:
     >=760px  a labelled button in the top bar, beside the theme toggle
     <760px   a full-width strip stuck under the bar, with the price on it
   Both disappear on a lesson, a certificate and a running quiz. Somebody who
   is reading is already being persuaded; interrupting that with a price is
   the surest way to end the session.
   ========================================================================== */

/* ------------------------------------------------------- top-bar button --- */
#unlockBtn{
  flex:0 0 auto;
  white-space:nowrap;
  gap:7px;
}
#unlockBtn[hidden]{display:none}
#unlockBtn .ub-i{width:15px;height:15px;flex:0 0 auto}

/* ------------------------------------------------------------ the strip --- */
#unlockBar{
  display:none;
  position:sticky;
  /* the bar above is sticky at 0 and this height, so this one sits exactly
     under it at every breakpoint without a hard-coded offset anywhere */
  top:var(--top-h);
  z-index:54;
  width:100%;
  align-items:center;
  justify-content:space-between;
  gap:10px;
  height:44px;
  padding:0 14px;
  border:0;
  color:#fff;
  font-family:var(--f-dis);
  text-align:left;
  cursor:pointer;
  background:var(--grad-brand);
  box-shadow:0 6px 18px -8px rgba(90,69,255,.55);
}
#unlockBar[hidden]{display:none}
#unlockBar .ul-l{display:flex; align-items:center; gap:9px; min-width:0}
#unlockBar .ul-i{width:15px;height:15px;flex:0 0 auto}
#unlockBar b{
  font-weight:750; font-size:13.6px; letter-spacing:-.01em;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
#unlockBar .ul-r{
  display:flex; align-items:center; gap:5px; flex:0 0 auto;
  font-weight:800; font-size:13px; opacity:.95;
}
#unlockBar .ul-c{width:14px;height:14px;flex:0 0 auto}
#unlockBar:active{filter:brightness(.96)}

@media (max-width:759.98px){
  #unlockBtn{display:none !important}
  #unlockBar:not([hidden]){display:flex}
  /* The strip and the lesson toolbar are both sticky at --top-h, so with the
     strip up they landed on the same line and the strip, higher up the stack,
     simply covered the toolbar. 44px is the strip's own height, declared eleven
     lines above this one. Only in this query: above 760 the way out of a
     preview is a button in the top bar and there is no strip to clear. */
  body.unlocking .lessonbar{top:calc(var(--top-h) + 44px)}
}
/* Under 340 the price is the first thing to go: the label is the ask, the
   number is a detail, and the tap target must not shrink for either. */
@media (max-width:339.98px){
  #unlockBar .ul-p{display:none}
}

/* ------------------------------------------------------ the mid-lesson wall */
/* Where the free reading stops, WALL_SECTIONS sections into the first locked
 * lesson -- one, today. See boot.php, which is where that number lives.
 *
 * IT IS DELIBERATELY NOT A MODAL AND NOT A BANNER. It is a card in the flow of
 * the page, in the place the next section would have been, because the point is
 * that the lesson CONTINUES -- a dialog over the top says "you have hit a
 * paywall", and a card in the flow says "there is more of this just here". */
.wallcard{
  margin:22px 0 8px; padding:clamp(20px,4vw,30px);
  border:1px solid var(--brand-line); border-radius:20px;
  background:linear-gradient(180deg,var(--brand-soft),var(--surface));
  text-align:center;
}
.wallcard .wc-ic{
  width:46px;height:46px;margin:0 auto 12px;border-radius:15px;
  display:grid;place-items:center;background:var(--surface);
  color:var(--brand);box-shadow:0 1px 0 var(--line)
}
.wallcard .wc-ic .i{width:22px;height:22px}
.wallcard h2{
  font-family:var(--f-dis);font-size:clamp(19px,2.7vw,23px);letter-spacing:-.02em;
  margin:0 0 6px
}
.wallcard > p{color:var(--ink-2);font-size:15px;line-height:1.6;margin:0 auto 18px;max-width:46ch}
.wallcard > p b{color:var(--ink)}
/* The buy button is the loud one and stands alone; see renderWall for why. */
.wallcard .wc-buy{min-width:min(20rem,100%)}
.wc-form{max-width:24rem;margin:0 auto}
.wc-lab{
  display:block;font-size:12px;letter-spacing:.09em;text-transform:uppercase;
  color:var(--ink-3);font-weight:700;margin-bottom:7px
}
/* Stacks under 420px. A 44px-tall button beside a shrinking input is how an
   email field ends up eight characters wide on a phone. */
.wc-row{display:flex;gap:8px;flex-wrap:wrap}
.wc-in{
  flex:1 1 11rem;min-width:0;height:44px;padding:0 13px;
  border:1px solid var(--line-strong);border-radius:12px;
  background:var(--surface);color:var(--ink);font:inherit;font-size:15px
}
.wc-in:focus{outline:2px solid var(--brand);outline-offset:1px;border-color:var(--brand)}
.wc-row .btn{height:44px;flex:0 0 auto}
/* CONTINUE WITH GOOGLE, above the email row. Full width and 44px, the same
   height as the field and the button beside it, so the two ways in read as one
   pair rather than as a button and an afterthought.
   The mark keeps Google's own four colours -- their brand guidelines require it
   and a recoloured one is worse than none, because a wrong-coloured Google
   button is the shape a phishing page takes. */
.wc-goog{width:100%;height:44px;gap:10px;margin-bottom:12px}
.wc-goog svg{flex:0 0 auto}
/* NO DIVIDER BETWEEN THE TWO WAYS IN. There was one -- "or leave your email" --
   and it put a second rule across a card that already had "not yet?" above it.
   Two rules, three buttons, a label and a line of fine print in 589px of a
   694px screen is what "the whole wall looks messy" was.
   .wc-lab covers both: "Finish just this lesson, free" is as true of one tap as
   of an email address, so the two controls sit under it as a pair and need
   nothing between them. */
.wc-fine{color:var(--ink-4);font-size:12.5px;line-height:1.5;margin:9px 0 0}
.wc-msg{font-size:13.5px;line-height:1.5;margin:10px 0 0;color:var(--ink-2)}
.wc-msg.ok{color:var(--ok)}
.wc-msg.bad{color:var(--no)}
.wc-or{
  display:flex;align-items:center;gap:12px;margin:20px auto 14px;max-width:24rem;
  color:var(--ink-4);font-size:12px;letter-spacing:.1em;text-transform:uppercase
}
.wc-or::before,.wc-or::after{content:"";flex:1;height:1px;background:var(--line)}

/* --------------------------------------------------- locked lesson rows --- */
/* Shown, never hidden. The list is the argument: fifty numbered mistakes, two
   of them open, and the student can read every title they are not buying. */
.lrow.locked .ls{color:var(--ink-4)}
.lrow.locked .lt b{color:var(--ink-3)}
/* the row still lifts on hover — it is not disabled, it leads somewhere */
.lr-lock{
  display:inline-flex; align-items:center; gap:5px;
  flex:0 0 auto;
  padding:4px 9px;
  border-radius:var(--r-full);
  background:var(--surface-3);
  border:1px solid var(--line);
  color:var(--ink-3);
  font-family:var(--f-dis); font-weight:700; font-size:11px;
  letter-spacing:.01em;
}
.lr-lock .i{width:12px;height:12px}

/* ---------------------------------------------------- the locked screen --- */
/* Where a locked lesson would have been. States plainly what is behind it and
   offers the one way through, rather than bouncing the student to Home. */
.lockwrap{
  max-width:560px; margin:0 auto; text-align:center;
  padding:44px 24px;
}
.lockwrap .lw-ic{
  width:66px;height:66px;margin:0 auto 20px;
  display:grid;place-items:center;
  border-radius:var(--r-xl);
  background:var(--brand-soft);
  color:var(--brand);
}
.lockwrap .lw-ic .i{width:28px;height:28px}
.lockwrap h2{
  font-family:var(--f-dis); font-weight:800; font-size:23px;
  letter-spacing:-.022em; color:var(--ink); margin:0 0 10px;
}
.lockwrap p{font-size:15px; line-height:1.65; color:var(--ink-3); margin:0 0 8px}
.lockwrap .lw-peek{
  margin:22px 0;
  padding:18px 20px;
  border-radius:var(--r-lg);
  background:var(--surface-2);
  border:1px solid var(--line);
  text-align:left;
}
.lockwrap .lw-peek .lw-k{
  font-family:var(--f-dis); font-weight:800; font-size:10.5px;
  letter-spacing:.1em; text-transform:uppercase; color:var(--ink-4);
  margin-bottom:9px;
}
.lockwrap .lw-acts{
  display:flex; gap:10px; justify-content:center; flex-wrap:wrap;
  margin-top:24px;
}

/* ------------------------------------------------- after a preview quiz --- */
/* Two different cards on purpose. The first quiz has only just proved the
   format works, so it says what is there and gets out of the way. The second
   is the last free thing in the course, and that is the moment to ask. */
.pvnote{
  margin-top:18px;
  padding:18px 20px;
  border-radius:var(--r-lg);
  border:1px solid var(--line);
  background:var(--surface-2);
  text-align:left;
}
.pvnote.ask{
  border-color:var(--brand-line);
  background:var(--brand-soft);
}
.pvnote .pv-t{
  font-family:var(--f-dis); font-weight:800; font-size:15.5px;
  letter-spacing:-.015em; color:var(--ink); margin-bottom:6px;
  display:flex; align-items:center; gap:8px;
}
.pvnote .pv-t .i{width:16px;height:16px;color:var(--brand);flex:0 0 auto}
.pvnote p{font-size:13.8px; line-height:1.6; color:var(--ink-3); margin:0}
.pvnote .pv-act{margin-top:14px; display:flex; gap:9px; flex-wrap:wrap}

@media (prefers-reduced-motion: reduce){
  #unlockBar:active{filter:none}
}
