/* ============================================================
   JazLearnThai - "Duo-Play" design system
   Thesis: a game you want to open daily. Bright navy + tassel-yellow,
   chunky rounded geometry, and tactile 3D press-down buttons.

   Signature: every interactive surface has a solid darker bottom edge
   that collapses as the surface presses down (:active), so the whole UI
   feels physically pressable. Correct answers bounce the mascot; wrong
   answers shake it. All motion respects prefers-reduced-motion.

   Layout goal: the full learning loop (prompt + 4 options) and the answer
   feedback fit the first fold on a phone. Mobile-first density.
   ============================================================ */

:root {
  /* Masked SVG speaker recolours cleanly with state (unlike an emoji glyph).
     Shared by the tap-to-hear word and the audio matching tile. */
  --spk-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M4%209h4l5-4v14l-5-4H4z'/%3E%3Cpath%20d='M16%208.5a4%204%200%20010%207M18.5%206a7.5%207.5%200%20010%2012'%20fill='none'%20stroke='black'%20stroke-width='2'%20stroke-linecap='round'/%3E%3C/svg%3E");
  /* --- brand palette --- */
  --navy:        #2E2C8B;   /* structure, text, hero */
  --navy-deep:   #23215F;   /* button 3D edges, deep shadow */
  --navy-soft:   #EEEEFB;   /* tinted fills on light */
  --yellow:      #FFE100;   /* primary action + XP */
  --yellow-deep: #E0B500;   /* yellow 3D edge */
  --green:       #3FD07A;   /* correct */
  --green-deep:  #29A65E;
  --green-soft:  #E4F9ED;
  --red:         #FF5468;   /* wrong */
  --red-deep:    #D63A4D;
  --red-soft:    #FFE7EA;
  --pink:        #F5A9BC;   /* hearts / lives */
  --sky:         #8ECAE6;   /* progress / audio accent */
  --sky-deep:    #5AA9CC;   /* sky 3D edge (pronunciation button) */
  --gold:        #F2C037;

  /* podium metals (ranking) */
  --silver:      #DCDBEA; --silver-2: #B9B7D2; --silver-edge: #9E9CBE;
  --bronze:      #EBB88C; --bronze-2: #CE924F; --bronze-edge: #A96C34;

  --canvas:      #FFFDF5;   /* warm off-white page */
  --canvas-2:    #FFF7E0;
  --surface:     #FFFFFF;
  --surface-2:   #F7F7FC;

  --ink:         #2E2C8B;   /* primary text = navy */
  --ink-2:       #4A4880;
  --ink-soft:    #7C7AA6;
  --ink-faint:   #A9A7C6;
  --line:        #E7E6F5;

  /* chunky radii */
  --r-xl: 28px;
  --r-lg: 22px;
  --r-md: 16px;
  --r-sm: 12px;

  /* soft lift shadows (distinct from the hard 3D press edges) */
  --lift-sm: 0 3px 10px -4px rgba(46,44,139,.18);
  --lift-md: 0 14px 30px -14px rgba(46,44,139,.30);
  --lift-lg: 0 28px 60px -26px rgba(46,44,139,.40);

  --font-display: "Baloo 2", "Nunito", ui-rounded, system-ui, sans-serif;
  --font-body: "Nunito", ui-rounded, system-ui, "Segoe UI", sans-serif;

  /* Thai learning surface: looped, beginner-friendly face (huruf berkepala). */
  --font-thai: "Sarabun", "IBM Plex Sans Thai", sans-serif;

  /* Romanization is its own type role, not body copy. Nunito has no glyph for
     u+01D0 or u+01D2, so "phuu-ying" and "khoo-thoot" borrowed their i-caron
     and o-caron from whatever the device fell back to: a different shape and
     weight mid-word. Sarabun carries all 20 tone-marked vowels the JLT
     standard uses (a e i o u x grave/acute/circumflex/caron), and it is
     already the Thai face, so a word and its reading look related.
     Verified per-glyph in design-systems/mockups/romanization-fonts.html;
     IBM Plex Sans Thai is worse than Nunito here (5 of 20 missing), so it is
     deliberately NOT in this fallback chain. */
  --font-roman: "Sarabun", Tahoma, sans-serif;

  --safe-b: env(safe-area-inset-bottom, 0px);
}

/* Sarabun subsets whose Thai-digit-zero is redrawn as a dotted circle: the
   vowel-carrier placeholder in the letter table and on the writing pad
   (scripts/build_carrier_font.py; Sarabun is OFL, license in
   fonts/OFL-Sarabun.txt). */
@font-face {
  font-family: "JLT Carrier";
  src: url("fonts/jlt-carrier-regular.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "JLT Carrier";
  src: url("fonts/jlt-carrier-bold.woff2") format("woff2");
  font-weight: 700;
  font-display: swap;
}

* { box-sizing: border-box; }
html, body { margin: 0; }
[hidden] { display: none !important; }
.svg-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* Every surface that prints a reading (cara baca), in one place so a new one
   is never accidentally left on --font-body. Meanings and Indonesian copy are
   NOT romanization and stay on the body face. */
.prompt-roman, .opt-roman, .r-roman, .ex-roman, .bd-roman,
.m-roman, .wc-roman, .letter-roman, .roman-toggle .aa {
  font-family: var(--font-roman);
}

body {
  font-family: var(--font-body);
  color: var(--ink);
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
  /* soft dotted "confetti" texture over a warm canvas */
  background-color: var(--canvas);
  background-image:
    radial-gradient(circle at 1px 1px, rgba(46,44,139,.055) 1.4px, transparent 0),
    radial-gradient(760px 480px at 90% -10%, rgba(255,225,0,.16), transparent 60%),
    radial-gradient(700px 520px at -8% 110%, rgba(142,202,230,.18), transparent 60%);
  background-size: 22px 22px, auto, auto;
  background-attachment: fixed;
}

/* ================= 3D PRESS BUTTONS (the signature) ================= */
/* A solid darker bottom edge (box-shadow, not blur) that collapses while
   the surface translates down on press. Reused across every control. */
.btn-primary, .opt, .btn-ghost {
  --edge: var(--navy-deep);
  --lift: 5px;
  border: 0; cursor: pointer;
  transition: transform .06s ease, box-shadow .06s ease, background .14s ease, border-color .14s ease;
}
.btn-primary:active:not(:disabled),
.opt:active:not(:disabled),
.btn-ghost:active {
  transform: translateY(var(--lift));
  box-shadow: 0 0 0 0 var(--edge) !important;
}
:focus-visible { outline: 3px solid var(--sky); outline-offset: 3px; }

/* ---------- App shell ---------- */
/* The frame is the whole page: every scroll belongs to an inner scroller
   (.answer-scroll, a .view, .class-groups), so the document itself must never
   scroll. Without this lock anything parked off-screen counts into iOS Safari's
   scrollable area (a closed .sheet-panel sits a panel-height below the fold on
   transform alone) and the learner can drag the exercise off the top into a
   tall run of empty canvas. Scoped to the app page; the landing shares this
   stylesheet and is a normal scrolling document. */
html:has(> body > .app) { overflow: hidden; }
.app {
  position: relative; z-index: 1;
  max-width: 560px; margin: 0 auto;
  height: 100dvh;             /* fixed frame so a practice session fits the page */
  padding: 16px 16px 0;       /* tabbar rides the bottom in-flow; no reservation */
  --app-gap: 13px;
  display: flex; flex-direction: column; gap: var(--app-gap);
  /* opacity-only entrance: never leaves a lingering transform, so the pinned
     Lanjut button stays viewport-relative on phones. */
  animation: fade .45s ease both;
}

/* ---------- Practice bar ---------- */
/* Back + title hard left, the practice controls hard right, one row: the title
   ellipses rather than wrapping the controls onto a second line. */
.brand { display: flex; align-items: center; justify-content: space-between; gap: 10px; }

/* Practice header controls: cara-baca ก/Aa pill (always visible) sitting beside
   a "..." menu that holds the practice direction. */
.play-controls { display: inline-flex; align-items: center; gap: 8px; flex: 0 0 auto; }

/* Romanization segmented pill; echoes the direction control's pill language. */
.roman-toggle {
  display: inline-flex; align-items: stretch; background: var(--surface);
  border: 2px solid var(--line); border-radius: 999px; padding: 3px; gap: 2px;
  box-shadow: var(--lift-sm); flex: 0 0 auto;
}
.roman-toggle button {
  border: 0; background: transparent; color: var(--ink-soft); cursor: pointer;
  font-family: var(--font-display); font-weight: 800; line-height: 1;
  min-width: 36px; padding: 8px 10px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  transition: background .16s ease, color .16s ease, box-shadow .16s ease;
}
.roman-toggle .th { font-family: var(--font-thai); font-size: 1.02rem; }
.roman-toggle .aa { font-size: .84rem; letter-spacing: .3px; }
.roman-toggle button:hover { color: var(--navy); }
.roman-toggle button[aria-pressed="true"] {
  color: var(--navy); background: var(--yellow); box-shadow: 0 3px 0 0 var(--yellow-deep);
}

/* Direction overflow menu; mode senyap on the exercise bar reuses it whole. */
.dir-menu { position: relative; flex: 0 0 auto; }
.dir-menu-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: 999px; box-shadow: var(--lift-sm);
  background: var(--surface); border: 2px solid var(--line); color: var(--ink-soft);
  cursor: pointer; transition: color .16s ease, border-color .16s ease;
}
.dir-menu-btn svg { width: 20px; height: 20px; }
.dir-menu-btn:hover, .dir-menu-btn[aria-expanded="true"] { color: var(--navy); border-color: #DEDDF6; }
.dir-menu-pop {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 40;
  display: flex; flex-direction: column; gap: 4px; min-width: 168px;
  background: var(--surface); border: 2px solid var(--line);
  border-radius: var(--r-md); padding: 8px; box-shadow: var(--lift-md);
}
.dir-menu-title { font-size: .64rem; font-weight: 800; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-soft); padding: 2px 6px 4px; }
.dir-menu-pop button {
  border: 0; background: transparent; color: var(--ink-2); cursor: pointer;
  font-family: var(--font-display); font-size: .84rem; font-weight: 700;
  text-align: left; white-space: nowrap; padding: 9px 10px; border-radius: var(--r-sm);
  transition: background .12s ease, color .12s ease;
}
.dir-menu-pop button:hover { background: var(--navy-soft); color: var(--navy); }
.dir-menu-pop button.active {
  color: var(--navy); background: var(--yellow); box-shadow: 0 2px 0 0 var(--yellow-deep);
}

/* ---------- Profile: "Kartu Pemain" — gold lai-thai (kranok) collectible card ---------- */
.k-eyebrow { display: flex; align-items: center; justify-content: center; gap: 9px; margin: 0; }
.ey-pill { font-size: .7rem; font-weight: 800; letter-spacing: .16em; color: var(--navy);
  background: var(--navy-soft); padding: 5px 12px; border-radius: 999px; }
.ey-note { font-size: .75rem; font-weight: 700; color: var(--ink-faint); }

/* A card look (skin) is one set of these vars; the card and the small previews
   on the Koleksi Kartu sheet both read them. Edisi Emas is the default; a skin
   class (skin-istana) overrides the set. */
.kartu, .cd-thumb {
  --k-rim: linear-gradient(135deg,#FFF3B0 0%,#F4C64A 24%,#C9992E 44%,#FBE38A 62%,#B8860B 82%,#F4C64A 100%);
  --k-face: radial-gradient(130% 100% at 22% 0%, #3B39A6 0%, var(--navy) 46%, #1F1D57 100%);
  --k-ring: #3FD07A;
}
.kartu {
  --px: 50%; --py: 50%;
  flex-shrink: 0; margin: 2px auto 0; width: 100%; max-width: 340px; border-radius: 29px;
}
/* Tilt only where initCardTilt() actually runs. A permanent 3D layer (perspective
   + will-change) makes iOS Safari rasterize the whole card once and upscale it,
   so every glyph, icon and the mascot go soft on phones. */
@media (hover: hover) and (pointer: fine) {
  .kartu {
    --rx: 0deg; --ry: 0deg;
    transform: perspective(760px) rotateX(var(--rx)) rotateY(var(--ry));
    transition: transform .5s cubic-bezier(.2,.8,.2,1); will-change: transform;
  }
}
/* gold-leaf rim (Thai temple gilding) */
.k-frame { position: relative; padding: 3px; border-radius: 29px;
  background: var(--k-rim);
  box-shadow: 0 4px 0 0 var(--navy-deep), 0 22px 44px -14px rgba(35,33,95,.55); }
.k-face { position: relative; overflow: hidden; border-radius: 26px; padding: 16px 17px 14px;
  background:
    radial-gradient(rgba(244,198,74,.07) 1px, transparent 1.7px) 0 0/18px 18px,
    var(--k-face);
  box-shadow: inset 0 0 0 1px rgba(212,175,55,.28), inset 0 2px 10px rgba(0,0,0,.30); }
/* gold sheen that shifts with tilt/pointer */
.k-foil { position: absolute; inset: 0; z-index: 1; pointer-events: none; mix-blend-mode: screen; opacity: .6;
  background:
    radial-gradient(150px 150px at var(--px) var(--py), rgba(255,240,190,.5), transparent 62%),
    repeating-linear-gradient(103deg, rgba(255,235,150,.10) 0 12%, rgba(255,255,255,.16) 12% 19%, rgba(201,153,46,.10) 19% 32%);
  background-size: auto, 240% 240%;
  background-position: 0 0, calc(var(--px) - 20%) calc(var(--py) - 20%); }
/* gold lai-thai ornament: double hairline frame + flame-scroll corners */
.k-thai { position: absolute; inset: 8px; z-index: 2; pointer-events: none; border-radius: 20px;
  border: 1.5px solid rgba(244,198,74,.55); box-shadow: inset 0 0 0 4px rgba(244,198,74,.16); }
.k-corner { position: absolute; width: 40px; height: 40px; z-index: 2; pointer-events: none;
  color: #F4C64A; }
.k-corner svg { width: 100%; height: 100%; display: block; }
.k-corner.tl { top: 5px; left: 5px; }
.k-corner.tr { top: 5px; right: 5px; transform: scaleX(-1); }
.k-corner.bl { bottom: 5px; left: 5px; transform: scaleY(-1); }
.k-corner.br { bottom: 5px; right: 5px; transform: scale(-1,-1); }
.k-content { position: relative; z-index: 3; }
.k-ribbon { display: flex; align-items: center; justify-content: space-between; padding: 0 28px; color: rgba(255,255,255,.72); }
.k-mark { display: flex; align-items: center; gap: 6px; font-weight: 900; font-size: .68rem; letter-spacing: .16em; }
.k-mark .el { font-size: .85rem; filter: saturate(0) brightness(1.6); opacity: .9; }
.k-rarity { display: flex; align-items: center; gap: 5px; font-family: "Sarabun", var(--font-body);
  font-weight: 700; font-size: .72rem; letter-spacing: .03em; color: #1F1D57;
  background: linear-gradient(120deg,#FFF0A8,#FFE100 55%,#E0B500); padding: 3px 11px; border-radius: 999px;
  box-shadow: 0 2px 0 0 rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.6); }
.k-portrait { position: relative; width: 150px; height: 150px; margin: 6px auto 2px; }
.k-portrait svg { transform: rotate(-90deg); overflow: visible; }
.k-ring-track { fill: none; stroke: rgba(255,255,255,.16); stroke-width: 11; }
.k-ring-prog { fill: none; stroke: url(#kGrad); stroke-width: 11; stroke-linecap: round;
  stroke-dasharray: 414.69; stroke-dashoffset: 414.69;
  filter: drop-shadow(0 2px 5px rgba(63,208,122,.55)); transition: stroke-dashoffset 1s cubic-bezier(.4,0,.2,1); }
.k-disc { position: absolute; inset: 15px; border-radius: 50%; overflow: hidden;
  background: radial-gradient(circle at 50% 34%, #fff, var(--canvas-2));
  display: flex; align-items: flex-end; justify-content: center;
  box-shadow: inset 0 0 0 4px rgba(255,255,255,.9), 0 8px 18px rgba(0,0,0,.35); }
.k-disc img { width: 116%; margin-bottom: -5px; filter: drop-shadow(0 5px 5px rgba(35,33,95,.28)); }
.k-lv { position: absolute; left: 50%; bottom: -6px; transform: translateX(-50%);
  font-family: var(--font-display); font-weight: 800; font-size: .82rem; color: var(--navy);
  background: var(--yellow); padding: 4px 14px; border-radius: 999px; border: 3px solid #fff;
  box-shadow: 0 4px 0 0 var(--yellow-deep); z-index: 4; white-space: nowrap; }
.k-name { text-align: center; margin-top: 14px; font-family: var(--font-display); font-weight: 800;
  font-size: 1.9rem; line-height: 1; color: #fff; text-shadow: 0 2px 0 rgba(0,0,0,.18); }
.k-title { text-align: center; margin-top: 5px; font-weight: 900; font-size: .72rem; letter-spacing: .12em;
  text-transform: uppercase; color: #FFE79A; }
.k-title .dot { color: rgba(255,255,255,.4); margin: 0 6px; }
.k-title .lv { color: #fff; }
.k-stats { display: flex; gap: 8px; margin-top: 14px; }
.k-badge { flex: 1; text-align: center; padding: 9px 4px 8px; border-radius: 12px;
  background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.12); border-top: 3px solid rgba(255,255,255,.2);
  box-shadow: inset 0 -1px 0 rgba(0,0,0,.25), inset 0 1px 0 rgba(255,255,255,.08); }
.k-badge.s { border-top-color: #FF8A1E; }
.k-badge.a { border-top-color: var(--green); }
.k-badge.x { border-top-color: var(--yellow); }
.k-badge .n { display: flex; align-items: center; justify-content: center; gap: 3px; min-height: 1.5em; font-family: var(--font-display); font-weight: 800; font-size: 1.25rem; color: #fff; line-height: 1; }
.k-badge .n .em { font-size: .95rem; }
.k-badge .l { display: block; margin-top: 4px; font-size: .56rem; font-weight: 900; letter-spacing: .08em; text-transform: uppercase; color: rgba(255,255,255,.62); }
.k-serial { display: flex; align-items: center; justify-content: space-between; margin-top: 13px; padding: 11px 26px 0;
  border-top: 1px solid rgba(255,255,255,.12); font-weight: 900; font-size: .6rem; letter-spacing: .16em; color: rgba(255,255,255,.5); }
.k-serial .mid { letter-spacing: .4em; color: rgba(255,255,255,.28); }

/* Profile account actions - grouped "Akun" card (rows split by a divider) */
.account-sec { margin: 20px 0 16px; }
.account-sec + .account-sec { margin-top: 4px; }
.account-sec .sec-label { margin-bottom: 10px; }

.account-card { background: var(--surface); border: 2px solid var(--line); border-radius: var(--r-lg);
  box-shadow: 0 4px 0 0 var(--line); overflow: hidden; }

.account-row { display: flex; align-items: center; gap: 14px; width: 100%; min-height: 56px;
  padding: 14px 16px; background: transparent; border: 0; text-align: left; cursor: pointer;
  color: var(--navy); font-family: var(--font-display);
  transition: background-color .14s ease, box-shadow .06s ease; }
.account-row + .account-row { border-top: 2px solid var(--line); }
.account-row:hover { background: var(--navy-soft); }
.account-row:active { background: var(--surface-2); box-shadow: inset 0 3px 6px rgba(46,44,139,.10); }
.account-row:focus-visible { outline: 3px solid var(--navy); outline-offset: -3px; background: var(--navy-soft); }

.row-ico { flex: none; width: 34px; height: 34px; border-radius: var(--r-sm); display: grid; place-items: center;
  background: var(--navy-soft); color: var(--navy); }
.row-ico svg { width: 20px; height: 20px; display: block; }
.row-label { flex: 1 1 auto; font-size: .98rem; font-weight: 700; color: var(--navy); line-height: 1.1; }
.row-chev { flex: none; color: var(--ink-faint); display: grid; place-items: center; transition: transform .12s ease; }
.row-chev svg { width: 18px; height: 18px; display: block; }
.account-row:hover .row-chev { transform: translateX(2px); color: var(--ink-soft); }

.account-row.is-exit .row-ico { background: var(--red-soft); color: var(--red-deep); }
.account-row.is-exit .row-label { color: var(--red-deep); }
.account-row.is-exit .row-chev { color: var(--red); }
.account-row.is-exit:hover { background: var(--red-soft); }
.account-row.is-exit:focus-visible { outline-color: var(--red-deep); background: var(--red-soft); }

@media (prefers-reduced-motion: reduce) { .account-row, .row-chev { transition: none; } }

/* Settings rows (profile): .account-row chassis, a switch where the chevron goes. */
.setting-row { cursor: pointer; }
.setting-row[hidden] { display: none; }
.row-sub { display: block; margin-top: 3px; font-family: var(--font-body); font-size: .76rem; font-weight: 600; color: var(--ink-soft); line-height: 1.25; }
.row-sub[hidden] { display: none; }
.row-sub.is-warn { color: var(--red-deep); }
.row-glyph { font-family: var(--font-thai, inherit); font-size: 1rem; font-weight: 700; gap: 0; line-height: 1; }
.row-glyph small { font-family: var(--font-display); font-size: .58rem; font-weight: 800; margin-top: -2px; }
.setting-switch { appearance: none; -webkit-appearance: none; flex: none; position: relative;
  width: 42px; height: 24px; border-radius: 999px; background: var(--line); cursor: pointer; transition: background .15s ease; }
.setting-switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px;
  border-radius: 50%; background: #fff; transition: transform .15s ease; }
.setting-switch:checked { background: var(--green, var(--navy)); }
.setting-switch:checked::after { transform: translateX(18px); }
.setting-switch:disabled { opacity: .5; cursor: default; }
/* Cara baca off hides the reading on questions, answers and the Kosakata
   board. .keep-roman opts a line out (a materi card is explanation, like the
   answer reveal). */
body.hide-roman .prompt-roman:not(.keep-roman), body.hide-roman .opt-roman,
body.hide-roman .wc-roman, body.hide-roman .tut-word-ro { display: none; }

/* quiet section labels */
.sec-label { font-size: .68rem; font-weight: 800; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-soft); margin: 0 2px; }

/* XP-to-next-level card */
.xpcard { background: var(--surface); border: 2px solid var(--line); border-radius: var(--r-lg);
  box-shadow: 0 4px 0 0 var(--line); padding: 15px 16px 16px; }
.xp-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 9px; }
.xp-head .g { font-family: var(--font-display); font-weight: 800; font-size: .95rem; }
.xp-head .g b { color: var(--green-deep); }
.xp-head .pct { font-weight: 900; font-size: .78rem; color: var(--ink-soft); }
.xp-track { height: 16px; background: var(--navy-soft); border-radius: 999px; overflow: hidden; border: 2px solid var(--line); }
.xp-fill { height: 100%; width: 0; border-radius: 999px; background: linear-gradient(90deg,var(--green),var(--green-deep));
  box-shadow: inset 0 2px 0 rgba(255,255,255,.4); transition: width 1s cubic-bezier(.4,0,.2,1); }
.xp-cap { margin-top: 8px; font-size: .8rem; font-weight: 800; color: var(--ink-soft); }
.xp-cap b { color: var(--navy); }

/* Koleksi (gelar + kartu): .account-row chassis; the count sits in the sub line. */
.koleksi-sec { margin: 0; }
.koleksi-sec .row-ico img { width: 26px; height: 26px; display: block; object-fit: contain; }
.koleksi-sec .row-sub b { color: var(--green-deep); font-weight: 900; }
.account-row[hidden] { display: none; }
.account-row:disabled { cursor: default; }
.account-row:disabled:hover { background: transparent; }

.titles-board { display: flex; flex-direction: column; gap: 18px; }
.tl-track { display: flex; flex-direction: column; gap: 8px; }
.tl-head { display: flex; align-items: baseline; justify-content: space-between; }
.tl-label { font-family: var(--font-display); font-weight: 800; font-size: .95rem; color: var(--navy); }
.tl-count { font-weight: 900; font-size: .75rem; color: var(--ink-soft); }
.tl-tiers { display: flex; flex-direction: column; gap: 6px; }
.tl-tier { display: flex; align-items: center; gap: 10px; padding: 9px 12px; width: 100%;
  background: var(--surface-2); border: 2px solid var(--line); border-radius: var(--r-md); opacity: .72; }
button.tl-tier { font: inherit; text-align: left; cursor: pointer; }
button.tl-tier:active { transform: translateY(1px); }
button.tl-tier .tl-req { color: var(--green-deep); } /* "Pakai" reads as an action */
.tl-tier.on { opacity: 1; background: var(--green-soft); border-color: #BFEBCF; }
.tl-tier.cur { background: var(--yellow); border-color: var(--yellow-deep); box-shadow: 0 3px 0 0 var(--yellow-deep); }
.tl-mark { flex: none; width: 20px; text-align: center; font-weight: 900; color: var(--ink-faint); }
.tl-tier.on .tl-mark { color: var(--green-deep); }
.tl-tier.cur .tl-mark { color: var(--navy); }
.tl-name { flex: 1 1 auto; font-family: var(--font-display); font-weight: 800; font-size: .9rem; color: var(--navy); }
.tl-req { flex: none; font-size: .72rem; font-weight: 800; color: var(--ink-soft); text-align: right; }
.tl-tier.cur .tl-req { color: var(--navy); }

/* Premium titles: the two rarest titles look and move unlike every other one,
   on the profile card (#pcTitleName) and on the title board. Set by titleFx()
   in app.js. Both: gold text, a crown, a glow that breathes.
   fx-royal (Anggota Istana, ever subscribed): slow breath, the crown sways.
   fx-pioneer (Perintis Istana, first 100 subscribers): a light keeps sweeping
   across the gold, the crown twinkles, a gold pill frames it, the board row
   pulses. All motion stops under prefers-reduced-motion; the pill stays, so
   the two still look different. */
.fx-royal, .fx-pioneer {
  background: linear-gradient(100deg, #E0A100 0%, #FFE58A 45%, #F2C037 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  filter: drop-shadow(0 0 5px rgba(242,192,55,.6));
  animation: fx-breathe 3s ease-in-out infinite;
}
.fx-royal::before, .fx-pioneer::before {
  content: ""; display: inline-block; width: 1.15em; height: .9em; margin-right: .35em;
  vertical-align: -.08em; background: #F2C037; transform-origin: 50% 100%;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 18'%3E%3Cpath d='M1 4l5.5 5L12 0l5.5 9L23 4l-2.5 14h-17z'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 18'%3E%3Cpath d='M1 4l5.5 5L12 0l5.5 9L23 4l-2.5 14h-17z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.fx-royal::before { animation: fx-sway 3s ease-in-out infinite; }
.fx-pioneer {
  background-image: linear-gradient(100deg, #E0A100 0%, #F2C037 35%, #FFFFFF 50%, #F2C037 65%, #E0A100 100%);
  background-size: 250% 100%;
  animation: fx-shine 2.4s linear infinite, fx-breathe 2.4s ease-in-out infinite;
  border: 1.5px solid rgba(242,192,55,.75); border-radius: 999px; padding: 2px 9px;
}
.fx-pioneer::before { animation: fx-twinkle 2.4s ease-in-out infinite; }
@keyframes fx-breathe {
  0%, 100% { filter: drop-shadow(0 0 3px rgba(242,192,55,.45)); }
  50%      { filter: drop-shadow(0 0 9px rgba(255,214,90,.9)); }
}
@keyframes fx-shine { from { background-position: 100% 0; } to { background-position: -150% 0; } }
@keyframes fx-sway {
  0%, 100% { transform: rotate(-7deg); }
  50%      { transform: rotate(7deg); }
}
@keyframes fx-twinkle {
  0%, 70%, 100% { transform: scale(1);    background: #F2C037; }
  80%           { transform: scale(1.25) rotate(-6deg); background: #FFFFFF; }
  90%           { transform: scale(1);    background: #FFE58A; }
}
/* On the light board the gold needs a dark row behind it. */
.tl-tier.tl-prem { background: var(--navy); border-color: var(--gold); box-shadow: 0 3px 0 0 var(--navy-deep);
  animation: fx-glow 3s ease-in-out infinite; --glow: 8px; }
.tl-tier.tl-pioneer { animation-duration: 2.4s; --glow: 16px; }
@keyframes fx-glow {
  0%, 100% { box-shadow: 0 3px 0 0 var(--navy-deep), 0 0 0 0 rgba(242,192,55,0); }
  50%      { box-shadow: 0 3px 0 0 var(--navy-deep), 0 0 var(--glow) 2px rgba(242,192,55,.55); }
}
.tl-tier.tl-prem .tl-mark, .tl-tier.tl-prem .tl-req { color: var(--gold); }
@media (prefers-reduced-motion: reduce) {
  .fx-royal, .fx-pioneer, .fx-royal::before, .fx-pioneer::before, .tl-tier.tl-prem { animation: none; }
}

/* Edisi Istana: the card look only Premium subscribers ever hold. Garnet face
   under a brighter gold rim that keeps catching the light; the level ring and
   the edition pill turn gold too. Motion stops under prefers-reduced-motion. */
.skin-istana {
  --k-rim: linear-gradient(135deg,#FFF6C8 0%,#F7D45C 18%,#B8860B 38%,#FFF1A8 52%,#C9992E 70%,#FFE58A 86%,#F4C64A 100%);
  --k-face: radial-gradient(130% 100% at 22% 0%, #7A1A3C 0%, #3E0A1F 48%, #16040C 100%);
  --k-ring: #F2C037;
}
.skin-istana .k-frame { background-size: 300% 300%; animation: istana-rim 6s ease-in-out infinite;
  box-shadow: 0 4px 0 0 #2A0612, 0 22px 44px -14px rgba(62,10,31,.7), 0 0 28px -6px rgba(242,192,55,.55); }
.skin-istana .k-face { box-shadow: inset 0 0 0 1px rgba(242,192,55,.5), inset 0 2px 10px rgba(0,0,0,.4); }
.skin-istana .k-foil { opacity: .8; }
.skin-istana .k-ring-prog { stroke: url(#kGradGold); filter: drop-shadow(0 2px 6px rgba(242,192,55,.6)); }
.skin-istana .k-rarity { background: linear-gradient(120deg,#FFF6C8,#FFE58A 55%,#E0A100); }
.skin-istana .k-mark { color: #FFE79A; }
.skin-istana .k-mark .el { filter: none; }
.skin-istana .k-badge { background: rgba(255,255,255,.05); border-color: rgba(242,192,55,.22); }
@keyframes istana-rim { 0%, 100% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } }
@media (prefers-reduced-motion: reduce) { .skin-istana .k-frame { animation: none; } }

/* Koleksi Kartu sheet: one row per look, a small card preview on the left. */
.cards-board { display: flex; flex-direction: column; gap: 10px; }
.cd-row { display: flex; align-items: center; gap: 14px; padding: 12px; width: 100%; text-align: left;
  background: var(--surface-2); border: 2px solid var(--line); border-radius: var(--r-lg); opacity: .78; font: inherit; }
button.cd-row { cursor: pointer; }
button.cd-row:active { transform: translateY(1px); }
.cd-row.on { opacity: 1; background: var(--green-soft); border-color: #BFEBCF; }
.cd-row.cur { background: var(--yellow); border-color: var(--yellow-deep); box-shadow: 0 3px 0 0 var(--yellow-deep); }
.cd-row.cd-prem { background: var(--navy); border-color: var(--gold); box-shadow: 0 3px 0 0 var(--navy-deep); }
.cd-row.cd-prem.on { animation: fx-glow 3s ease-in-out infinite; --glow: 10px; }
.cd-thumb { display: block; flex: none; width: 62px; height: 84px; padding: 2px; border-radius: 10px; background: var(--k-rim);
  box-shadow: 0 3px 0 0 rgba(35,33,95,.5); }
.cd-face { display: block; position: relative; width: 100%; height: 100%; border-radius: 8px; overflow: hidden;
  background: radial-gradient(rgba(244,198,74,.1) .6px, transparent 1px) 0 0/7px 7px, var(--k-face);
  box-shadow: inset 0 0 0 1px rgba(244,198,74,.35); }
.cd-face::before { content: ""; position: absolute; left: 0; right: 0; top: 16px; width: 26px; height: 26px; margin: 0 auto; box-sizing: border-box;
  border-radius: 50%; border: 3px solid var(--k-ring); background: radial-gradient(circle at 50% 34%, #fff, var(--canvas-2)); }
.cd-face::after { content: ""; position: absolute; left: 14px; right: 14px; bottom: 16px; height: 5px; border-radius: 3px;
  background: rgba(255,255,255,.75); box-shadow: 0 10px 0 -1px rgba(255,231,154,.6); }
.cd-row.cd-locked .cd-thumb { filter: grayscale(.55) brightness(.75); }
.cd-body { flex: 1 1 auto; min-width: 0; }
.cd-name { display: block; font-family: var(--font-display); font-weight: 800; font-size: 1rem; color: var(--navy); }
.cd-row.cd-prem .cd-name { color: #fff; }
.cd-desc { display: block; margin-top: 3px; font-size: .74rem; font-weight: 700; line-height: 1.35; color: var(--ink-soft); }
.cd-row.cd-prem .cd-desc { color: rgba(255,255,255,.72); }
.cd-row.cd-prem.on .cd-desc { color: #FFE79A; }
.cd-req { flex: none; align-self: flex-start; font-size: .72rem; font-weight: 800; color: var(--ink-soft); text-align: right; max-width: 34%; }
button.cd-row .cd-req { color: var(--green-deep); }
.cd-row.cur .cd-req { color: var(--navy); }
.cd-row.cd-prem .cd-req { color: var(--gold); }
.cd-row.cd-prem .cd-req img { width: 14px; height: 14px; vertical-align: -2px; filter: invert(1) sepia(1) saturate(4) hue-rotate(5deg) brightness(1.1); }
/* The thank-you note under the unlocked Premium card. */
.cd-thanks { margin: 2px 0 0; padding: 12px 14px; border-radius: var(--r-md); background: var(--navy); color: #FFE79A;
  border: 1.5px solid rgba(242,192,55,.6); font-size: .8rem; font-weight: 700; line-height: 1.45; }
.cd-thanks b { font-family: var(--font-display); font-weight: 800; color: #fff; }

/* Statistik strip */
.strip { display: grid; grid-template-columns: repeat(4,1fr); gap: 8px; }
.strip .stat { background: var(--surface); border: 2px solid var(--line); border-radius: var(--r-md); box-shadow: 0 4px 0 0 var(--line);
  padding: 12px 4px 10px; text-align: center; min-height: 70px; display: flex; flex-direction: column; justify-content: center; gap: 5px; }
.strip .stat .n { font-family: var(--font-display); font-weight: 800; font-size: 1.35rem; line-height: 1; font-variant-numeric: tabular-nums; }
.strip .stat .l { font-size: .55rem; font-weight: 900; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-faint); line-height: 1.2; }
.strip .stat.due .n { color: var(--navy); }
.strip .stat.master .n { color: var(--ink-faint); }
.strip .stat.ok .n { color: var(--green-deep); }
.strip .stat.bad .n { color: var(--red-deep); }

/* enrollment chip */
.member { display: flex; align-items: center; gap: 8px; background: var(--surface); border: 2px solid var(--line);
  border-radius: 999px; box-shadow: 0 3px 0 0 var(--line); padding: 8px 14px; font-size: .78rem; font-weight: 800; color: var(--ink-soft); }
/* Whole-pixel box, no filter: the SVG rasterizes at full device resolution.
   em sizing here lands on fractional pixels and reads soft on phones. */
.member-ic { width: 22px; height: 22px; display: block; flex: none; margin: -3px 0; }
.member b { color: var(--navy); }
/* Peek variant: label on its own line, one chip per enrolled class below. */
.member.list { flex-wrap: wrap; border-radius: var(--r-md); padding: 12px 14px; white-space: nowrap; }
.member.list > b { flex-basis: 100%; display: flex; flex-wrap: wrap; gap: 6px; white-space: normal; }
.member-chip { background: var(--navy-soft); color: var(--navy); border-radius: 999px; padding: 5px 11px; line-height: 1.3; }

@media (prefers-reduced-motion: reduce) {
  .kartu { transform: none; }
  .k-ring-prog, .xp-fill { transition: none; }
}

/* ---------- Login ---------- */
.login {
  position: fixed; inset: 0; z-index: 40; padding: 24px; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(700px 560px at 50% 32%, rgba(142,202,230,.22), transparent 72%);
  animation: fade .25s ease both;
}
/* Signature: the Thai letters you're about to learn, drifting behind the card. */
.login-scene { position: absolute; inset: 0; pointer-events: none; z-index: 0; }
.login-scene span {
  position: absolute; left: var(--x); top: var(--y);
  font-family: var(--font-thai); font-weight: 700; line-height: 1;
  font-size: calc(5.5rem * var(--s, 1));
  color: var(--navy); opacity: .06;
  transform: translate(-50%, -50%);
  animation: drift 9s ease-in-out infinite; animation-delay: var(--d, 0s);
  will-change: transform;
}
.login-scene span:nth-child(even) { color: var(--yellow-deep); opacity: .12; }

.login-card {
  position: relative; z-index: 1; width: 100%; max-width: 400px;
  background: var(--surface); border: 3px solid var(--navy);
  border-radius: var(--r-xl); overflow: hidden;
  box-shadow: 0 14px 0 0 var(--navy-deep), var(--lift-lg);
  animation: pop .45s cubic-bezier(.2,.9,.3,1) both;
}
/* Navy hero cap: white wordmark on a dotted field, mascot straddling the seam. */
.login-cap {
  position: relative; text-align: center; padding: 22px 24px 74px;
  background:
    radial-gradient(circle at 1.5px 1.5px, rgba(255,255,255,.16) 1.4px, transparent 0) 0 0 / 17px 17px,
    linear-gradient(158deg, #3A38A8 0%, var(--navy) 58%, var(--navy-deep) 100%);
}
.login-logo { width: 150px; max-width: 66%; height: auto; display: block; margin: 0 auto; }
/* White coin behind the mascot so its navy mortarboard reads against the navy
   cap instead of blending in. Sits low on the cap so it clears the wordmark. */
.login-cap::after {
  content: ""; position: absolute; left: 50%; margin-left: -53px; bottom: -46px;
  width: 106px; height: 106px; border-radius: 50%;
  background: var(--surface); border: 3px solid var(--navy);
  box-shadow: var(--lift-md); z-index: 1;
}
.login-mascot {
  position: absolute; left: 50%; margin-left: -43px; bottom: -38px;
  width: 86px; height: auto; z-index: 2;
  filter: drop-shadow(0 5px 7px rgba(35,33,95,.20));
  animation: bob 3s ease-in-out infinite;
}
.login-body { padding: 56px 26px 26px; text-align: center; }
.login-hi {
  display: inline-block; font-family: var(--font-thai); font-weight: 700; font-size: .95rem;
  color: var(--navy); background: var(--yellow); border-radius: 999px;
  padding: 5px 15px; box-shadow: 0 3px 0 0 var(--yellow-deep); margin-bottom: 12px;
}
.login-title { font-family: var(--font-display); font-weight: 800; font-size: 1.55rem; margin: 0 0 8px; color: var(--navy); }
.login-sub { color: var(--ink-soft); font-size: .92rem; line-height: 1.5; margin: 0 0 20px; font-weight: 600; }

.login-field { position: relative; margin-bottom: 14px; }
.login-field-icon {
  position: absolute; left: 15px; top: 50%; transform: translateY(-50%);
  width: 20px; height: 20px; color: var(--ink-faint); pointer-events: none;
  transition: color .15s ease;
}
.login-field:focus-within .login-field-icon { color: var(--navy); }
.login-input {
  width: 100%; border: 2.5px solid var(--line); background: var(--surface-2);
  border-radius: var(--r-md); padding: 14px 16px 14px 44px;
  font-family: var(--font-body); font-size: 1rem; font-weight: 700; color: var(--navy);
  transition: border-color .15s ease, box-shadow .15s ease, background .15s ease;
}
.login-input::placeholder { color: var(--ink-faint); font-weight: 600; }
.login-input:focus { outline: 0; border-color: var(--navy); background: var(--surface); box-shadow: 0 0 0 4px var(--navy-soft); }
.login-submit .login-arrow { font-weight: 900; transition: transform .16s ease; }
.login-submit:hover:not(:disabled) .login-arrow { transform: translateX(3px); }
.login-google {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  margin: 12px 0 0; padding: 14px 18px; border-radius: 14px;
  border: 2px solid var(--navy-soft); background: var(--surface); color: var(--navy);
  font-weight: 800; text-align: center; text-decoration: none;
  transition: background .15s ease, border-color .15s ease;
}
.login-google-icon { width: 20px; height: 20px; flex: none; }
.login-google:hover { background: var(--navy-soft); border-color: var(--navy); }
.login-error { color: var(--red-deep); font-size: .86rem; line-height: 1.45; margin: 14px 0 0; font-weight: 700; }
.login-note { color: var(--ink-faint); font-size: .82rem; margin: 18px 0 0; font-weight: 600; }
.login-note-link { border: 0; background: transparent; padding: 0; font: inherit; font-weight: 800;
  color: var(--navy); text-decoration: underline; cursor: pointer; }
.register-form { display: flex; flex-direction: column; gap: 10px; }
.login-forgot { display: block; margin: 12px auto 0; border: 0; background: transparent;
  color: var(--ink-soft); font-family: var(--font-display); font-weight: 800; font-size: .84rem;
  cursor: pointer; text-decoration: underline; }
.login-forgot:hover { color: var(--navy); }

/* Set-password + forgot-password modals: a compact card reusing .overlay. */
.pw-card {
  background: var(--surface); border: 3px solid var(--navy); border-radius: var(--r-xl);
  padding: 26px 24px 20px; max-width: 380px; width: 100%; margin: auto; text-align: center;
  box-shadow: 0 12px 0 0 var(--navy-deep), var(--lift-lg);
  animation: pop .4s cubic-bezier(.2,.9,.3,1) both;
}
.pw-title { font-family: var(--font-display); font-weight: 800; font-size: 1.35rem; color: var(--navy); margin: 0 0 6px; }
.pw-sub { color: var(--ink-soft); font-size: .9rem; line-height: 1.5; font-weight: 600; margin: 0 0 18px; }
.pw-input {
  width: 100%; padding: 14px 16px; border: 2px solid var(--line); border-radius: 14px;
  font-family: inherit; font-size: 1rem; font-weight: 700; color: var(--ink); background: var(--surface-2);
}
.pw-input::placeholder { color: var(--ink-faint); font-weight: 600; }
.pw-input:focus { outline: 0; border-color: var(--navy); background: var(--surface); box-shadow: 0 0 0 4px var(--navy-soft); }
.pw-card .btn-primary { width: 100%; margin-top: 16px; }
.pw-error { color: var(--red-deep); font-size: .85rem; line-height: 1.4; margin: 12px 0 0; font-weight: 700; }
.pw-error.ok { color: var(--green-deep); }
.pw-skip { display: block; margin: 14px auto 0; border: 0; background: transparent;
  color: var(--ink-soft); font-family: var(--font-display); font-weight: 800; font-size: .86rem; cursor: pointer; }
.pw-skip:hover { color: var(--navy); }

/* ---------- Delete account (task 128) ---------- */
/* What the learner loses, as a plain list inside the .pw-card: red bullets,
   left-aligned, so the step reads as a warning, not a form. */
.del-list { list-style: none; margin: 0 0 4px; padding: 12px 14px; text-align: left;
  border: 2px solid var(--red-soft); border-radius: var(--r-md); background: #FFF6F7; }
.del-list li { position: relative; padding-left: 20px; font-size: .88rem; font-weight: 700; color: var(--ink-2); line-height: 1.4; }
.del-list li + li { margin-top: 6px; }
.del-list li::before { content: ""; position: absolute; left: 2px; top: .45em; width: 8px; height: 8px; border-radius: 50%; background: var(--red); }
/* The one destructive button: .btn-primary's shape in red, so it is never
   mistaken for the yellow "go on" button the rest of the app uses. */
.btn-primary.is-danger { background: var(--red); color: #fff; box-shadow: 0 5px 0 0 var(--red-deep); }
/* Step 1's way forward is the quiet link, in red; the yellow button keeps the account. */
.pw-skip.is-danger { color: var(--red-deep); }
.pw-skip.is-danger:hover { color: var(--red); }
.del-word { font-family: var(--font-display); font-weight: 800; color: var(--red-deep); letter-spacing: .08em; }
.pw-input.del-input { text-align: center; letter-spacing: .2em; text-transform: uppercase; }
/* After the delete the login screen carries one calm line; green, not the red error. */
.login-error.ok { color: var(--green-deep); }

/* ---------- New-learner tour (task 68) ---------- */
/* The tour is the .overlay + .pw-card chassis the password prompt uses, so it
   reads as the same product voice: a card with a hard navy edge, one yellow
   action, one quiet skip. The card rides the bottom of the screen when it
   fits (margin-top: auto soaks up the free space), so the mascot standing on
   its top edge has the overlay's top padding as headroom. When the card is
   taller than the screen the auto margin is zero, the card starts under that
   padding and the overlay scrolls; a centred or bottom-aligned card would push
   the mascot off the top where no scroll can reach it. */
.overlay.tour { align-items: flex-start; padding: 80px 16px 20px; }
/* Every card is the height of the tallest one, so Lanjut never jumps between
   cards; dots and buttons sit at the bottom. min() keeps the card inside a
   short phone (the viewport minus the overlay padding). */
.tut-card { position: relative; margin: auto 0 0; padding-top: 92px; display: flex; flex-direction: column;
  min-height: min(660px, calc(100dvh - 100px)); }
/* The specimen box stretches to the space left above the actions, so every
   card is full to the same line; its content sits centred inside. */
.tut-main { flex: 1; display: flex; flex-direction: column; }
.tut-main .tut-spec { flex: 1; display: flex; flex-direction: column; justify-content: center; }
.tut-card .tut-dots { padding-top: 18px; }
/* The mascot sits on the card's top edge like on the question card, but
   bigger: on this screen it is the teacher, not a reaction. */
.tut-mascot { position: absolute; top: -60px; left: 50%; transform: translateX(-50%); width: 132px; height: 132px; pointer-events: none; }
.tut-mascot img { width: 100%; height: 100%; object-fit: contain; filter: drop-shadow(0 10px 12px rgba(46,44,139,.22)); animation: bob 3s ease-in-out infinite; }
.tut-card.is-hello .tut-mascot { width: 150px; height: 150px; top: -62px; }
.tut-card.is-hello { padding-top: 104px; }
.tut-logo { width: 136px; margin: 0 auto 6px; display: block; }
.tut-step { font-size: .62rem; font-weight: 900; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); margin: 0 0 8px; }
.tut-title { font-family: var(--font-display); font-weight: 800; font-size: 1.32rem; line-height: 1.15; color: var(--navy); margin: 0 0 8px; }
.tut-body { color: var(--ink-2); font-size: .92rem; line-height: 1.5; font-weight: 600; margin: 0; }
/* The specimen: the real component the step is about, shown at true size, so
   the first time a learner meets it in the app they have already seen it. */
.tut-spec { margin: 14px 0 2px; padding: 14px 12px; border: 2px solid var(--line); border-radius: var(--r-lg); background: var(--surface-2); }
.tut-spec-head { display: block; margin: 0 0 8px; font-size: .62rem; font-weight: 900; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); text-align: left; }
.tut-spec-head-2 { margin-top: 16px; }
.tut-spec-cap { display: block; margin-top: 8px; font-size: .68rem; font-weight: 800; color: var(--ink-soft); }
.tut-spec.is-live { border-color: var(--sky); box-shadow: 0 0 0 4px rgba(142,202,230,.35); }
.tut-spec .account-row, .tut-spec .jm-subtopic, .tut-spec .xpcard { text-align: left; }
/* The specimen card stands alone: no rail to a previous or next card. */
.tut-spec .jm-subtopic::before, .tut-spec .jm-subtopic::after { content: none; }
.tut-spec .xpcard { margin: 0; }
.tut-spec .quickstart { margin-top: 12px; font-size: 1.05rem; padding: 12px; }
.tut-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.tut-list li { flex: 1; display: flex; align-items: center; gap: 10px; padding: 4px 10px; border: 2px solid var(--line); border-radius: 12px; background: var(--surface);
  font-family: var(--font-display); font-weight: 800; font-size: .84rem; color: var(--navy); text-align: left; }
.tut-list img { width: 22px; height: 22px; object-fit: contain; flex: none; }
.tut-dots { display: flex; justify-content: center; gap: 6px; }
.tut-dots i { width: 8px; height: 8px; border-radius: 50%; background: var(--line); transition: width .18s ease, background .18s ease; }
.tut-dots i.is-on { width: 22px; border-radius: 999px; background: var(--navy); }
.tut-dots i.is-done { background: var(--sky); }
.tut-card .btn-primary { margin-top: 14px; }
.tut-card .pw-skip { margin-top: 12px; }
/* stage and cycle: the pip states, drawn with the pips' own colours */
.tut-legend { list-style: none; margin: 12px 0 0; padding: 10px 0 0; border-top: 2px dashed var(--line); display: grid; grid-template-columns: 1fr 1fr; gap: 7px 10px; }
.tut-legend li { display: flex; align-items: center; gap: 7px; font-size: .72rem; font-weight: 800; color: var(--ink-2); text-align: left; line-height: 1.2; }
.tut-legend i { flex: none; width: 16px; height: 16px; border-radius: 5px; border: 2px solid var(--line); background: var(--surface); }
.tut-legend i.is-filled { background: var(--green-soft); border-color: var(--green); }
.tut-legend i.is-next { background: var(--yellow); border-color: var(--yellow-deep); }
.tut-legend i.is-ring { border: 3px solid #22995A; }
/* thai tea: the real Beranda chip beside its note, then the energy sheet's shape.
   .tut-spec on the three sheet rules: .energy-sheet's own rules come later in
   this file, and at equal specificity the later one would win. */
.tut-tea-chip { display: flex; align-items: center; gap: 12px; text-align: left; }
.tut-tea-chip .gchip { flex: none; }
.tut-tea-note { font-size: .78rem; font-weight: 700; color: var(--ink-soft); line-height: 1.35; }
.tut-tea-sheet { margin-top: 12px; padding-top: 12px; border-top: 2px dashed var(--line); }
.tut-spec .tut-tea-sheet .energy-cup { width: 64px; height: 64px; margin: 0 auto 6px; }
.tut-spec .tut-tea-sheet .energy-copy { font-size: .84rem; font-weight: 600; margin-bottom: 10px; }
.tut-spec .tut-tea-sheet .energy-timer { margin: 0; padding: 9px 14px; font-size: .9rem; }
/* exercise kinds */
.tut-kinds { flex: 1; display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 1fr; gap: 8px; }
.tut-kind { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; padding: 9px 4px 8px; border: 2px solid var(--line); border-radius: 12px; background: var(--surface); }
.tut-kind img { width: 30px; height: 30px; object-fit: contain; }
.tut-kind span { font-family: var(--font-display); font-weight: 800; font-size: .68rem; color: var(--navy); line-height: 1.1; }
/* reading aid: two sample words under the real ก/Aa pill */
.tut-word { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.tut-word-th { font-family: var(--font-thai); font-weight: 700; font-size: 2rem; color: var(--navy); line-height: 1.1; text-align: left; }
.tut-word-ro { display: block; font-family: var(--font-roman); font-weight: 600; font-size: .95rem; color: var(--sky-deep); text-align: left; }
.tut-word-id { display: block; font-size: .78rem; font-weight: 700; color: var(--ink-soft); text-align: left; }
.tut-word-2 { margin-top: 12px; padding-top: 12px; border-top: 2px dashed var(--line); }
/* XP and rank: the Profil XP card plus a sample weekly board */
.tut-board { display: flex; flex-direction: column; gap: 4px; margin-top: 10px; }
.tut-board div { display: flex; align-items: center; gap: 8px; padding: 3px 9px; border-radius: 9px; background: var(--surface); border: 2px solid var(--line); font-size: .74rem; font-weight: 800; color: var(--ink-2); }
.tut-board div b { width: 16px; color: var(--ink-faint); font-family: var(--font-display); }
.tut-board div span { flex: 1; text-align: left; }
.tut-board div.is-me { background: var(--navy-soft); border-color: var(--navy); color: var(--navy); }
.tut-board div.is-me b { color: var(--navy); }
/* Short phones (iPhone SE 667, small Android 640): a smaller mascot and
   tighter type so most cards fit without scrolling. */
@media (max-height: 720px) {
  .overlay.tour { padding-top: 62px; }
  .tut-card { padding-top: 70px; min-height: calc(100dvh - 82px); }
  .tut-card.is-hello { padding-top: 78px; }
  .tut-mascot { width: 104px; height: 104px; top: -48px; }
  .tut-card.is-hello .tut-mascot { width: 116px; height: 116px; top: -50px; }
  .tut-logo { width: 110px; margin-bottom: 4px; }
  .tut-step { margin-bottom: 5px; }
  .tut-title { font-size: 1.15rem; margin-bottom: 6px; }
  .tut-body { font-size: .86rem; line-height: 1.4; }
  .tut-spec { margin-top: 10px; padding: 10px; }
  .tut-list { gap: 3px; }
  .tut-list li { padding: 2px 8px; font-size: .8rem; }
  .tut-kind { padding: 6px 4px; }
  .tut-kind img { width: 24px; height: 24px; }
  .tut-word-th { font-size: 1.6rem; }
  .tut-tea-sheet { margin-top: 8px; padding-top: 8px; }
  .tut-spec .tut-tea-sheet .energy-cup { display: none; } /* the chip already shows the cup */
  .tut-legend { margin-top: 8px; padding-top: 8px; }
  .tut-board { gap: 2px; margin-top: 6px; }
  .tut-board div { padding: 1px 9px; font-size: .7rem; }
  .tut-card .tut-dots { padding-top: 12px; }
  .tut-card .btn-primary { margin-top: 10px; }
  .tut-card .pw-skip { margin-top: 8px; }
}

/* ---------- Card ---------- */
/* Question flashcard: sizes to its content (mascot + meta + prompt) and sits at
   the top of the practice frame; the answers fill the space below it. */
.card {
  position: relative;
  background: var(--surface);
  border: 3px solid var(--navy);
  border-radius: var(--r-xl);
  padding: 24px 20px 28px;
  box-shadow: 0 10px 0 0 var(--navy-deep), var(--lift-md);
  display: flex; flex-direction: column; gap: 14px;
  flex: 0 0 auto;
  min-height: 215px;   /* a bigger box, so the question sits with room to breathe */
  margin-bottom: 22px; /* separation between the question card and the answers */
}
/* Scrollable answer area below the question card: options before answering,
   reveal + example after. It fills the remaining frame and centers the options
   so 4 choices don't look sparse; `safe` falls back to top-align (scrollable)
   when the review is tall. Overflow lives here so option press-shadows survive. */
.answer-scroll {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: 14px;
  justify-content: safe center;
  padding: 2px 2px 6px; /* room for the option 3D bottom edges */
}
/* The box scrolls; its children never shrink. A child with its own min-height
   (.ord-line) loses the flex content floor, so under a tall prompt (the listen
   speaker) it was squeezed and its chips spilled under the reveal (task 96). */
.answer-scroll > * { flex-shrink: 0; }
.answer-scroll .options { gap: 12px; }
/* Multiple choice fills the frame, on the class card and in the journey
   exercise alike (both build their options inside an .answer-scroll, so this is
   the one copy of the rule): the options grid takes the leftover height and
   splits it evenly, so four choices spread down the screen instead of floating
   in a sea of empty canvas. Capped so a tall desktop window doesn't inflate
   them into slabs. Once answered the stretch releases (with the distractors
   collapsed to .gone), so the reveal + example rise into the fold. */
.answer-scroll .options:not(.answered) { flex: 1 0 auto; grid-auto-rows: 1fr; max-height: 480px; }
.card.dealt { animation: deal .42s cubic-bezier(.2,.8,.2,1) both; }

/* Mascot peeks over the top-right corner and reacts to the answer. The card
   is offset by the peek height so the mascot clears the practice bar. */
.question-card { margin-top: 42px; }
/* Short screens (small phones): the question card gives back its breathing
   room so all four answers make the fold; a still-taller set scrolls inside
   .answer-scroll as before. */
@media (max-height: 720px) {
  .question-card { min-height: 150px; padding: 14px 16px 18px; margin-bottom: 12px; }
  .prompt { font-size: 1.4rem; }
  .prompt.is-thai { font-size: 2.1rem; }
  .answer-scroll { gap: 10px; }
  .answer-scroll .options { gap: 9px; }
  .opt { padding: 10px 12px 10px 46px; }
}
.mascot { position: absolute; top: -46px; right: 10px; width: 88px; height: 92px; pointer-events: none; z-index: 2; }
.mascot img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; filter: drop-shadow(0 8px 10px rgba(46,44,139,.20)); }
.mascot .m-idle { animation: bob 3s ease-in-out infinite; }
.mascot .m-correct, .mascot .m-wrong { opacity: 0; transform: scale(.7); }
body.correct-answer .mascot .m-idle,
body.wrong-answer   .mascot .m-idle { opacity: 0; }
body.correct-answer .mascot .m-correct { opacity: 1; transform: scale(1); animation: pop-bounce .5s cubic-bezier(.2,1.3,.4,1) both; }
body.wrong-answer   .mascot .m-wrong   { opacity: 1; transform: scale(1); animation: mascot-shake .45s ease both; }

.card-meta { display: flex; align-items: center; justify-content: space-between; padding-right: 74px; }
.level-label { font-size: .62rem; text-transform: uppercase; letter-spacing: .7px; color: var(--ink-faint); font-weight: 800; }
.dir-pill {
  font-size: .72rem; font-weight: 800; color: var(--navy);
  background: var(--navy-soft); border: 2px solid #DEDDF6;
  padding: 4px 11px; border-radius: 999px;
}

/* Fill the card body and center the question, so there's even margin between
   the text and the box edges as the box grows. */
.prompt-wrap { text-align: center; padding: 8px 4px; flex: 1 1 auto; display: flex; flex-direction: column; justify-content: center; }
.prompt-tag { display: inline-block; font-size: .76rem; color: var(--ink-soft); font-weight: 800; margin-bottom: 8px; letter-spacing: .3px; }
.prompt {
  font-family: var(--font-display); font-weight: 700;
  font-size: 1.75rem; line-height: 1.25; color: var(--navy); word-break: break-word;
  /* Even lines instead of a full line plus a one-word orphan. */
  text-wrap: balance;
}
.prompt.is-thai { font-family: var(--font-thai); font-size: 2.8rem; font-weight: 700; letter-spacing: .5px; }
/* A sentence is not a word. setGlyphText (app.js) stamps data-len, so the type
   steps down as the prompt grows instead of wrapping single-word display size
   into three ragged lines; the leading opens up so the tap-word underline stops
   colliding with the next line's tone marks. Beats .prompt.is-thai everywhere
   (three compound parts), so the responsive sizes below stay untouched. */
.prompt.is-thai[data-len="md"] { font-size: 2.2rem; line-height: 1.4; letter-spacing: .2px; }
.prompt.is-thai[data-len="lg"] { font-size: 1.8rem; line-height: 1.5; letter-spacing: 0; }
.prompt.is-thai[data-len="xl"] { font-size: 1.5rem; line-height: 1.55; letter-spacing: 0; }
.prompt-roman, .prompt-clue { font-weight: 800; font-size: 1.05rem; margin-top: 6px; text-wrap: balance; }
.prompt-clue { font-family: var(--font-body); }
.prompt-roman { color: var(--green-deep); }
/* The homograph class chip. Quiet on purpose: it is a disambiguator, not a
   second question, so it reads as a label under the prompt rather than
   competing with it. */
.prompt-class {
  /* width:fit-content + auto margins, not inline-block: the prompt column is a
     stretch flex, so an inline-block still spans the card and reads as an input
     field rather than a label. */
  display: block; width: fit-content; margin: 8px auto 0; padding: 2px 10px;
  border: 1px solid var(--line, rgba(0,0,0,.12)); border-radius: 999px;
  font-size: .82rem; font-weight: 700; letter-spacing: .01em;
  color: var(--muted, #6E6D89); background: rgba(255,255,255,.6);
}
/* The cloze clue is a meaning, not a reading: it must not read as the line
   above it, which is the sentence's cara baca. */
.prompt-clue { color: var(--navy); }

/* ---------- Options (3D press tiles) ---------- */
.options { display: grid; gap: 11px; }
.opt {
  position: relative; text-align: left;
  background: var(--surface); border: 2.5px solid var(--line);
  color: var(--navy);
  font-family: var(--font-body); font-size: 1.04rem; font-weight: 700;
  padding: 13px 14px 13px 50px; border-radius: var(--r-md);
  box-shadow: 0 4px 0 0 var(--line);
  animation: rise .32s ease both;
}
.opt.is-thai { font-family: var(--font-thai); font-size: 1.35rem; font-weight: 600; }
.opt-roman { display: block; font-size: .8rem; font-weight: 700; color: var(--ink-soft); margin-top: 2px; }
.opt .badge {
  position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  width: 26px; height: 26px; border-radius: 8px; display: grid; place-items: center;
  background: var(--surface-2); border: 2px solid var(--line); color: var(--ink-soft);
  font-family: var(--font-display); font-size: .82rem; font-weight: 800;
}
/* Hover only where a real pointer exists. On touch the browser parks a sticky
   hover at the last tap point, so the tile that lands under the "Lanjut"
   button reads as pre-selected on every new card. */
@media (hover: hover) and (pointer: fine) {
  .opt:hover:not(:disabled) { border-color: var(--navy); box-shadow: 0 4px 0 0 var(--navy); transform: translateY(-1px); }
  .opt:hover:not(:disabled) .badge { border-color: var(--navy); color: var(--navy); }
}
.opt:disabled { cursor: default; }
.opt.correct { background: var(--green-soft); border-color: var(--green); color: var(--green-deep); box-shadow: 0 4px 0 0 var(--green-deep); }
.opt.correct .badge { background: var(--green); border-color: var(--green); color: #fff; }
.opt.wrong { background: var(--red-soft); border-color: var(--red); color: var(--red-deep); box-shadow: 0 4px 0 0 var(--red-deep); animation: shake .4s ease; }
/* Save failed (e.g. server hiccup): shake the tapped tile; the card re-arms. */
.opt.shake-err { animation: shake .4s ease; }
.opt.wrong .badge { background: var(--red); border-color: var(--red); color: #fff; }

/* After answering, the two un-picked distractors collapse to reclaim the fold. */
.opt.gone {
  padding-top: 0; padding-bottom: 0; margin: 0;
  max-height: 0; min-height: 0; opacity: 0; border-width: 0; box-shadow: none;
  pointer-events: none; overflow: hidden;
  transition: max-height .28s ease, opacity .2s ease, padding .28s ease;
}

/* ---------- Card matching (two columns of tap tiles) ---------- */
/* Fill the practice frame like .answer-scroll does, so a short round (few pairs)
   spreads down the screen instead of clustering at the top. */
.match { display: flex; flex-direction: column; gap: 12px; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.match-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.match-head .prompt-tag { margin-bottom: 0; }
/* Row-major fill: each row is [prompt, answer]; align-items:stretch (grid
   default) makes both cells take the row's height, so the taller Thai+roman
   tile sets the height its meaning counterpart matches. The grid grows to fill
   the board and align-content spreads the rows evenly (top, between, bottom), so
   fewer pairs -> more breathing room rather than a gap at the bottom. */
.match-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; flex: 1 1 auto; align-content: space-evenly; }
.mtile {
  min-height: 62px; text-align: center;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: var(--surface); border: 2.5px solid var(--line); color: var(--navy);
  font-family: var(--font-body); font-size: .98rem; font-weight: 700; line-height: 1.25;
  padding: 10px 8px; border-radius: var(--r-md);
  box-shadow: 0 4px 0 0 var(--line);
  animation: rise .32s ease both;
}
.mtile.is-thai { font-family: var(--font-thai); font-size: 1.25rem; font-weight: 600; }
/* Audio board: the prompt tile is the clip, so it shows a speaker instead of
   the word. Tapping plays it; picking it matches as any other tile does. */
.mtile.is-heard::after {
  content: ""; display: block; width: 26px; height: 26px;
  background-color: var(--sky-deep);
  -webkit-mask: var(--spk-ico) center/contain no-repeat;
          mask: var(--spk-ico) center/contain no-repeat;
}
.mtile.is-heard.playing::after { background-color: var(--green-deep); animation: spk-wave .6s linear infinite; }
@media (hover: hover) and (pointer: fine) {
  .mtile:hover:not(.matched) { border-color: var(--navy); box-shadow: 0 4px 0 0 var(--navy); transform: translateY(-1px); }
}
.mtile.sel { background: var(--navy-soft); border-color: var(--navy); box-shadow: 0 4px 0 0 var(--navy-deep); }
.mtile.miss { background: var(--red-soft); border-color: var(--red); color: var(--red-deep); animation: shake .4s ease; }
/* A matched pair stays in place (the grid must not reflow under a finger) but
   goes quiet, so only the unsolved tiles read as tappable. */
.mtile.matched {
  background: var(--green-soft); border-color: var(--green); color: var(--green-deep);
  box-shadow: none; opacity: .45; pointer-events: none;
  transition: opacity .3s ease, background .2s ease;
}

/* ---------- Reveal + example ---------- */
.afterthought { display: flex; flex-direction: column; gap: 11px; animation: rise .34s ease both; }
.reveal {
  background: var(--canvas-2); border: 2px solid var(--yellow); border-radius: var(--r-md);
  padding: 13px 15px; line-height: 1.45;
}
.reveal .r-head { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.reveal .r-thai { font-family: var(--font-thai); font-size: 1.5rem; font-weight: 700; color: var(--navy); }
.reveal .r-roman { color: var(--green-deep); font-weight: 800; font-size: 1rem; }
/* Not scoped to .reveal: a material card shows the same arti line. */
.r-meaning { font-weight: 800; margin-top: 3px; color: var(--navy); }

.example {
  background: var(--surface-2); border: 2px solid var(--line); border-radius: var(--r-md);
  padding: 13px 15px;
}
.example .ex-label { font-size: .64rem; text-transform: uppercase; letter-spacing: .8px; color: var(--sky); font-weight: 800; margin-bottom: 8px; }
.example .ex-thai { font-family: var(--font-thai); font-size: 1.4rem; font-weight: 600; line-height: 1.4; color: var(--navy); }
.example .ex-roman { color: var(--green-deep); font-weight: 800; font-size: .94rem; margin-top: 4px; }
.example .ex-meaning { color: var(--ink-soft); font-style: italic; font-size: .94rem; margin-top: 3px; font-weight: 600; }

/* Tap the Thai word to hear it (Chirp 3 HD audio). The word itself is the
   control: a dotted sky underline plus a small speaker glyph signal it, and the
   word turns green while playing. Sky = the audio accent. */
.tap-word {
  cursor: pointer;
  text-decoration: underline dotted var(--sky-deep);
  text-underline-offset: 7px;
  text-decoration-thickness: 2px;
  -webkit-tap-highlight-color: transparent;
  transition: color .15s, text-decoration-color .15s, transform .06s;
}
.tap-word::after {
  content: ""; display: inline-block; width: .5em; height: .5em;
  margin-left: .18em; vertical-align: .42em;
  background-color: var(--sky-deep);
  -webkit-mask: var(--spk-ico) center/contain no-repeat;
          mask: var(--spk-ico) center/contain no-repeat;
  transition: background-color .15s;
}
.tap-word:active { transform: translateY(1px); }
.tap-word.playing { color: var(--green-deep); text-decoration-color: var(--green); }
/* While playing, the glyph steps through equalizer frames: the word's own
   little sound wave. Same masked-SVG trick as the idle speaker. */
.tap-word.playing::after { background-color: var(--green-deep); animation: spk-wave .6s linear infinite; }
@keyframes spk-wave {
  0%, 100% { --spk-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Crect%20x='2'%20y='8'%20width='3.5'%20height='8'%20rx='1.75'/%3E%3Crect%20x='8'%20y='4'%20width='3.5'%20height='16'%20rx='1.75'/%3E%3Crect%20x='14'%20y='7'%20width='3.5'%20height='10'%20rx='1.75'/%3E%3Crect%20x='20'%20y='2'%20width='3.5'%20height='20'%20rx='1.75'/%3E%3C/svg%3E"); }
  33% { --spk-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Crect%20x='2'%20y='3'%20width='3.5'%20height='18'%20rx='1.75'/%3E%3Crect%20x='8'%20y='8'%20width='3.5'%20height='8'%20rx='1.75'/%3E%3Crect%20x='14'%20y='2'%20width='3.5'%20height='20'%20rx='1.75'/%3E%3Crect%20x='20'%20y='7'%20width='3.5'%20height='10'%20rx='1.75'/%3E%3C/svg%3E"); }
  66% { --spk-ico: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Crect%20x='2'%20y='6'%20width='3.5'%20height='12'%20rx='1.75'/%3E%3Crect%20x='8'%20y='1'%20width='3.5'%20height='22'%20rx='1.75'/%3E%3Crect%20x='14'%20y='8'%20width='3.5'%20height='8'%20rx='1.75'/%3E%3Crect%20x='20'%20y='4'%20width='3.5'%20height='16'%20rx='1.75'/%3E%3C/svg%3E"); }
}

/* Question prompt: keep the WORD centered and float the speaker glyph just to
   its right, so the icon never shifts the word off-center. */
/* Reserve the glyph's width (.5em + .22em gap) so a full-width wrapped
   prompt keeps the icon inside the card. */
.prompt.tap-word { display: inline-block; align-self: center; position: relative; max-width: calc(100% - .8em); }
.prompt.tap-word::after {
  position: absolute; left: 100%; top: 50%; transform: translateY(-50%);
  margin-left: .22em; width: .5em; height: .5em; vertical-align: baseline;
}
/* Anything longer than a word may wrap, and then there is no single center to
   protect while a glyph floating beside the block reads as detached: let it ride
   at the end of the last line instead (the inline default), and give the text
   the full width; one less wrap for a phrase that only just overflowed. */
.prompt.is-thai:not([data-len="sm"]).tap-word { max-width: 100%; }
.prompt.is-thai:not([data-len="sm"]).tap-word::after { position: static; transform: none; vertical-align: .3em; }
@media (prefers-reduced-motion: reduce) {
  .tap-word, .tap-word::after { transition: none; }
  .tap-word:active { transform: none; }
  .tap-word.playing::after { animation: none; }
}

.bd-toggle {
  margin-top: 11px; width: 100%;
  display: flex; align-items: center; justify-content: center; gap: 7px;
  border: 2px dashed var(--line); background: transparent; cursor: pointer;
  font-family: var(--font-display); font-size: .82rem; font-weight: 800; color: var(--navy);
  padding: 9px 12px; border-radius: var(--r-sm); transition: background .15s ease, border-color .15s ease;
}
.bd-toggle:hover { background: var(--surface); border-color: var(--navy); }
.bd-toggle .chev { transition: transform .2s ease; font-size: .7rem; }
.bd-toggle.open .chev { transform: rotate(180deg); }
.breakdown { margin-top: 11px; border-top: 2px dashed var(--line); padding-top: 11px; display: flex; flex-direction: column; gap: 8px; }
.breakdown.collapsed { display: none; }
.bd-row { display: grid; grid-template-columns: minmax(54px,auto) minmax(78px,auto) 1fr; gap: 12px; align-items: baseline; font-size: .92rem; }
.bd-thai { font-family: var(--font-thai); font-weight: 600; color: var(--navy); }
.bd-roman { color: var(--green-deep); font-weight: 700; }
.bd-arti { color: var(--ink-soft); font-weight: 600; }

/* ---------- Primary + ghost buttons ---------- */
.card-actions { display: flex; flex: 0 0 auto; }
/* Once answered, Lanjut owns the bottom band; drop the pre-answer footer/hint. */
body.answered .footer-row { display: none; }
.btn-primary {
  width: 100%;
  font-family: var(--font-display); font-size: 1.08rem; font-weight: 800; color: var(--navy);
  padding: 15px; border-radius: var(--r-md);
  background: var(--yellow);
  box-shadow: 0 5px 0 0 var(--yellow-deep);
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  letter-spacing: .2px;
}
.btn-primary:hover:not(:disabled) { filter: brightness(1.03); }
.btn-primary:disabled { opacity: .55; cursor: default; }
.kbd { font-family: var(--font-body); font-size: .72rem; font-weight: 800; background: rgba(46,44,139,.14); color: var(--navy); border-radius: 6px; padding: 2px 7px; }

.footer-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }

.btn-ghost {
  --edge: #D8D7EE; --lift: 4px;
  border: 2px solid var(--line); background: var(--surface); color: var(--ink-soft);
  font-family: var(--font-display); font-size: .84rem; font-weight: 800;
  padding: 10px 18px; border-radius: 999px; box-shadow: 0 4px 0 0 #E4E3F2;
}
.btn-ghost:hover { border-color: var(--red); color: var(--red-deep); box-shadow: 0 4px 0 0 var(--red); }

/* ---------- Home landing (quick-start + class picker) ---------- */
/* Practice is a full-height flex frame: header + question stay fixed, the card
   flexes to fill, and Lanjut sits at the bottom, so the layout is identical
   between the question and the answer-review (only the scroll area's content
   swaps). */
.class-practice:not([hidden]) {
  display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; gap: 11px;
}
.class-start:not([hidden]) {
  display: flex; flex-direction: column; gap: 13px; animation: fade .25s ease both;
  flex: 1 1 auto; min-height: 0; overflow-y: auto; /* scroll the class list, not the page */
}

/* Top bar: greeting + two real stat chips (mastered / due). */
.gchip {
  display: flex; align-items: center; gap: 5px;
  border: 2px solid var(--line); border-radius: 999px;
  padding: 6px 12px 6px 9px; font-family: var(--font-display); font-weight: 800; font-size: .9rem;
}
.gchip .ic { width: 19px; height: 19px; display: block; flex: 0 0 auto; }
.gchip .profile-trophy-icon {
  width: 20px;
  height: 20px;
  display: block;
  flex: 0 0 auto;
  margin-left: -1px;
  object-fit: contain;
}
.gchip b { min-width: 3ch; text-align: center; font-variant-numeric: tabular-nums; }
.gchip .ic path, .gchip .ic rect { stroke: var(--navy); stroke-width: 2; stroke-linejoin: round; }
.gchip.mastered { background: #F1EEFF; border-color: #CFC7FF; box-shadow: 0 3px 0 0 #CFC7FF; }
.gchip.mastered b { color: #5948C8; }
.gchip.due { background: var(--red-soft); border-color: #FBD4D9; box-shadow: 0 3px 0 0 #FBD4D9; }
.gchip.due b { color: var(--red-deep); }
.gchip.streak { background: #FFF3E0; border-color: #FFE0B2; box-shadow: 0 3px 0 0 #FFE0B2;
  position: relative; font: inherit; font-family: var(--font-display); font-weight: 800; font-size: .9rem;
  color: inherit; cursor: pointer; -webkit-appearance: none; appearance: none; }
.gchip.streak b { color: #E8710A; }
.gchip.streak:active { transform: translateY(2px); box-shadow: 0 1px 0 0 #FFE0B2; }
.gchip.streak .profile-fire-icon { overflow: visible; }
/* Streak in embers (task 125): one or two days missed, the count is kept. The
   chip goes to a dim brick red; the last grace day adds the red "needs you" dot. */
.gchip.streak.bara { background: #FBEDE6; border-color: #F2CFC0; box-shadow: 0 3px 0 0 #F2CFC0; }
.gchip.streak.bara b { color: #8C2F12; }
.gchip.streak.bara:active { box-shadow: 0 1px 0 0 #F2CFC0; }
.gchip.streak.bara.last::after { content: ""; position: absolute; top: -3px; left: -3px; width: 10px; height: 10px;
  border-radius: 50%; background: var(--red); border: 2px solid var(--surface); }

/* Streak flame states (task 125). #profileFireMark is one <symbol> used by
   every chip, the streak sheet and the day strip; CSS variables cross into
   each <use>, so the host's class sets the look:
     (none)       lit: the normal flame
     .ember       one or two days missed: the outer tongue is gone, a small dim
                  tongue flickers unevenly on the coal
     .ember.last  the last grace day: a fingernail-sized ember breathing
                  slowly, ash falling
   Design A in design-systems/mockups/streak-bara-variants.html. */
.fm-outer { fill: var(--fm-outer-fill, url(#profileFireOuter)); opacity: var(--fm-outer-op, 1); }
.fm-inner { fill: var(--fm-inner-fill, url(#profileFireInner)); transform: var(--fm-inner-tf, none);
  transform-box: fill-box; transform-origin: 50% 100%; animation: var(--fm-inner-anim, none); }
.fm-coal { fill: var(--fm-coal-fill, #FFD03A); transform: var(--fm-coal-tf, none); transform-box: fill-box; transform-origin: 50% 100%; }
.fm-ash { display: var(--fm-ash, none); fill: var(--ink-faint); }
.fm-ash circle { animation: baraAsh 3.4s linear infinite; }
.fm-ash circle:nth-child(2) { animation-delay: -1.1s; }
.fm-ash circle:nth-child(3) { animation-delay: -2.3s; }
.ember { --fm-outer-op: 0; --fm-inner-fill: #B9471F; --fm-inner-tf: scale(.8); --fm-inner-anim: baraGutter 1.7s linear infinite; --fm-coal-fill: #5E2510; }
.ember.last { --fm-inner-fill: #A13A17; --fm-inner-tf: scale(.42); --fm-inner-anim: baraDying 3.4s ease-in-out infinite; --fm-coal-fill: #3E180A; --fm-coal-tf: scale(.85); --fm-ash: block; }
/* gutter: a flame fighting for air; dying: slow breaths with long dark gaps. */
@keyframes baraGutter { 0% { opacity: 1; } 7% { opacity: .5; } 12% { opacity: 1; } 30% { opacity: .85; } 34% { opacity: .3; } 40% { opacity: .95; } 60% { opacity: .7; } 63% { opacity: 1; } 80% { opacity: .45; } 86% { opacity: .9; } 100% { opacity: 1; } }
@keyframes baraDying { 0%, 100% { opacity: .22; } 35% { opacity: .9; } 50% { opacity: .3; } 70% { opacity: .8; } }
@keyframes baraAsh { 0% { opacity: 0; transform: translateY(0); } 15% { opacity: .9; } 100% { opacity: 0; transform: translateY(58px); } }
@media (prefers-reduced-motion: reduce) {
  .ember, .ember.last { --fm-inner-anim: none; --fm-ash: none; }
}

/* Streak sheet (opened from a streak chip): the thai tea sheet's chassis
   (.energy-sheet) plus the flame with Nong Jay, the 7-day strip and the
   rule line. Day cells: on = practised, gap = skipped since the last practice
   (ember), today = dashed question mark until practised, last = last grace day. */
.streak-sheet .ss-fig { position: relative; height: 112px; margin: 0 auto 6px; display: flex; align-items: flex-end; justify-content: center; gap: 2px; }
.streak-sheet .ss-flame { width: 96px; height: 96px; overflow: visible; }
.streak-sheet .ss-fig img { width: 88px; height: auto; margin-bottom: -4px; }
.ss-days { display: flex; justify-content: space-between; gap: 5px; margin: 6px 0 14px; }
.sd { flex: 1 1 0; display: flex; flex-direction: column; align-items: center; gap: 5px; font-size: .62rem; font-weight: 900;
  letter-spacing: .04em; text-transform: uppercase; color: var(--ink-faint); }
.sd i { width: 34px; height: 34px; border-radius: 12px; display: grid; place-items: center; background: var(--navy-soft); border: 2px solid transparent; }
.sd .sd-flame { width: 20px; height: 20px; overflow: visible; }
.sd.on { color: #E8710A; } .sd.on i { background: #FFF3E0; border-color: #FFE0B2; }
.sd.on.now { color: var(--navy); } .sd.on.now i { border-color: var(--navy); }
.sd.gap { color: #8C2F12; } .sd.gap i { background: #FBEDE6; border-color: #F2CFC0; }
.sd.today:not(.on) { color: var(--navy); } .sd.today:not(.on) i { background: var(--surface); border: 2px dashed var(--navy); }
.sd.today:not(.on) i::after { content: "?"; font-family: var(--font-display); font-weight: 800; font-size: 1rem; color: inherit; }
.sd.today.last { color: var(--red-deep); } .sd.today.last i { border-color: var(--red-deep); }
.sd b { font-family: var(--font-display); font-size: .6rem; letter-spacing: 0; text-transform: none; color: inherit; }
.energy-sheet .energy-timer.last, .energy-sheet .energy-timer.last b { color: var(--red-deep); }
.streak-sheet .btn-primary { width: 100%; }
.streak-sheet .ss-rule { margin: 10px 0 0; font-size: .74rem; font-weight: 600; color: var(--ink-faint); line-height: 1.45; }
/* Thai tea = the daily practice energy. Tappable (it explains itself and hands
   out the free refill), so it is a button; drained, it goes grey. */
.gchip.energy { background: #F3ECFF; border-color: #D8CBF7; box-shadow: 0 3px 0 0 #D8CBF7;
  font: inherit; font-family: var(--font-display); font-weight: 800; font-size: .9rem;
  color: inherit; cursor: pointer; -webkit-appearance: none; appearance: none; }
.gchip.energy b { color: #6847B5; }
.gchip.energy:active { transform: translateY(2px); box-shadow: 0 1px 0 0 #D8CBF7; }
.gchip.energy.empty { background: var(--surface-2); border-color: var(--line); box-shadow: 0 3px 0 0 var(--line); }
.gchip.energy.empty b { color: var(--ink-faint); }
.gchip .gchip-tea { width: 21px; height: 21px; display: block; flex: 0 0 auto; margin: -3px 0 -3px -2px; }

/* Out-of-tea sheet: the glass, one line of rules, the countdown to the next
   glass, and (admins only) a refill button. */
.energy-sheet { text-align: center; }
.energy-sheet .energy-cup { width: 96px; height: 96px; display: block; margin: 4px auto 10px; }
.energy-sheet .energy-copy { margin: 0 0 14px; color: var(--ink-2); line-height: 1.5; }
/* The countdown is the answer to "when can I practise again", so it reads as a
   plate rather than a caption; the digits are tabular so they do not jitter. */
.energy-sheet .energy-timer {
  margin: 0 0 18px; padding: 12px 16px; border-radius: var(--r-lg);
  background: #F3ECFF; color: #6847B5; font-weight: 700;
}
.energy-sheet .energy-timer b {
  font-family: var(--font-display); font-size: 1.25rem;
  font-variant-numeric: tabular-nums; margin-left: 6px;
}
.energy-sheet .energy-refill {
  width: 100%; padding: 14px; border: 0; border-radius: var(--r-lg);
  background: var(--green); color: #fff; box-shadow: 0 4px 0 0 var(--green-deep);
  font-family: var(--font-display); font-weight: 800; font-size: 1rem; cursor: pointer;
}
.energy-sheet .energy-refill:active { transform: translateY(3px); box-shadow: 0 1px 0 0 var(--green-deep); }
/* Quit confirmation: staying is the primary, leaving the quiet way out. */
.quit-sheet .btn-primary, .quit-sheet .drill-exit { width: 100%; }
.quit-sheet .drill-exit { margin-top: 10px; }

/* Profile streak flame icon: reused in the player card plate and activity headline. */
.profile-fire-icon {
  width: 1.05em;
  height: 1.05em;
  display: inline-block;
  vertical-align: -.25em;
  flex: 0 0 auto;
}
.k-badge .n .profile-fire-icon { width: 1.08em; height: 1.08em; vertical-align: -.3em; }
.k-badge .n .k-acc-icon { width: 1.08em; height: 1.08em; vertical-align: -.3em; }
.k-badge .n .k-xp-icon { width: 1.08em; height: 1.08em; vertical-align: -.3em; }
.cal-head .profile-fire-icon { width: 1.38em; height: 1.38em; vertical-align: -.43em; margin-right: 3px; }
.gchip.streak .profile-fire-icon { width: 20px; height: 20px; margin: -2px 0 -3px -1px; }

/* Profile streak heatmap: current run headline + 10-week activity grid. */
/* Activity heatmap (matches the Kartu Pemain mockup): week-major intensity grid */
.cal { background: var(--surface); border: 2px solid var(--line); border-radius: var(--r-lg);
  box-shadow: 0 4px 0 0 var(--line); padding: 15px 16px 14px; }
.cal-head { display: flex; align-items: center; font-weight: 800; font-size: .85rem; color: var(--ink-soft); margin-bottom: 11px; }
.cal-head b { font-family: var(--font-display); font-size: 1.25rem; color: #E8710A; margin: 0 5px; }
.cal-head .rec { margin-left: auto; font-size: .72rem; font-weight: 900; color: var(--ink-faint); }
.cal-head .rec b { font-family: var(--font-body); font-size: .72rem; color: #E8710A; margin: 0 3px; }
.cal-grid { display: grid; grid-template-rows: repeat(7, 13px); grid-auto-flow: column; gap: 5px; justify-content: start; }
.cal .cell { width: 13px; height: 13px; border-radius: 4px; background: var(--navy-soft); }
.cal .cell.lv1 { background: #FFDCA8; }
.cal .cell.lv2 { background: #FF8A1E; }
.cal .cell.lv3 { background: #E8710A; }
.cal .cell.today { outline: 2px solid var(--navy); outline-offset: 1px; }
/* A day the streak's grace rule bridged (task 125): dashed ember, not a hole. */
.cal .cell.bara { background: #FBEDE6; outline: 1.5px dashed #C9502A; outline-offset: -1.5px; }
.cal-legend { display: flex; align-items: center; gap: 5px; margin-top: 12px; font-size: .66rem; font-weight: 800; color: var(--ink-faint); }
.lg-cell { width: 11px; height: 11px; border-radius: 3px; }

/* Aktivitas: heatmap left, flame gauge right */
.cal-body { display: flex; gap: 18px; align-items: stretch; }
.cal-left { flex: 0 0 auto; }
.cal-right { flex: 1 1 auto; display: flex; flex-direction: column; align-items: center; justify-content: center;
  border-left: 2px solid var(--line); padding-left: 16px; min-width: 0; }

.gauge {
  --gauge-size: 176px;
  position: relative;
  width: var(--gauge-size);
  height: var(--gauge-size);
  display: grid;
  place-items: center;
  isolation: isolate;
}
.gauge::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background:
    radial-gradient(circle at 50% 52%, rgba(255,215,148,.66) 0 41%, rgba(255,239,207,.62) 58%, rgba(255,255,255,.9) 73%),
    #fff;
  border: max(8px, calc(var(--gauge-size) * .06)) solid #ECECFF;
  box-shadow: inset 0 -10px 24px rgba(255,178,77,.12), 0 12px 28px -20px rgba(46,44,139,.45);
  z-index: -2;
  animation: streakBadgeBreathe 4.8s ease-in-out infinite;
}
.gauge::after {
  content: "";
  position: absolute;
  inset: 13%;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 58%, rgba(255,152,42,.28), transparent 66%);
  filter: blur(4px);
  z-index: -1;
  animation: streakGlowPulse 3.6s ease-in-out infinite;
}
.g-ring { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); overflow: visible; opacity: 0; pointer-events: none; }
.g-track { fill: none; stroke: var(--navy-soft); stroke-width: 12; }
.g-prog { fill: none; stroke: url(#streakRingGrad); stroke-width: 12; stroke-linecap: round;
  filter: drop-shadow(0 2px 4px rgba(232,113,10,.4)); transition: stroke-dashoffset 1.1s cubic-bezier(.4,0,.2,1); }
.g-rec { fill: none; stroke: var(--navy); stroke-width: 14; stroke-linecap: butt; }

.flame-svg {
  width: 74%;
  height: auto;
  overflow: visible;
  filter: drop-shadow(0 8px 10px rgba(205,78,0,.20));
}
.flame-svg path {
  transform-box: fill-box;
  transform-origin: 50% 100%;
}
.flame-outer-mark {
  animation: streakOuterFlame 3.4s ease-in-out infinite;
}
.flame-inner-mark {
  animation: streakInnerFlame 2.25s ease-in-out infinite;
}
.flame-coal-mark {
  animation: streakCoalFlame 2.9s ease-in-out infinite;
}
.g-num {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 52px;
  fill: #6B2400;
  letter-spacing: 0;
  text-anchor: middle;
  dominant-baseline: central;
  paint-order: stroke fill;
  stroke: rgba(255,179,71,.18);
  stroke-width: 2px;
}

/* Belum beruntun: streak 0 puts the flame out until the next practice day. */
.gauge.dormant::before { animation: none; }
.gauge.dormant::after { display: none; }
.gauge.dormant .flame-svg { filter: grayscale(1) opacity(.45); }
.gauge.dormant .flame-svg path { animation: none; }
.gauge.dormant .g-num { fill: var(--ink-soft); stroke: none; }

/* Embers (task 125): the gauge's own flame paths follow design A, the halo
   cools to a dim peach, and the digits keep their colour behind a light
   stroke so they stay readable over the ember, still or moving. */
.gauge.bara::before { animation: none;
  background: radial-gradient(circle at 50% 52%, rgba(242,207,192,.7) 0 41%, rgba(251,237,230,.75) 58%, rgba(255,255,255,.9) 73%), #fff;
  box-shadow: inset 0 -10px 24px rgba(201,80,42,.12), 0 12px 28px -20px rgba(46,44,139,.45); }
.gauge.bara::after { background: radial-gradient(circle at 50% 58%, rgba(201,80,42,.26), transparent 66%); animation-duration: 1.7s; }
.gauge.bara .flame-svg { overflow: visible; filter: drop-shadow(0 8px 10px rgba(140,47,18,.2)); }
.gauge.bara .flame-outer-mark { opacity: 0; animation: none; }
.gauge.bara .flame-inner-mark { fill: #B9471F; transform: scale(.8); animation: baraGutter 1.7s linear infinite; }
.gauge.bara .flame-coal-mark { fill: #5E2510; animation: none; }
.gauge.bara.last .flame-inner-mark { fill: #A13A17; transform: scale(.42); animation: baraDying 3.4s ease-in-out infinite; }
.gauge.bara.last .flame-coal-mark { fill: #3E180A; transform: scale(.85); }
.gauge.bara.last .fm-ash { display: block; }
.gauge.bara .g-num { fill: #6B2400; stroke: rgba(255,255,255,.9); stroke-width: 6px; }
@media (prefers-reduced-motion: reduce) {
  .gauge.bara .flame-svg path, .gauge.bara::after { animation: none; }
  .gauge.bara.last .fm-ash { display: none; }
}

.g-unit { margin-top: 4px; font-family: var(--font-display); font-weight: 800; font-size: .8rem; color: var(--ink-soft); }
.g-rec-cap { margin-top: 6px; font-size: .68rem; font-weight: 900; letter-spacing: .02em; color: var(--ink-faint); text-align: center; }
.g-rec-cap b { font-family: var(--font-body); color: #E8710A; margin: 0 2px; }
.g-rec-cap.is-best { color: #E8710A; }

@keyframes streakBadgeBreathe {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.025); }
}
@keyframes streakGlowPulse {
  0%, 100% { opacity: .72; transform: scale(1); }
  50% { opacity: 1; transform: scale(1.08); }
}
@keyframes streakOuterFlame {
  0%, 100% { transform: scaleY(1) skewX(0deg); }
  24% { transform: scaleY(1.018) skewX(-.8deg); }
  52% { transform: scaleY(.99) skewX(.7deg); }
  78% { transform: scaleY(1.014) skewX(-.35deg); }
}
@keyframes streakInnerFlame {
  0%, 100% { transform: scaleX(1) scaleY(1) translateY(0); }
  34% { transform: scaleX(.96) scaleY(1.07) translateY(-3px); }
  68% { transform: scaleX(1.04) scaleY(.98) translateY(1px); }
}
@keyframes streakCoalFlame {
  0%, 100% { transform: scaleX(1) translateY(0); opacity: .96; }
  50% { transform: scaleX(1.08) translateY(-1px); opacity: 1; }
}

@media (max-width: 440px) {
  /* keep heatmap + gauge on one row; shrink both to fit a narrow phone */
  .cal-body { gap: 10px; align-items: center; }
  .cal-left { min-width: 0; }
  .cal-grid { grid-template-rows: repeat(7, 11px); gap: 4px; }
  .cal .cell { width: 11px; height: 11px; }
  .cal-legend { gap: 4px; font-size: .58rem; flex-wrap: wrap; }
  .cal-right { padding-left: 10px; }
  .gauge { --gauge-size: 118px; }
}

/* Hero: the shared page header (.ah-top, defined with the Beranda tab) on the
   dotted canvas, then the due pile as the shared night-sky card (.ah-hero
   .ah-next) with the three practice books floating where that card carries
   its mascot. */
.hub-banner { flex: 0 0 auto; padding: 2px 2px 0; animation: pop .5s cubic-bezier(.2,.9,.3,1.1) both; }
.hub-banner .ah-top { padding: 0; }
.hub-go { margin-top: 12px; }
/* The due count is the stat this card exists for, so it reads as a stat: the
   number on its own line, the sentence as its label. Inline, the oversized
   number wrapped the sentence into an orphan on narrow phones. The zero state
   has no <b> and keeps the plain headline (hence :has). */
.ah-next.hub-go h1:has(b) { font-size: .92rem; font-weight: 800; color: rgba(255,255,255,.8); }
.hub-go h1 b {
  display: block; margin-bottom: 1px;
  font-size: 2.7rem; line-height: 1; color: var(--yellow);
  text-shadow: 0 2px 0 rgba(23,19,68,.35);
}
/* The art is landscape and carries its own glow, so it centers in the card and
   bleeds past the right padding instead of standing on the floor like the
   mascot does. Same bob as the quest-card art. */
.hub-go .hub-art {
  width: 190px; align-self: center; margin: 0 -20px 0 -8px;
  animation: v1Bob 4.6s ease-in-out infinite;
}
@media (min-width: 560px) {
  .hub-go .hub-art { width: 236px; }
}
@media (prefers-reduced-motion: reduce) {
  .ah-stars, .ah-next-art { animation: none; }
  .quickstart::after { content: none; }
}

/* Primary CTA: one big "Main sekarang", the signature 3D press. */
.quickstart {
  position: relative; overflow: hidden;
  display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%;
  background: var(--yellow); border: 0; border-radius: var(--r-lg); color: var(--navy);
  font-family: var(--font-display); font-weight: 800; font-size: 1.2rem; padding: 16px;
  cursor: pointer; box-shadow: 0 6px 0 0 var(--yellow-deep), var(--lift-md);
  transition: transform .08s ease, box-shadow .08s ease, filter .15s ease;
}
/* One light pass every few seconds. This is the only yellow on a dark card, so
   a sheen is enough to say "press me"; no pulse, no scale, nothing that moves
   the layout. */
.quickstart::after {
  content: ""; position: absolute; inset: 0 auto 0 -60%; width: 38%; pointer-events: none;
  background: linear-gradient(100deg, transparent, rgba(255,255,255,.55), transparent);
  animation: qsSheen 4.2s ease-in-out infinite;
}
@keyframes qsSheen { 0%, 55% { transform: translateX(0); } 100% { transform: translateX(500%); } }
.quickstart:hover { filter: brightness(1.03); }
.quickstart:active { transform: translateY(5px); box-shadow: 0 1px 0 0 var(--yellow-deep); }
.qs-play { font-size: .8rem; }

/* "Sinar ajaib" quest card (design-systems/home-widgets.html, widget 02/03
   family), here carrying mastered-word review. The card base, body text, and
   CTA are the shipped invite classes; this only adds the glow + halo layer. */
/* flex-shrink 0: the card sits directly in the home-start flex column, and its
   overflow:hidden would otherwise let flex squeeze it to a clipped sliver. */
.quest { margin: 0 2px; flex: 0 0 auto; }
.quest::before {
  content: ""; position: absolute; top: -52px; right: -52px; width: 200px; height: 200px; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,225,0,.30) 0%, rgba(242,192,55,.14) 46%, transparent 70%);
  pointer-events: none;
}
.quest-top { display: flex; gap: 12px; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.v1-halo { position: relative; flex: 0 0 auto; width: 84px; height: 84px; isolation: isolate; }
.v1-halo img { display: block; animation: v1Bob 4.6s ease-in-out infinite; will-change: transform; }
.v1-halo::before {
  content: ""; position: absolute; inset: 10%; border-radius: 50%; z-index: -1;
  background: radial-gradient(circle at 50% 58%, rgba(242,192,55,.45), transparent 68%);
  filter: blur(5px); animation: v1Glow 3.8s ease-in-out infinite;
}
.v1-spark {
  position: absolute; display: block; pointer-events: none;
  clip-path: polygon(50% 0, 63% 37%, 100% 50%, 63% 63%, 50% 100%, 37% 63%, 0 50%, 37% 37%);
  animation: v1Twinkle 2.6s ease-in-out infinite;
}
.v1-s1 { width: 16px; height: 16px; top: -8px; left: -12px; background: var(--yellow); }
.v1-s2 { width: 10px; height: 10px; top: 30px; left: -20px; background: #E8930C; animation-delay: .9s; animation-duration: 3.1s; }
.v1-s3 { width: 12px; height: 12px; top: -13px; right: 2px; background: var(--gold); animation-delay: 1.7s; animation-duration: 2.2s; }
@keyframes v1Bob {
  0%, 100% { transform: translateY(0) rotate(-2deg); }
  50% { transform: translateY(-4px) rotate(2.5deg); }
}
@keyframes v1Glow { 0%, 100% { opacity: .65; transform: scale(1); } 50% { opacity: 1; transform: scale(1.1); } }
@keyframes v1Twinkle {
  0%, 100% { transform: scale(.35) rotate(0deg); opacity: .2; }
  50% { transform: scale(1) rotate(35deg); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .v1-halo img, .v1-halo::before, .v1-spark, .hub-go .hub-art { animation: none; }
  .v1-spark { opacity: .85; }
}

.class-head { display: flex; align-items: baseline; justify-content: space-between; margin: 6px 2px 0; }
.class-head-title { font-family: var(--font-display); font-weight: 800; font-size: 1.05rem; color: var(--navy); }
/* Subtle "browse other open classes" action; unobtrusive next to the title. */
.class-more {
  border: 0; background: transparent; cursor: pointer;
  font-family: var(--font-display); font-weight: 800; font-size: .8rem;
  color: var(--ink-soft); padding: 4px 9px; border-radius: 999px;
  transition: color .14s ease, background .14s ease;
}
.class-more:hover { color: var(--navy); background: var(--navy-soft); }
.class-more:active { transform: translateY(1px); }

/* One section per Kelas: a heading over its own shelf. */
.class-groups { display: flex; flex-direction: column; gap: 4px; }
.class-group-title { font-family: var(--font-display); font-weight: 800; font-size: .9rem; color: var(--navy); margin: 8px 2px 0; }

/* Classes as a horizontal shelf of tappable "stage" cards. */
.class-list {
  display: flex; gap: 11px; overflow-x: auto; overflow-y: hidden;
  padding: 4px 2px 10px; scroll-snap-type: x proximity;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.class-list::-webkit-scrollbar { display: none; }
.stage {
  scroll-snap-align: start; flex: 0 0 auto; width: 156px; text-align: left; cursor: pointer;
  position: relative;
  background: var(--surface); border: 2px solid var(--line); border-radius: var(--r-md);
  box-shadow: 0 4px 0 0 var(--line); padding: 13px 13px 14px;
  transition: transform .08s ease, box-shadow .08s ease, border-color .15s ease;
  animation: rise .4s cubic-bezier(.2,.9,.3,1) both; animation-delay: calc(var(--i, 0) * 55ms);
}
.stage:hover { border-color: var(--navy); }
.stage:active { transform: translateY(4px); box-shadow: 0 0 0 0 var(--line); }
.stage:focus-visible { outline: 3px solid var(--sky); outline-offset: 2px; }
.stage.complete { background: #F1EEFF; border-color: #CFC7FF; box-shadow: 0 4px 0 0 #CFC7FF; }
.stage-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 9px; }
.stage-flag {
  display: block;
  width: 30px;
  height: 30px;
  margin: -5px 0;
  object-fit: contain;
}
.stage-trophy {
  display: block;
  width: 30px;
  height: 30px;
  margin: -8px -4px -5px 0;
  object-fit: contain;
}
.stage-pill {
  font-family: var(--font-display); font-weight: 800; font-size: .74rem;
  padding: 3px 9px; border-radius: 999px; min-width: 22px; text-align: center;
}
/* Only the finished card carries a pill now; the counting ones are gone with the
   line they duplicated. */
.stage-pill.done { background: #E5DFFF; color: #5948C8; }
.stage-date { display: block; font-family: var(--font-display); font-weight: 800; font-size: 1rem; color: var(--navy); line-height: 1.15; }
.stage-sub { display: block; font-size: .72rem; font-weight: 700; color: var(--ink-faint); margin: 3px 0 10px; }
.stage-sub b { color: var(--ink-2); }
.mini { display: block; height: 8px; background: var(--navy-soft); border-radius: 999px; overflow: hidden; }
.mini i { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--green), var(--green-deep)); }
/* Mastery tiers: the grey is this bar's own track (words never answered right),
   so only the three coloured tiers are drawn. Purple matches the finished card
   below, because a fully mastered session is the same picture either way. */
.mini.tiers { display: flex; gap: 2px; }
.mini.tiers i { border-radius: 0; }
.mini.tiers i:first-child { border-radius: 999px 0 0 999px; }
.mini.tiers i:last-child { border-radius: 0 999px 999px 0; }
.mini.tiers i.t1 { background: var(--gold); }
.mini.tiers i.t2 { background: var(--green); }
.mini.tiers i.t3 { background: linear-gradient(90deg, #8D7AF2, #5948C8); }
.stage.complete .mini { background: #E1DCFF; }
.stage.complete .mini i { background: linear-gradient(90deg, #8D7AF2, #5948C8); }
/* One line, so every card on a shelf is the same height. */

/* Colour key for the tier bars, once above the whole shelf. */
.class-legend { display: flex; gap: 12px; flex-wrap: wrap; margin: 6px 2px 2px; font-size: .72rem; font-weight: 800; color: var(--ink-2); }
.class-legend b { display: inline-flex; align-items: center; gap: 5px; }
.class-legend s { width: 12px; height: 8px; border-radius: 999px; text-decoration: none; }
.class-legend .lg0 { background: var(--navy-soft); }
.class-legend .lg1 { background: var(--gold); }
.class-legend .lg2 { background: var(--green); }
.class-legend .lg3 { background: linear-gradient(90deg, #8D7AF2, #5948C8); }

.class-empty { width: 100%; text-align: center; color: var(--ink-faint); font-weight: 600; font-size: .88rem; padding: 16px 0; }

/* ================= "Belum ikut kelas?" invite + open-class sheet =================
   Shown in place of the class shelf when a learner has no practiceable vocab.
   openClassWidget() mounts the card; the sheet below is the generic openSheet(). */
.invite {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; text-align: left;
  background: var(--surface); border: 2px solid var(--line); border-radius: var(--r-lg);
  box-shadow: 0 5px 0 0 var(--line); padding: 16px 16px 14px; margin: 4px 2px 8px;
  animation: rise .4s cubic-bezier(.2,.9,.3,1) both;
}
/* Ticket look: the card reads as a class admission pass. A header row carries
   the "Undangan kelas" tag plus a BE-year serial (calm) or a rubber stamp with
   the open-class count; a perforated tear line separates body from CTA stub.
   overflow:hidden clips the notch circles into punched half-holes. */
.invite-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: -2px 0 10px; }
.invite-tag { font-family: var(--font-display); font-weight: 800; font-size: .6rem; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-faint); }
.invite-tag::before {
  content: ""; display: inline-block; width: 8px; height: 8px; border: 2px solid #D9D8F0;
  border-radius: 50%; margin-right: 7px; vertical-align: -1px;
}
.invite-serial {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 700; font-size: .62rem;
  letter-spacing: .08em; color: var(--ink-faint);
}
.invite-stamp {
  font-family: var(--font-display); font-weight: 800; font-size: .6rem; letter-spacing: .09em;
  text-transform: uppercase; white-space: nowrap; color: #E8930C;
  border: 2px solid #E8930C; border-radius: 7px; padding: 3px 9px 4px;
  background: rgba(232,147,12,.08); transform: rotate(-3deg);
  animation: inviteThump 3.4s ease-in-out infinite;
}
.invite-tear { position: relative; height: 0; border-top: 2px dashed #D9D8F0; margin: 2px 0 14px; }
.invite-tear::before, .invite-tear::after {
  content: ""; position: absolute; top: -9px; width: 16px; height: 16px; border-radius: 50%;
  background: var(--canvas); border: 2px solid var(--line);
}
.invite-tear::before { left: -24px; }
.invite-tear::after { right: -24px; }
.invite.calm .invite-tear { border-top-color: var(--line); }
.invite.calm img { opacity: .92; filter: saturate(.8); }
.invite-top { display: flex; gap: 12px; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.invite img { width: 84px; height: 84px; object-fit: contain; flex: 0 0 auto; }
@keyframes inviteThump {
  0%, 100% { transform: rotate(-3deg) scale(1); }
  50% { transform: rotate(-2deg) scale(1.04); }
}
.invite-body { min-width: 0; flex: 1 1 auto; }
.invite-body h3 { font-family: var(--font-display); font-weight: 800; font-size: 1.24rem; margin: 0; color: var(--navy); }
.invite-body p { font-size: .9rem; line-height: 1.45; color: var(--ink-soft); margin: 7px 0 0; font-weight: 600; }
.invite-body p b { color: #E8930C; font-weight: 900; }
.invite-cta {
  display: flex; align-items: center; gap: 8px; width: 100%; cursor: pointer; border: 0;
  text-decoration: none;
  font-family: var(--font-display); font-weight: 800; font-size: .96rem;
  background: var(--navy-soft); color: var(--navy); border-radius: var(--r-md); padding: 11px 16px;
  box-shadow: 0 4px 0 0 #D9D8F0; transition: transform .08s ease, box-shadow .08s ease;
}
.invite-cta .lbl { flex: 1 1 auto; text-align: center; }
.invite-cta .chev { flex: 0 0 auto; }
.invite-cta:active { transform: translateY(4px); box-shadow: 0 0 0 0 #D9D8F0; }
.invite-cta.primary { background: var(--yellow); box-shadow: 0 4px 0 0 var(--yellow-deep); }
.invite-cta.primary:active { box-shadow: 0 0 0 0 var(--yellow-deep); }

/* Generic bottom sheet (openSheet/closeSheet): one reusable element on <body>. */
.sheet-root { position: fixed; inset: 0; z-index: 1000; pointer-events: none; }
.sheet-root.open { pointer-events: auto; }
.sheet-scrim { position: absolute; inset: 0; background: rgba(35,33,95,.42); opacity: 0; transition: opacity .28s ease; }
.sheet-root.open .sheet-scrim { opacity: 1; }
.sheet-panel {
  position: absolute; left: 0; right: 0; bottom: 0; margin: 0 auto; max-width: 560px;
  background: var(--surface); border-radius: 26px 26px 0 0; max-height: 86dvh; overflow-y: auto;
  box-shadow: 0 -18px 40px -20px rgba(46,44,139,.5);
  padding: 8px 16px calc(18px + var(--safe-b));
  transform: translateY(102%); transition: transform .34s cubic-bezier(.2,.9,.3,1);
}
.sheet-root.open .sheet-panel { transform: translateY(0); }
.sheet-grab { display: block; width: 44px; height: 5px; border: 0; padding: 0; border-radius: 999px; background: var(--line); margin: 8px auto 14px; cursor: pointer; }
/* Explicit close, sticky so it stays reachable while a long sheet scrolls. */
.sheet-x { position: sticky; top: 6px; float: right; z-index: 3; display: grid; place-items: center;
  width: 34px; height: 34px; border: 0; padding: 0 0 3px; border-radius: 50%; cursor: pointer;
  background: var(--surface-2); color: var(--ink-soft); font-size: 1.5rem; line-height: 1; font-weight: 700;
  box-shadow: 0 2px 8px rgba(46,44,139,.18); }
.sheet-x:active { transform: scale(.92); }
.sheet-title { font-family: var(--font-display); font-weight: 800; font-size: 1.15rem; margin: 0 0 3px; color: var(--navy); }
.sheet-sub { font-size: .82rem; color: var(--ink-soft); font-weight: 600; margin: 0 0 14px; }

.oc-list { display: flex; flex-direction: column; }
/* The enrollment flow above the open-class list: three numbered steps. */
.oc-steps { margin: 0 0 14px; padding: 10px 12px 10px 30px; background: var(--navy-soft); border-radius: var(--r-sm);
  font-size: .8rem; font-weight: 600; color: var(--ink-2); line-height: 1.45; }
.oc-steps li + li { margin-top: 4px; }
.oc-steps li::marker { font-weight: 800; color: var(--navy); }
.oc-row { display: flex; align-items: center; gap: 12px; padding: 13px 0; border-top: 1px solid var(--line); }
.oc-row:first-child { border-top: 0; }
.oc-body { flex: 1 1 auto; min-width: 0; }
.oc-name { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 4px;
  font-family: var(--font-display); font-weight: 800; font-size: .95rem; color: var(--navy); }
.oc-badge { font-family: var(--font-display); font-weight: 800; font-size: .62rem; letter-spacing: .03em;
  text-transform: uppercase; padding: 3px 8px; border-radius: 999px; }
.oc-badge.t-private { background: var(--navy-soft); color: var(--navy); }
.oc-badge.t-semi { background: #E3F1F8; color: var(--sky-deep); }
.oc-badge.t-group { background: #FDEAF0; color: #C25378; }
.oc-badge.t-other { background: var(--surface-2); color: var(--ink-soft); }
.oc-sched { font-size: .78rem; font-weight: 700; color: var(--ink-soft); }
.oc-teacher { font-size: .76rem; font-weight: 700; color: var(--ink-faint); margin-top: 2px; }
.oc-join {
  flex: 0 0 auto; text-decoration: none; font-family: var(--font-display); font-weight: 800; font-size: .84rem;
  background: var(--yellow); color: var(--navy); border-radius: 12px; padding: 9px 15px;
  box-shadow: 0 4px 0 0 var(--yellow-deep); transition: transform .08s ease, box-shadow .08s ease;
}
.oc-join:active { transform: translateY(4px); box-shadow: 0 0 0 0 var(--yellow-deep); }
button.oc-join { border: 0; cursor: pointer; font: inherit; font-family: var(--font-display); font-weight: 800; font-size: .84rem; }
.oc-join:disabled { opacity: .6; cursor: default; }
.oc-state { flex: 0 0 auto; font-family: var(--font-display); font-weight: 800; font-size: .76rem; color: var(--navy);
  background: var(--navy-soft); border-radius: 999px; padding: 6px 11px; text-align: center; }
.oc-price { font-family: var(--font-display); font-weight: 800; font-size: .92rem; color: var(--ink-2); margin-top: 4px; }
.oc-price small { font-weight: 700; font-size: .74rem; color: var(--ink-faint); }
/* A live request: the state pill, with a quiet way out under it (Batalkan). */
.oc-live { flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 6px; }
.oc-live[hidden] { display: none; }
.oc-cancel { border: 0; background: none; cursor: pointer; font: inherit; font-size: .74rem; font-weight: 800;
  color: var(--ink-faint); text-decoration: underline; text-underline-offset: 3px; padding: 2px 4px; }
/* Batalkan asks once, across the row's full width, not in a second sheet. */
.oc-row.confirming { flex-wrap: wrap; }
.oc-confirm { flex: 1 0 100%; background: var(--red-soft); border-radius: var(--r-sm); padding: 11px 12px; }
.oc-confirm b { display: block; font-family: var(--font-display); color: var(--navy); font-size: .9rem; }
.oc-confirm p { margin: 2px 0 10px; font-size: .78rem; font-weight: 600; color: var(--ink-2); }
.oc-confirm .acts { display: flex; gap: 8px; justify-content: flex-end; }
.oc-confirm button { border: 0; cursor: pointer; font-family: var(--font-display); font-weight: 800; font-size: .8rem;
  border-radius: 10px; padding: 7px 13px; }
.oc-confirm button:disabled { opacity: .6; cursor: default; }
.oc-confirm .back { background: var(--surface-2); color: var(--navy); }
.oc-confirm .yes { background: var(--red-deep); color: #fff; }

@media (prefers-reduced-motion: reduce) {
  .sheet-scrim, .sheet-panel, .invite { transition: none; animation: none; }
  .invite-stamp { animation: none; }
}

/* Practice header: back to the landing + which slice you're drilling. The row's
   spare width belongs to the progress bar (#setBar, the .drill-bar the journey
   exercise wears), so the title ellipses before the bar gives up any room. */
.practice-head { display: flex; align-items: center; gap: 10px; min-width: 0; flex: 0 1 auto; }
.brand .drill-bar { min-width: 64px; }
.back-btn {
  flex: 0 0 auto; width: 38px; height: 38px; border-radius: 12px;
  border: 2px solid var(--line); background: var(--surface); color: var(--ink-soft);
  display: grid; place-items: center; cursor: pointer;
  box-shadow: var(--lift-sm); transition: all .15s ease;
}
.back-btn:hover { border-color: var(--navy); color: var(--navy); }
.back-btn svg { width: 20px; height: 20px; }
.practice-title {
  font-family: var(--font-display); font-weight: 800; font-size: 1.05rem; color: var(--navy);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ---------- Summary overlay ---------- */
.overlay {
  position: fixed; inset: 0; z-index: 50; padding: 64px 20px 20px;
  display: flex; align-items: center; justify-content: center;
  overflow-y: auto;
  animation: fade .25s ease both;
}
/* Scrim is its own layer behind the card, like .sheet-scrim. A backdrop-filter
   on the ancestor makes iOS Safari blur the modal's own text along with the
   backdrop; a sibling layer frosts the background only. */
.overlay::before {
  content: ""; position: fixed; inset: 0; z-index: -1;
  background: rgba(35,33,95,.55); backdrop-filter: blur(7px);
}
.summary-card {
  position: relative; background: var(--surface); border: 3px solid var(--navy);
  border-radius: var(--r-xl); padding: 30px 24px 24px; max-width: 460px; width: 100%;
  box-shadow: 0 12px 0 0 var(--navy-deep), var(--lift-lg); text-align: center;
  animation: pop .4s cubic-bezier(.2,.9,.3,1) both; margin: auto;
}
.summary-mascot { width: 96px; height: auto; display: block; margin: -74px auto 2px; filter: drop-shadow(0 10px 14px rgba(46,44,139,.22)); }
.summary-close { position: absolute; top: 12px; right: 14px; border: 0; background: var(--surface-2); width: 32px; height: 32px; border-radius: 50%; font-size: 1.3rem; line-height: 1; color: var(--ink-soft); cursor: pointer; }
.summary-close:hover { color: var(--navy); background: var(--navy-soft); }
.summary-kicker { font-size: .72rem; text-transform: uppercase; letter-spacing: 1px; color: var(--sky); font-weight: 800; }

.ring-wrap { position: relative; width: 132px; height: 132px; margin: 14px auto 16px; }
.ring { width: 132px; height: 132px; transform: rotate(-90deg); }
.ring-bg { fill: none; stroke: var(--line); stroke-width: 12; }
.ring-fg { fill: none; stroke: var(--green); stroke-width: 12; stroke-linecap: round; stroke-dasharray: 327; stroke-dashoffset: 327; transition: stroke-dashoffset .9s cubic-bezier(.2,.8,.2,1); }
.ring-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.ring-num { font-family: var(--font-display); font-weight: 800; font-size: 2.1rem; color: var(--navy); line-height: 1; }
.ring-lbl { font-size: .68rem; text-transform: uppercase; letter-spacing: .6px; color: var(--ink-faint); margin-top: 3px; font-weight: 800; }

.summary-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 9px; margin-bottom: 18px; }
.sstat { background: var(--surface-2); border: 2px solid var(--line); border-radius: var(--r-md); padding: 12px 4px; }
.sstat .num { display: block; font-family: var(--font-display); font-weight: 800; font-size: 1.4rem; line-height: 1; color: var(--navy); }
.sstat.ok .num { color: var(--green-deep); }
.sstat.bad .num { color: var(--red-deep); }
.sstat.streak .num { color: var(--gold); }
.sstat .lbl { display: block; font-size: .58rem; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .4px; margin-top: 5px; font-weight: 800; }

/* Two actions under the recap: play another set (primary) and leave. The leave
   button is the same .drill-exit the journey's finish screen and the quit sheet
   use, stretched to match the one above it. */
.summary-card .drill-exit { width: 100%; margin-top: 10px; }
.summary-miss { margin-bottom: 18px; text-align: left; }
.summary-miss h3 { font-size: .74rem; color: var(--red-deep); text-transform: uppercase; letter-spacing: .6px; margin: 0 0 9px; font-weight: 800; }
.summary-miss ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; max-height: 210px; overflow-y: auto; }
.summary-miss li { background: var(--red-soft); border: 2px solid #FBD4D9; border-radius: var(--r-sm); padding: 10px 13px; font-size: .94rem; font-weight: 600; }
.summary-miss li .m-thai { font-family: var(--font-thai); font-weight: 700; color: var(--navy); }
.summary-miss li .m-roman { color: var(--green-deep); font-weight: 700; }
.summary-miss li .m-arti { color: var(--ink-soft); }
.summary-miss .clean { background: var(--green-soft); border-color: #BFE9CF; color: var(--green-deep); padding: 12px 14px; border-radius: var(--r-sm); font-weight: 800; }

/* ---------- Views + bottom menu ---------- */
.view { display: flex; flex-direction: column; gap: 13px; }
/* The active view fills the space under the brand bar. Ranking/Profile scroll
   internally when long; Home has no view-level overflow so the practice card's
   3D shadow is never clipped (the answers scroll via .answer-scroll instead). */
.view:not([hidden]) { flex: 1 1 auto; min-height: 0; }
#viewRanking:not([hidden]), #viewProfile:not([hidden]) { overflow-y: auto; }
/* The scrollers end under the tabbar's edge; a little inner padding keeps the
   last card off the bar when scrolled to the bottom. */
#viewHome:not([hidden]), #viewHuruf:not([hidden]),
#viewRanking:not([hidden]), #viewProfile:not([hidden]) { padding-bottom: 10px; }
/* Profile stacks a few standalone cards; give them more air. The ranking
   profile peek clones those same cards into the sheet, so it takes its rhythm
   from this one rule instead of a second set of margins to keep in sync. */
#viewProfile, .profile-peek { display: flex; flex-direction: column; gap: 14px; }
.view-title { font-family: var(--font-display); font-weight: 800; color: var(--navy); font-size: 1.5rem; margin: 4px 0 0; text-align: center; }
.view-sub { color: var(--ink-soft); font-weight: 600; font-size: .9rem; line-height: 1.5; text-align: center; margin: 0 4px 6px; }

/* Practice is full-screen: hide the bottom menu and give its reserved space
   back to the practice frame (Lanjut then pins to the very bottom). */
body.practicing .tabbar { display: none; }
body.practicing .app { padding-bottom: calc(14px + var(--safe-b)); }

.tabbar {
  /* In-flow last child of the 100dvh .app column (not position:fixed): a fixed
     bar anchors to iOS Safari's layout viewport, which disagrees with dvh when
     the toolbar is dragged and strands the bar above the real bottom. Riding the
     dvh column keeps it glued to the visible bottom. The negative inline margin
     cancels .app's side padding so the bar stays edge-to-edge (matched per
     breakpoint below). The negative top margin swallows the column gap, so the
     scrolling view runs under the bar's edge instead of stopping at a hard line
     with a strip of bare canvas showing above the bar. */
  flex: none; min-height: calc(74px + var(--safe-b)); margin: calc(-1 * var(--app-gap)) -16px 0;
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 4px;
  /* Solid: the bar covers the scroller's clipped edge, and a translucent fill
     would show that edge as a hard line through the bar. */
  background: var(--surface); border: 1px solid var(--line); border-bottom: 0;
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  box-shadow: 0 -10px 30px -20px rgba(46,44,139,.34);
  padding: 9px 10px calc(7px + var(--safe-b));
}
/* Inactive tabs read in --ink-2 (8.3:1 on white); --ink-faint was 2.3:1 and
   failed AA. 12px/700 labels: 10px at weight 800 blurred on phones. */
.tabbar button {
  min-width: 0; min-height: 58px; border: 0; background: transparent; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  font-family: var(--font-display); font-weight: 700; font-size: .75rem; line-height: 1;
  color: var(--ink-2); padding: 8px 4px 7px; border-radius: var(--r-md);
  transition: color .15s ease, background .15s ease, transform .15s ease;
}
/* .tabbar button sets display:flex, which would otherwise override the hidden
   attribute on the admin-only Huruf tab; re-hide it explicitly. */
.tabbar button[hidden] { display: none; }
.tabbar button svg { width: 24px; height: 24px; flex: none; stroke-width: 2; }
/* Mouse only: an unguarded :hover sticks on the tapped tab on touch screens. */
@media (hover: hover) and (pointer: fine) {
  .tabbar button:hover { color: var(--navy); background: rgba(238,238,251,.5); }
}
.tabbar button.active { color: var(--navy); background: var(--navy-soft); }
.tabbar button:active { transform: translateY(1px); }

/* ---------- Ranking: "Papan Juara" ---------- */
/* Top 3 rise onto a 3D podium with confetti; ranks 4+ are a static list; a
   sticky self-summary bar keeps the viewer's own standing in view. Selectors
   are scoped to #viewRanking because the podium/list reuse short, generic
   class names (row, stage, xp, lv, nm) that also appear elsewhere. */
.rank-hero {
  position: relative; flex: none; overflow: hidden;
  display: flex; flex-direction: column; gap: 15px;
  padding: 22px 18px 0; border-radius: var(--r-xl);
  color: #fff;
  background:
    radial-gradient(90% 62% at 95% 2%, rgba(115,45,226,.72), transparent 68%),
    radial-gradient(95% 76% at 7% 1%, rgba(255,255,255,.08), transparent 65%),
    linear-gradient(155deg, #28216f 0%, #2d2384 48%, #211b67 100%);
  box-shadow: 0 8px 0 #171344, 0 16px 30px -20px rgba(23,19,68,.7),
    inset 0 1px 0 rgba(255,255,255,.2);
}
.rank-top { position: relative; z-index: 4; display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
.rank-tt { min-width: 0; padding-top: 2px; }
.rank-tt h1 {
  margin: 0; color: #fff;
  font-family: var(--font-display); font-weight: 800; font-size: 1.65rem; line-height: 1.05;
}
.rank-reset {
  flex: none; display: grid; grid-template-columns: auto auto; align-items: center; column-gap: 12px;
  min-width: 124px; border-radius: 999px; padding: 10px 18px;
  font-weight: 900; font-size: .72rem; color: rgba(255,255,255,.9);
  background: rgba(255,255,255,.12); border: 1px solid rgba(255,255,255,.06);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.08); line-height: 1.08;
}
.rank-reset b { color: #ff4c79; font-size: .78rem; }
.rank-lead {
  margin: 6px 0 0; color: rgba(255,255,255,.72);
  font-size: .82rem; font-weight: 800; line-height: 1.3;
}

/* Board toggle (Perjalanan / Kelas): yellow 3D segmented pill (#rankTabs). */
.rank-tabs {
  position: relative; z-index: 4; display: flex; gap: 4px;
  background: rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.04);
  border-radius: 999px; padding: 4px;
  box-shadow: inset 0 1px 1px rgba(255,255,255,.06);
}
.rank-tab {
  flex: 1; border: none; background: transparent; cursor: pointer;
  font-family: var(--font-display); font-weight: 800; font-size: .88rem; color: rgba(255,255,255,.88);
  border-radius: 999px; padding: 9px 4px 10px; transition: transform .06s ease, background .15s ease;
}
.rank-tab.is-on { color: var(--navy); background: var(--yellow); box-shadow: 0 3px 0 0 var(--yellow-deep); }
.rank-tab.is-on:active { transform: translateY(3px); box-shadow: 0 0 0 0 var(--yellow-deep); }

/* Period toggle (this week / all time): a strip at the foot of the card, under
   the podium steps, so it sits next to the numbers it changes. */
.rank-foot {
  position: relative; z-index: 3; display: flex; justify-content: center; gap: 22px;
  margin: -15px -18px 0; padding: 9px 0 10px; background: rgba(0,0,0,.28);
}
.rank-foot button {
  border: none; background: transparent; cursor: pointer; font-family: var(--font-display);
  font-weight: 800; font-size: .76rem; color: rgba(255,255,255,.6); padding: 2px 0 4px;
  border-bottom: 2px solid transparent; transition: color .15s ease;
}
.rank-foot button.is-on { color: var(--yellow); border-bottom-color: var(--yellow); }

/* The viewer's slot under the card: their nudge, or the class invite. */
.rank-slot:empty { display: none; }

/* ---- Podium stage: the top three feel like a medal ceremony ---- */
.rank-stage {
  /* flex-shrink:0 is load-bearing: overflow:hidden gives a flex item min-height:0,
     so inside the fixed-height scrolling view the stage would otherwise be
     squished to its padding and clip the podium. */
  position: relative; flex: none; overflow: hidden;
  margin: -4px -18px 0; padding: 4px 18px 0;
}
/* falling confetti; behind the podium here, behind the drill finish screen too */
.rain { position: absolute; inset: 0; z-index: 1; pointer-events: none; overflow: hidden; }
.rain i { position: absolute; top: -14px; width: 8px; height: 12px; border-radius: 2px; opacity: .9; animation: rankFall linear infinite; }
@keyframes rankFall {
  0% { transform: translateY(-14px) rotate(0deg); opacity: 0; }
  10% { opacity: .95; }
  /* --fall lets a taller host (the drill finish screen) carry it to the bottom */
  100% { transform: translateY(var(--fall, 320px)) rotate(420deg); opacity: .15; }
}
.rank-stage .podium { position: relative; z-index: 2; display: flex; align-items: flex-end; justify-content: center; gap: 8px; padding-top: 2px; }
.rank-stage .p-col { flex: 1; max-width: 118px; display: flex; flex-direction: column; align-items: center; }
.rank-stage .crown { font-size: 1.6rem; line-height: 1; filter: drop-shadow(0 3px 3px rgba(0,0,0,.25)); animation: bob 2.6s ease-in-out infinite; }
.rank-stage .p-mascot { position: relative; display: grid; place-items: center; filter: drop-shadow(0 6px 8px rgba(0,0,0,.28)); }
.rank-stage .p-mascot img { width: 100%; display: block; }
.rank-stage .p1 .p-mascot { width: 92px; }
.rank-stage .p1 .p-mascot img { animation: bob 3s ease-in-out infinite; }
.rank-stage .p2 .p-mascot { width: 70px; margin-bottom: 2px; }
.rank-stage .p3 .p-mascot { width: 66px; margin-bottom: 2px; }
/* sparkles around the champion */
.rank-stage .spark { position: absolute; font-size: .85rem; opacity: 0; animation: rankTwinkle 2.4s ease-in-out infinite; }
.rank-stage .spark.s1 { top: 2px; left: -6px; animation-delay: 0s; }
.rank-stage .spark.s2 { top: 14px; right: -8px; animation-delay: .8s; font-size: .7rem; }
.rank-stage .spark.s3 { bottom: 8px; left: -4px; animation-delay: 1.5s; font-size: .62rem; }
@keyframes rankTwinkle { 0%, 100% { opacity: 0; transform: scale(.6); } 50% { opacity: 1; transform: scale(1); } }
.rank-stage .p-badge {
  margin-top: -8px; width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center;
  font-family: var(--font-display); font-weight: 800; font-size: 1rem; color: var(--navy-deep);
  border: 3px solid #fff; position: relative; z-index: 3;
}
.rank-stage .p1 .p-badge { background: radial-gradient(circle at 38% 30%, #FFF0B0, var(--gold) 55%, var(--yellow-deep)); box-shadow: 0 3px 0 0 var(--yellow-deep); width: 40px; height: 40px; font-size: 1.15rem; }
.rank-stage .p2 .p-badge { background: radial-gradient(circle at 38% 30%, #fff, #cbb0ff 50%, #9b68e7); box-shadow: 0 3px 0 0 #7542bd; }
.rank-stage .p3 .p-badge { background: radial-gradient(circle at 38% 30%, #FFE7C2, #ffac48 50%, #e47713); box-shadow: 0 3px 0 0 #b95008; }
.rank-stage .p-name { color: #fff; font-family: var(--font-display); font-weight: 800; font-size: .95rem; line-height: 1.1; margin-top: 6px; text-align: center; white-space: nowrap; display: flex; align-items: center; gap: 5px; justify-content: center; max-width: 100%; }
.rank-stage .p1 .p-name { font-size: 1.05rem; }
.rank-stage .p-name .kamu { flex: none; font-family: var(--font-body); font-size: .56rem; font-weight: 900; letter-spacing: .4px; text-transform: uppercase; color: var(--navy-deep); background: var(--yellow); padding: 1px 6px; border-radius: 999px; }
.rank-stage .p-xp { color: var(--yellow); font-weight: 900; font-size: .82rem; font-variant-numeric: tabular-nums; margin-top: 1px; }
.rank-stage .p-meta { display: flex; gap: 5px; margin: 5px 0 8px; }
.rank-stage .p-tag { font-size: .62rem; font-weight: 900; color: #fff; background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.28); border-radius: 999px; padding: 2px 7px; line-height: 1.2; }
/* the physical steps */
.rank-stage .p-step { width: 100%; border-radius: 14px 14px 0 0; display: grid; place-items: start center; padding-top: 8px; font-family: var(--font-display); font-weight: 800; color: rgba(0,0,0,.32); font-size: 1.6rem; position: relative; }
.rank-stage .p-step::after { content: ""; position: absolute; top: 8px; left: 12px; right: 12px; height: 3px; border-radius: 3px; background: rgba(255,255,255,.35); }
.rank-stage .p1 .p-step { height: 104px; background: linear-gradient(180deg,#FFE99A,var(--gold)); box-shadow: inset 0 -10px 0 rgba(224,181,0,.55), 0 0 0 3px rgba(255,255,255,.10); }
.rank-stage .p2 .p-step { height: 74px; background: linear-gradient(180deg,#c584ff,#8d4de2); box-shadow: inset 0 -10px 0 rgba(91,45,154,.5), 0 0 0 3px rgba(255,255,255,.08); color: rgba(255,255,255,.88); }
.rank-stage .p3 .p-step { height: 58px; background: linear-gradient(180deg,#ffad43,#e87913); box-shadow: inset 0 -10px 0 rgba(168,73,0,.45), 0 0 0 3px rgba(255,255,255,.08); color: rgba(255,255,255,.88); }

/* ---- List heading + rows (ranks 4+) ---- */
.rank-listhead { display: flex; align-items: center; gap: 9px; margin-top: 4px; }
.rank-listhead h2 { font-family: var(--font-display); font-weight: 800; font-size: 1rem; }
.rank-listhead .rule { flex: 1; height: 2px; background: var(--line); border-radius: 2px; }
.rank-listhead .cnt { font-family: var(--font-display); font-weight: 800; font-size: .72rem; color: var(--ink-soft); background: var(--navy-soft); padding: 2px 10px; border-radius: 999px; }
.rank-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
#viewRanking .row {
  display: flex; align-items: center; gap: 12px; width: 100%;
  background: var(--surface); border: 2px solid var(--line); border-radius: var(--r-md);
  box-shadow: 0 3px 0 0 var(--line); padding: 11px 14px 11px 11px;
}
/* Board entries open that learner's profile peek in the shared sheet. */
#viewRanking [data-slug] { cursor: pointer; }
#viewRanking .row[data-slug]:active { transform: translateY(2px); box-shadow: 0 1px 0 0 var(--line); }
#viewRanking .rk { flex: none; width: 26px; text-align: center; font-family: var(--font-display); font-weight: 800; font-size: 1rem; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
#viewRanking .av { flex: none; width: 44px; height: 44px; border-radius: 50%; overflow: hidden; background: radial-gradient(circle at 50% 32%, #fff, var(--canvas-2)); box-shadow: inset 0 0 0 3px #fff, 0 3px 7px rgba(46,44,139,.14); display: flex; align-items: flex-end; justify-content: center; }
#viewRanking .av img { width: 118%; margin-bottom: -2px; }
#viewRanking .who { flex: 1; min-width: 0; }
#viewRanking .who .nm { font-family: var(--font-display); font-weight: 800; font-size: 1rem; line-height: 1.1; display: flex; align-items: center; gap: 7px; }
#viewRanking .who .nm .kamu { flex: none; font-family: var(--font-body); font-weight: 900; font-size: .62rem; letter-spacing: .4px; color: #fff; background: var(--navy); padding: 2px 8px; border-radius: 999px; text-transform: uppercase; }
#viewRanking .who .mt { display: flex; align-items: center; gap: 8px; margin-top: 3px; font-size: .75rem; font-weight: 800; color: var(--ink-soft); }
#viewRanking .lv { color: var(--navy); background: var(--navy-soft); border-radius: 7px; padding: 1px 7px; font-weight: 900; }
#viewRanking .fire { display: inline-flex; align-items: center; gap: 4px; color: var(--red-deep); white-space: nowrap; }
#viewRanking .fire.cold { color: var(--ink-faint); }
#viewRanking .fire b { color: inherit; font-weight: 900; }
#viewRanking .rank-fire-num { display: inline-block; line-height: 1; transform: translateY(1px); }
#viewRanking .rank-fire-icon {
  width: 1.08em;
  height: 1.08em;
  flex: none;
  margin: -.16em -1px -.24em 0;
}
.rank-stage .p-tag.fire {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  height: calc(1em + 8px);
  line-height: 1;
  padding-top: 3px;
  padding-bottom: 3px;
}
.rank-stage .p-tag.fire .rank-fire-icon {
  width: 1em;
  height: 1em;
  margin: 0 -1px 0 -1px;
}
.rank-stage .p-tag.fire .rank-fire-num {
  display: inline-flex;
  align-items: center;
  height: 1.2em;
  line-height: 1;
  color: #FFD1B0;
  transform: none;
}
#viewRanking .xp { flex: none; text-align: right; }
#viewRanking .xp b { display: block; font-family: var(--font-display); font-weight: 800; font-size: 1rem; line-height: 1; font-variant-numeric: tabular-nums; }
#viewRanking .xp span { font-size: .56rem; font-weight: 900; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-faint); }
#viewRanking .row.me { border-color: var(--navy); box-shadow: 0 4px 0 0 var(--navy); background: linear-gradient(180deg, var(--navy-soft), #fff 70%); }
#viewRanking .row.me .rk { color: var(--navy); }

/* You-vs-next nudge (always in the slot right under the card) */
#viewRanking .nudge {
  display: flex; align-items: center; gap: 9px; padding: 10px 12px;
  background: var(--green-soft); border: 2px solid #BFEFD3; border-radius: var(--r-md);
  box-shadow: 0 3px 0 0 #BFEFD3;
}
#viewRanking .nudge .em { font-size: 1.1rem; flex: none; }
#viewRanking .nudge p { font-size: .8rem; font-weight: 800; color: var(--green-deep); line-height: 1.35; }
#viewRanking .nudge p b { color: var(--navy); }

.rank-more { align-self: center; margin-top: 2px; }

/* Sticky self-summary: pinned just above the fixed tab bar while scrolling. */
.rank-self {
  position: sticky; bottom: 6px; z-index: 5; margin-top: 4px;
  display: flex; align-items: center; gap: 11px;
  background: linear-gradient(180deg, var(--navy), var(--navy-deep));
  border-radius: var(--r-lg); padding: 10px 14px 10px 11px;
  box-shadow: 0 8px 22px rgba(23,21,63,.4), 0 4px 0 0 #14123a;
}
.rank-self .srk { flex: none; width: 38px; height: 38px; border-radius: 12px; display: grid; place-items: center; font-family: var(--font-display); font-weight: 800; font-size: 1.05rem; color: var(--navy-deep); background: linear-gradient(180deg, var(--yellow), var(--yellow-deep)); box-shadow: 0 2px 0 rgba(0,0,0,.2); }
.rank-self .srk small { display: block; font-size: .5rem; font-weight: 900; color: var(--navy); letter-spacing: .5px; margin-bottom: -2px; }
.rank-self .sbody { flex: 1; min-width: 0; color: #fff; }
.rank-self .snm { font-family: var(--font-display); font-weight: 800; font-size: .95rem; line-height: 1.1; display: flex; align-items: center; gap: 7px; }
.rank-self .snm .kamu { flex: none; font-size: .56rem; font-weight: 900; letter-spacing: .4px; background: var(--yellow); color: var(--navy-deep); padding: 1px 7px; border-radius: 999px; text-transform: uppercase; }
.rank-self .ssub {
  display: flex;
  align-items: center;
  gap: 5px;
  flex-wrap: wrap;
  font-size: .7rem;
  font-weight: 800;
  line-height: 1.15;
  color: rgba(255,255,255,.72);
  margin-top: 2px;
}
.rank-self .ssub > span,
.rank-self .ssub em {
  display: inline-flex;
  align-items: center;
  height: 1.15em;
  line-height: 1;
}
.rank-self .ssub .sep { opacity: .9; }
.rank-self .fire.on-dark {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: inherit;
  height: 1.15em;
  line-height: 1;
  vertical-align: 0;
}
.rank-self .fire.on-dark .rank-fire-icon {
  width: .95em;
  height: .95em;
  margin: 0 -1px 0 0;
}
.rank-self .fire.on-dark .rank-fire-num {
  display: inline-flex;
  align-items: center;
  height: 1.15em;
  line-height: 1;
  color: inherit;
  transform: none;
}
.rank-self .ssub em { font-style: normal; color: var(--yellow); }
.rank-self .sxp { flex: none; text-align: right; color: #fff; }
.rank-self .sxp b { display: block; font-family: var(--font-display); font-weight: 800; font-size: 1.05rem; line-height: 1; font-variant-numeric: tabular-nums; }
.rank-self .sxp span { font-size: .56rem; font-weight: 900; letter-spacing: .06em; text-transform: uppercase; color: rgba(255,255,255,.6); }

.rank-empty { text-align: center; color: var(--ink-faint); font-weight: 700; padding: 22px; }

@media (prefers-reduced-motion: reduce) {
  .rain, .rank-stage .spark { display: none; }
}

/* ---------- Level-up toast ---------- */
.levelup-toast {
  position: fixed; top: 18px; left: 50%; z-index: 50;
  display: flex; align-items: center; gap: 12px;
  background: var(--navy); border: 3px solid var(--yellow); border-radius: 999px;
  padding: 9px 22px 9px 11px; box-shadow: var(--lift-lg);
  opacity: 0; transform: translateX(-50%) translateY(-18px);
  transition: opacity .3s ease, transform .3s ease;
}
.levelup-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.levelup-toast img { width: 42px; height: 42px; object-fit: contain; }
.lt-kicker { display: block; font-size: .6rem; text-transform: uppercase; letter-spacing: 1.5px; color: var(--yellow); font-weight: 800; }
.lt-level { display: block; font-family: var(--font-display); font-weight: 800; font-size: 1.1rem; color: #fff; }
/* "Api menyala!" (task 125): the same toast, told apart by the flame and a
   fire-orange edge where level-up is yellow. */
.levelup-toast.fire { border-color: #FF8A1E; white-space: nowrap; }
.levelup-toast.fire .profile-fire-icon { width: 42px; height: 42px; }
.levelup-toast.fire .lt-kicker { color: #FFB343; }

/* ---------- Animations ---------- */
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@keyframes deal { from { opacity: 0; transform: translateY(16px) scale(.985); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes pop { from { opacity: 0; transform: translateY(14px) scale(.96); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes shake { 10%,90%{transform:translateX(-1px)} 20%,80%{transform:translateX(2px)} 30%,50%,70%{transform:translateX(-4px)} 40%,60%{transform:translateX(4px)} }
@keyframes bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-6px)} }
@keyframes drift { 0%,100%{ transform: translate(-50%,-50%) rotate(-7deg); } 50%{ transform: translate(-50%,calc(-50% - 18px)) rotate(7deg); } }
@keyframes pop-bounce { 0%{transform:scale(.6)} 55%{transform:scale(1.15)} 100%{transform:scale(1)} }
@keyframes mascot-shake { 0%,100%{transform:translateX(0) rotate(0)} 25%{transform:translateX(-5px) rotate(-5deg)} 75%{transform:translateX(5px) rotate(5deg)} }
/* The finish screen borrows the mascot's intro motion; the intro itself now
   lives in welcome.css, on the landing page. */
@keyframes welcome-halo-breathe {
  0%,100% { transform: scale(1); box-shadow: 0 12px 34px rgba(224,181,0,.24); }
  50% { transform: scale(1.055); box-shadow: 0 16px 40px rgba(224,181,0,.32); }
}
@keyframes welcome-mascot-in {
  0% { opacity: 0; transform: translateY(35px) rotate(-9deg) scale(.64); }
  58% { opacity: 1; transform: translateY(-5px) rotate(4deg) scale(1.08); }
  78% { transform: translateY(2px) rotate(-2deg) scale(.98); }
  100% { opacity: 1; transform: translateY(0) rotate(0) scale(1); }
}
@keyframes welcome-mascot-float {
  0%,100% { transform: translateY(0) rotate(0); }
  24% { transform: translateY(-4px) rotate(-1deg); }
  52% { transform: translateY(-7px) rotate(2deg); }
  76% { transform: translateY(-3px) rotate(-1deg); }
}
@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

/* ---------- Phone: pin continue, keep loop in the fold ---------- */
@media (max-width: 560px) {
  .app { padding: 13px 13px 0; --app-gap: 11px; }
  .tabbar { margin: calc(-1 * var(--app-gap)) -13px 0; }
  .view { gap: 11px; }
  .brand { gap: 8px; }
  .play-controls { gap: 6px; }
  .roman-toggle button { min-width: 32px; padding: 7px 8px; }
  .dir-menu-btn { width: 38px; height: 38px; }

  .card { padding: 22px 16px 26px; gap: 12px; min-height: 195px; }
  .question-card { margin-top: 36px; }
  .mascot { width: 72px; height: 76px; top: -40px; right: 6px; }
  .card-meta { padding-right: 62px; }
  .prompt.is-thai { font-size: 2.5rem; }
  .prompt { font-size: 1.55rem; }

  .summary-stats { grid-template-columns: repeat(2, 1fr); }
  .bd-row { grid-template-columns: minmax(48px,auto) 1fr; }
  .bd-row .bd-arti { grid-column: 1 / -1; margin-top: -2px; }
}

/* Wider screens: a little more generosity. */
@media (min-width: 561px) {
  .app { max-width: 600px; padding: 24px 20px 0; --app-gap: 16px; }
  .view { gap: 16px; }
  .tabbar { margin: calc(-1 * var(--app-gap)) -20px 0; }
  .card { padding: 32px 26px 38px; gap: 18px; min-height: 250px; }
  .question-card { margin-top: 44px; }
  .mascot { width: 98px; height: 104px; top: -52px; }
  .prompt.is-thai { font-size: 3.1rem; }
  .prompt { font-size: 2rem; }
}

/* ======================= Beranda: the learner journey =================== */
/* One scroll from top to bottom: a celebratory hero with the level ring, one
   strong action, then the curriculum as a vertical path of topics, subtopics
   and their exercise pips. */
#viewHome:not([hidden]),
#viewHuruf:not([hidden]) { overflow-y: auto; }
.admin-home { display: flex; flex-direction: column; gap: 14px; padding: 2px 0 8px; }

.ah-top { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 2px; }
.ah-greeting { min-width: 0; }
.ah-greeting small { display: block; color: var(--ink-soft); font-size: .76rem; font-weight: 800; }
.ah-greeting b {
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--navy); font-family: var(--font-display); font-size: 1.55rem; font-weight: 800; line-height: 1.08;
}
.ah-greeting b span { color: var(--green-deep); }
.ah-hero {
  position: relative; isolation: isolate; overflow: hidden;
  display: flex; flex-direction: column; align-items: center;
  padding: 17px 16px 18px; border: 2px solid #242067; border-radius: var(--r-xl);
  color: #fff; box-shadow: 0 7px 0 #171344, 0 20px 34px -25px rgba(23,19,68,.85);
  background:
    radial-gradient(95% 65% at 92% 4%, rgba(102,69,221,.72), transparent 62%),
    radial-gradient(72% 66% at 5% 32%, rgba(58,87,195,.45), transparent 66%),
    linear-gradient(160deg, #24216F 0%, #2E2C8B 54%, #211B67 100%);
}
.ah-hero::before,
.ah-hero::after { content: ""; position: absolute; z-index: -1; pointer-events: none; }
.ah-hero::before {
  inset: auto -12% -62px; height: 145px; opacity: .55;
  background:
    radial-gradient(ellipse at 14% 100%, #28645F 0 38%, transparent 39%),
    radial-gradient(ellipse at 36% 100%, #2A765F 0 34%, transparent 35%),
    radial-gradient(ellipse at 66% 100%, #255B55 0 37%, transparent 38%),
    radial-gradient(ellipse at 88% 100%, #34725C 0 40%, transparent 41%);
}
.ah-hero::after {
  width: 185px; height: 185px; left: 50%; top: 70px; transform: translateX(-50%);
  border-radius: 50%; background: rgba(255,225,0,.12); filter: blur(24px);
}
.ah-stars {
  position: absolute; inset: 0; z-index: -1; opacity: .85; pointer-events: none;
  background-image:
    radial-gradient(circle at 9% 18%, #FFE36B 0 1.5px, transparent 2px),
    radial-gradient(circle at 25% 42%, #fff 0 1px, transparent 1.5px),
    radial-gradient(circle at 43% 13%, #FFE36B 0 1px, transparent 1.5px),
    radial-gradient(circle at 68% 30%, #fff 0 1.5px, transparent 2px),
    radial-gradient(circle at 88% 13%, #FFE36B 0 1.5px, transparent 2px),
    radial-gradient(circle at 94% 49%, #fff 0 1px, transparent 1.5px);
  animation: starDrift 9s ease-in-out infinite alternate;
}
/* The hero is the first thing seen on every visit, so it breathes instead of
   sitting there printed: the star field drifts a few pixels one way and back.
   Slow enough that it is never the thing you look at. */
@keyframes starDrift { to { transform: translate3d(-6px, 5px, 0); } }
.ah-chips { display: flex; gap: 7px; }

/* Next-lesson card: the hero chassis laid out as copy + mascot instead of a
   level ring; the mascot bleeds into the card's bottom-right corner. */
.ah-next { flex-direction: row; align-items: center; gap: 6px; padding: 18px 14px 18px 18px; }
.ah-next-copy { position: relative; z-index: 2; flex: 1 1 auto; min-width: 0; }
.ah-next-tag {
  display: inline-block; padding: 5px 12px; border-radius: 999px;
  background: rgba(255,255,255,.17); color: rgba(255,255,255,.94);
  font-size: .64rem; font-weight: 900; letter-spacing: .1em; text-transform: uppercase;
}
.ah-next h1 {
  margin: 11px 0 0; color: #fff; font-family: var(--font-display);
  font-size: 1.42rem; font-weight: 800; line-height: 1.14;
}
.ah-next-count { margin: 9px 0 0; color: rgba(255,255,255,.74); font-size: .84rem; font-weight: 800; }
.ah-next-count b { color: #fff; }
/* The commitment row under the headline: kind, card count, rough minutes.
   Journey hero only (the hub's hero has no such row in its markup). */
.ah-next-meta { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 9px; }
.ah-next-meta span {
  display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px;
  border-radius: 999px; background: rgba(255,255,255,.14); color: #fff;
  font-size: .7rem; font-weight: 800; white-space: nowrap;
}
.ah-next-meta img { width: 14px; height: 14px; display: block; }
/* No sub-line between headline and CTA, so the button carries the gap; a
   hidden CTA (hub-go all-done state) then contributes no margin at all. */
.ah-next .quickstart { width: auto; margin-top: 14px; padding: 13px 22px; font-size: 1.06rem; }
/* Hub hero on narrow phones: the copy column keeps its content width so the
   CTA label stays on one line; the landscape art gives way instead. */
.hub-go .ah-next-copy { flex-shrink: 0; }
.hub-go .hub-art { flex-shrink: 1; min-width: 0; }
/* The CTA names its price under the label: same button, honest about the one
   glass of thai tea a journey exercise costs. */
.qs-copy { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.05; }
.qs-cost { font-family: var(--font-body); font-size: .6rem; font-weight: 800; opacity: .68; }
/* The same idle bob every other mascot in the app has; .hub-go's landscape art
   keeps its own slower v1Bob (more specific selector). */
.ah-next-art {
  position: relative; z-index: 1; flex: 0 0 auto; width: 150px;
  align-self: flex-end; margin: 0 -8px -18px 0;
  filter: drop-shadow(0 10px 14px rgba(11,9,57,.4));
  animation: bob 3.6s ease-in-out infinite;
}
/* Phone: the mascot leaves the flex row for the card's bottom-right corner,
   so the tag and headline get the card's full width; the meta chips and the
   CTA stop short of it instead of printing over its head. The hub's landscape
   art keeps its centered in-row layout. */
@media (max-width: 560px) {
  .ah-next-art:not(.hub-art) { position: absolute; right: 4px; bottom: -12px; width: 98px; margin: 0; }
  .ah-next-meta { padding-right: 94px; }
  .ah-next:not(.hub-go) .quickstart { max-width: calc(100% - 102px); }
}

.jm-topic-list { display: flex; flex-direction: column; gap: 11px; }
.jm-loading,
.jm-error,
.jm-inline-empty {
  padding: 18px; border: 2px dashed var(--line); border-radius: var(--r-md);
  background: rgba(255,255,255,.65); color: var(--ink-soft); font-size: .82rem; font-weight: 800; text-align: center;
}
.jm-error { display: flex; flex-direction: column; gap: 3px; color: var(--red-deep); border-color: #FBD4D9; background: var(--red-soft); }
.jm-error span { color: var(--ink-soft); font-size: .72rem; }

.jm-topic { border: 2px solid var(--line); border-radius: var(--r-lg); background: var(--surface); box-shadow: 0 4px 0 var(--line); overflow: hidden; }
.jm-topic[open] { border-color: #D8D4F6; box-shadow: 0 5px 0 #D8D4F6, var(--lift-sm); }
.jm-topic > summary {
  display: grid; grid-template-columns: auto minmax(0,1fr) auto; align-items: center; gap: 11px;
  min-height: 76px; padding: 13px 14px; cursor: pointer; list-style: none; color: var(--navy);
}
.jm-topic > summary::-webkit-details-marker { display: none; }
.jm-topic > summary:hover { background: #FAFAFE; }
.jm-topic > summary:focus-visible { outline: 3px solid var(--sky); outline-offset: -3px; }
.jm-topic-number {
  align-self: center; min-width: 55px; padding: 6px 8px; border-radius: 999px;
  background: var(--navy); color: #fff; box-shadow: 0 3px 0 var(--navy-deep);
  font-family: var(--font-display); font-size: .62rem; font-weight: 800; text-align: center; text-transform: uppercase;
}
.jm-topic-copy { min-width: 0; }
.jm-topic-copy b { display: block; overflow: hidden; color: var(--navy); font-family: var(--font-display); font-size: 1.06rem; font-weight: 800; line-height: 1.12; text-overflow: ellipsis; white-space: nowrap; }
.jm-topic-meter { display: block; max-width: 200px; height: 9px; margin-top: 7px; overflow: hidden; border: 2px solid var(--line); border-radius: 999px; background: var(--navy-soft); }
.jm-topic-meter i { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--green), var(--green-deep)); }
.jm-topic-chev { color: var(--navy); font-family: var(--font-display); font-size: 1.25rem; font-weight: 800; transition: transform .18s ease; }
.jm-topic[open] .jm-topic-chev { transform: rotate(180deg); }

.jm-sub-list { position: relative; display: flex; flex-direction: column; gap: 10px; padding: 3px 12px 15px 26px; }
/* The rail is drawn per card, not once down the list: each card joins its own
   marker centre to its neighbour's, so the line starts and ends exactly on a
   circle whatever height the cards happen to be. left = half the 48px marker
   column - half the 4px rail. */
.jm-subtopic::after,
.jm-subtopic::before { content: ""; position: absolute; left: 22px; width: 4px; background: var(--line); }
.jm-subtopic::before { top: 50%; bottom: -10px; }            /* down to the next card */
.jm-subtopic::after { top: 0; bottom: 50%; }                 /* up from the previous one */
.jm-subtopic:last-child::before,
.jm-subtopic:first-child::after { content: none; }
.jm-subtopic {
  position: relative; display: grid; grid-template-columns: 48px minmax(0,1fr); gap: 10px; align-items: center;
  width: 100%; padding: 0; border: 0; background: none; font: inherit; text-align: left;
  animation: rise .32s ease both; animation-delay: calc(var(--i, 0) * 50ms);
}
.jm-subtopic.is-locked { opacity: .72; }
.jm-sub-marker {
  position: relative; z-index: 2; width: 48px; height: 48px; display: grid; place-items: center;
  border: 4px solid var(--surface); border-radius: 50%;
  background: var(--line); color: var(--ink-faint); box-shadow: 0 0 0 2px var(--line), 0 3px 8px rgba(46,44,139,.16);
  font-family: var(--font-display); font-size: 1.25rem; font-weight: 900;
}
.jm-sub-marker img { width: 26px; height: 26px; object-fit: contain; filter: grayscale(1); opacity: .55; }
.jm-sub-marker .jr-lock { width: 21px; height: 21px; color: var(--ink-faint); }
.jm-subtopic.is-open .jm-sub-marker { color: var(--ink-soft); font-size: 1.5rem; }
.jm-subtopic.is-done .jm-sub-marker { background: var(--green); color: #fff; box-shadow: 0 0 0 2px var(--green-deep), 0 4px 10px rgba(41,166,94,.25); }
.jm-subtopic.is-current .jm-sub-marker { background: var(--yellow); color: var(--navy); box-shadow: 0 0 0 2px var(--yellow-deep), 0 4px 10px rgba(224,181,0,.22); font-size: .95rem; }
.jm-sub-body { min-width: 0; padding: 12px 12px 11px; border: 2px solid var(--line); border-radius: var(--r-md); background: var(--surface); box-shadow: 0 3px 0 var(--line); }
.jm-subtopic.is-done .jm-sub-body { border-color: #BFEBCF; background: linear-gradient(135deg, #fff 0, var(--green-soft) 100%); box-shadow: 0 3px 0 #BFEBCF; }
.jm-subtopic.is-current .jm-sub-body { border-color: var(--yellow-deep); background: linear-gradient(135deg, #fff 0, #FFFBE1 100%); box-shadow: 0 4px 0 var(--yellow-deep), var(--lift-sm); }
.jm-sub-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.jm-sub-top h3 { min-width: 0; margin: 0; overflow: hidden; color: var(--navy); font-family: var(--font-display); font-size: .98rem; font-weight: 800; line-height: 1.15; text-overflow: ellipsis; white-space: nowrap; }
.jm-sub-status { flex: none; padding: 4px 8px; border-radius: 999px; background: var(--navy-soft); color: var(--ink-soft); font-size: .55rem; font-weight: 900; text-transform: uppercase; }
.is-done .jm-sub-status { background: var(--green); color: #fff; }
.is-current .jm-sub-status { background: var(--navy); color: #fff; }
/* The pips are capped at 62px (a subtopic tops out at 6 stages) and the
   connectors between them absorb the row's slack, so even a short row reaches
   the card's right edge instead of leaving a gap there. Spacing lives in the
   connectors rather than in `gap`/`justify-content` because each connector is
   a real element sized to its own span, which is what keeps the chain drawn
   exactly between two pips however far apart the row pushes them. */
.jm-stage-icons { display: flex; align-items: center; margin-top: 8px; }
.jm-stage-icon { position: relative; flex: 0 1 62px; min-width: 0; aspect-ratio: 1; padding: 0; display: grid; place-items: center; border: 2px solid var(--line); border-radius: 12px; background: var(--surface); }
/* Connector back to the previous pip, so the row reads as one ordered chain;
   it turns green once the pip behind it is done. */
.jm-link { flex: 1 1 10px; min-width: 10px; height: 3px; border-radius: 2px; background: var(--line); }
.jm-stage-icon.is-filled + .jm-link { background: var(--green); }
/* A poked locked pip (or teaser) shakes its head; the pip that just finished
   pops once as the map comes back. Both ride sfx (denied / sparkle). */
.jm-stage-icon.is-denied, .jr-next.is-denied { animation: shake .4s ease; }
.jm-stage-icon.is-just-filled { animation: pop-bounce .5s cubic-bezier(.2,1.3,.4,1) both; }
button.jm-stage-icon { -webkit-appearance: none; appearance: none; cursor: pointer; font: inherit; transition: transform .15s ease, box-shadow .15s ease; }
/* Hover only on devices with a real pointer: on touch it would stick after a tap. */
@media (hover: hover) {
  button.jm-stage-icon:hover { transform: translateY(-3px) scale(1.06); box-shadow: 0 6px 12px rgba(15,42,74,.18); }
  button.jm-stage-icon:hover img { animation: stagePop .4s ease; }
}
@keyframes stagePop { 40% { transform: rotate(-7deg) scale(1.12); } }
button.jm-stage-icon:active { transform: translateY(1px); }
@media (prefers-reduced-motion: reduce) {
  button.jm-stage-icon { transition: none; }
  button.jm-stage-icon:hover { transform: none; }
  button.jm-stage-icon:hover img { animation: none; }
}
button.jm-stage-icon:focus-visible { outline: 3px solid var(--sky); outline-offset: 2px; }
.jm-stage-icon img { width: 68%; height: 68%; object-fit: contain; }
.jm-cycle-orbit {
  position: absolute; z-index: 0; top: -5px; left: -5px;
  width: calc(100% + 10px); height: calc(100% + 10px);
  overflow: visible; pointer-events: none;
}
.jm-cycle-orbit path { fill: none; stroke-linecap: round; stroke-linejoin: round; }
.jm-cycle-orbit .is-pending { stroke: #C4D5CA; stroke-width: 2.5; }
.jm-cycle-orbit .is-passed { stroke: #22995A; stroke-width: 3.5; }
button.jm-stage-icon.has-cycles:focus-visible { outline-offset: 6px; }
.jm-stage-icon.has-cycles img { position: relative; z-index: 1; }
/* Done reads from the green frame and tint, not from knocking the icon out to
   white: the icon keeps its own colors so it stays recognizable. */
.jm-stage-icon.is-filled { border-color: var(--green-deep); background: var(--green-soft); box-shadow: 0 2px 0 var(--green-deep); }
.jm-stage-icon.is-empty { opacity: .5; filter: grayscale(1); }
/* A pip you can actually start reads as a control, not a progress dot. */
.jm-stage-icon.is-playable.is-empty { opacity: .85; filter: none; border-color: var(--navy-soft); }
.jm-stage-icon.is-next { border-color: var(--yellow-deep); background: var(--yellow); box-shadow: none; }
button.jm-stage-icon.is-next:hover { box-shadow: none; }
.cycle-next { color: var(--navy); }

.jr-next {
  display: grid; grid-template-columns: 58px minmax(0,1fr) auto; gap: 10px; align-items: center;
  padding: 10px 14px 10px 8px; border: 2px dashed #C9C4EE; border-radius: var(--r-lg);
  background: rgba(255,255,255,.6);
}
.jr-next > img { width: 58px; height: 58px; object-fit: contain; opacity: .85; }
.jr-next-copy b { display: block; color: var(--navy); font-family: var(--font-display); font-size: 1.02rem; font-weight: 800; }
.jr-next-copy small { display: block; margin-top: 2px; color: var(--ink-soft); font-size: .68rem; font-weight: 800; }
.jr-next-lock { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 50%; background: var(--navy-soft); color: var(--ink-soft); }
.jr-next-lock .jr-lock { width: 19px; height: 19px; }
/* Lompat topik (task 45): the same locked card, offering the way in early. The
   lock chip is replaced by a full-width press button under the copy. */
.jr-next.can-skip { grid-template-columns: 58px minmax(0,1fr); row-gap: 8px; padding-bottom: 12px; }
.jr-skip {
  grid-column: 1 / -1; display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; border: 0; border-radius: var(--r-md); padding: 10px 14px 11px; cursor: pointer;
  font-family: var(--font-display); font-weight: 800; font-size: .9rem; color: var(--navy);
  background: var(--yellow); box-shadow: 0 4px 0 0 var(--yellow-deep);
  transition: transform .06s ease, box-shadow .06s ease;
}
.jr-skip:active:not(:disabled) { transform: translateY(4px); box-shadow: 0 0 0 0 var(--yellow-deep); }
.jr-skip:disabled { opacity: .7; cursor: progress; }
.jr-skip svg { width: 18px; height: 18px; }
.skip-sheet { text-align: center; }
.skip-sheet .skip-art { width: 76px; height: 76px; display: block; margin: 2px auto 10px; }
.skip-rules { list-style: none; margin: 0 0 16px; padding: 0; text-align: left; display: grid; gap: 8px; }
.skip-rules li { display: grid; grid-template-columns: 34px 1fr; gap: 10px; align-items: center;
  background: var(--canvas); border-radius: var(--r-md); padding: 9px 12px 9px 9px;
  font-weight: 700; font-size: .84rem; color: var(--ink-2); line-height: 1.4; }
.skip-rules li b { color: var(--navy); }
.skip-rules .ico { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center;
  background: var(--navy-soft); font-family: var(--font-display); font-weight: 800; color: var(--navy); font-size: .82rem; }
.skip-rules .ico.green { background: var(--green-soft); color: var(--green-deep); }
.skip-sheet .btn-primary { width: 100%; display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
.skip-sheet .btn-primary img { width: 20px; height: 20px; }
.skip-sheet .drill-exit { width: 100%; margin-top: 8px; }
.skip-tag { display: inline-flex; align-items: center; background: #FFF4C2; color: var(--navy);
  border-radius: 999px; padding: 3px 10px; font-size: .7rem; font-weight: 800; font-family: var(--font-display); }
.drill-done .skip-unlock { display: flex; align-items: center; gap: 10px; background: var(--green-soft);
  border-radius: var(--r-md); padding: 10px 14px; margin: 0 0 14px; max-width: 320px; width: 100%;
  text-align: left; color: var(--green-deep); font-weight: 800; font-size: .84rem; }
.drill-done .skip-unlock svg { width: 22px; height: 22px; flex: 0 0 auto; }
.jm-no-stage { color: var(--ink-faint); font-size: .62rem; font-weight: 800; }

.jm-empty { display: flex; flex-direction: column; align-items: center; padding: 20px; border: 2px dashed var(--line); border-radius: var(--r-lg); background: rgba(255,255,255,.72); text-align: center; }
.jm-empty img { width: 82px; height: 82px; object-fit: contain; }
.jm-empty b { font-family: var(--font-display); font-size: 1.05rem; }
.jm-empty p { margin: 3px 0 12px; color: var(--ink-soft); font-size: .76rem; font-weight: 700; }
.jm-empty a { padding: 9px 14px; border-radius: var(--r-sm); background: var(--yellow); color: var(--navy); box-shadow: 0 3px 0 var(--yellow-deep); font-size: .76rem; font-weight: 900; text-decoration: none; }

@media (max-width: 400px) {
  .ah-hero { padding-inline: 13px; }
  .ah-greeting b { font-size: 1.32rem; }
  .hub-go .hub-art { width: 132px; }
  .ah-next h1 { font-size: 1.24rem; }
  .ah-next .quickstart { padding: 12px 16px; font-size: .98rem; }
  .jm-sub-list { padding-left: 17px; }
  .jm-subtopic::before,
  .jm-subtopic::after { left: 17px; } /* 38/2 - 4/2 */
  .jm-subtopic { grid-template-columns: 38px minmax(0,1fr); gap: 8px; }
  .jm-sub-marker { width: 38px; height: 38px; font-size: 1rem; }
  .jm-sub-marker img { width: 21px; height: 21px; }
  .jm-sub-status { display: none; }
}

/* ======================= Huruf: the letters tab ========================= */
/* The letters topic on its own page. Same chassis as the journey, but the art
   is the script itself: floating glyph tiles where the hero mascot would sit,
   and a giant ghost glyph on each track card. Tracks alternate their accent
   (sky, then gold) and go green once every stage is done. */
.hf-tiles { position: relative; z-index: 1; display: flex; gap: 8px; align-self: center; flex: 0 0 auto; margin-right: 4px; }
.hf-tile {
  display: grid; place-items: center; width: 50px; height: 56px;
  border: 2px solid rgba(255,255,255,.28); border-radius: 14px;
  background: rgba(255,255,255,.12); box-shadow: 0 4px 0 rgba(9,7,48,.5);
  color: #FFE36B; font-family: var(--font-thai); font-size: 1.65rem; font-weight: 700;
  animation: hfFloat 3.4s ease-in-out infinite;
}
/* The float rides the `translate` property so it stacks on each tile's tilt. */
.hf-tile:nth-child(1) { transform: rotate(-7deg); }
.hf-tile:nth-child(2) { margin-top: 15px; transform: rotate(4deg); color: #fff; animation-delay: .6s; }
.hf-tile:nth-child(3) { margin-top: -7px; transform: rotate(9deg); animation-delay: 1.2s; }
@keyframes hfFloat { 50% { translate: 0 -6px; } }
@media (prefers-reduced-motion: reduce) { .hf-tile { animation: none; } }

.hf-track-list { display: flex; flex-direction: column; gap: 12px; }
.hf-track {
  --hf-accent: var(--sky); --hf-edge: var(--sky-deep); --hf-soft: #EAF6FC;
  position: relative; overflow: hidden; isolation: isolate;
  padding: 15px 16px 14px; border: 2px solid var(--line); border-radius: var(--r-lg);
  background: linear-gradient(135deg, var(--surface) 45%, var(--hf-soft) 100%);
  box-shadow: 0 4px 0 var(--line);
  animation: rise .32s ease both; animation-delay: calc(var(--i, 0) * 60ms);
}
.hf-track:nth-child(2n) { --hf-accent: var(--gold); --hf-edge: var(--yellow-deep); --hf-soft: #FFF6D6; }
/* Anchored top-right beside the title, where the tinted gradient leaves it
   exposed; the pips row then cuts across its lower half. Drawn in the accent's
   edge tone: the pale accent itself vanishes into the card's own tint. */
.hf-ghost {
  position: absolute; z-index: -1; right: -6px; top: -14px; transform: rotate(8deg);
  color: var(--hf-edge); opacity: .24; pointer-events: none;
  font-family: var(--font-thai); font-weight: 700; font-size: 7.8rem; line-height: 1.15;
}
.hf-track-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.hf-track-head h2 {
  margin: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--navy); font-family: var(--font-display); font-size: 1.18rem; font-weight: 800;
}
.hf-track .jm-stage-icons { margin-top: 11px; }

@media (max-width: 400px) {
  .hf-tile { width: 40px; height: 46px; font-size: 1.3rem; border-radius: 11px; }
  .hf-ghost { font-size: 5.8rem; }
}

/* ======================= The letter table ==============================
   The "letters" stage kind: the whole table on one screen inside the drill
   body, on the same board chassis (.match) the matching grid uses. */
/* Always four columns, whatever the screen: the box sizes follow the grid's own
   width (container query units) instead of the boxes deciding the column count,
   so the table keeps its shape from a 320px phone up to the 560px app cap. */
.letter-grid {
  container-type: inline-size;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 2cqw;
}
.letter-group-title {
  grid-column: 1 / -1; margin: 12px 0 2px;
  font-family: var(--font-display); font-weight: 800; font-size: .95rem; color: var(--ink-2);
}
.letter-card {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 3cqw 1.6cqw 2.6cqw; min-height: 27cqw; cursor: pointer;
  text-align: center; overflow-wrap: anywhere;
  background: var(--surface); border: 2px solid var(--line); border-radius: var(--r-md);
  box-shadow: 0 3px 0 0 var(--line);
  transition: transform .08s ease, box-shadow .08s ease, border-color .15s ease;
}
.letter-card:hover { border-color: var(--navy); }
.letter-card:active { transform: translateY(3px); box-shadow: 0 0 0 0 var(--line); }
.letter-card.playing { border-color: var(--sky-deep); box-shadow: 0 3px 0 0 var(--sky-deep); }
/* Same face and weight as the class page's Thai prompt (.prompt.is-thai).
   "JLT Carrier" sits in front only to render the vowel-carrier ๐ as a dotted
   circle (see displayGlyph in app.js); every other glyph falls to Sarabun. */
.letter-glyph { font-family: "JLT Carrier", var(--font-thai); font-weight: 700; font-size: clamp(1.15rem, 7.5cqw, 2rem); line-height: 1.25; color: var(--ink); }
/* Anywhere else a stored ◌ glyph lands (quiz options, match tiles, reveals):
   setGlyphText in app.js swaps ◌ for ๐ and adds this class. Doubled so it
   out-ranks the two-class font rules it lands on (.prompt.is-thai, .r-thai). */
.glyph-carrier.glyph-carrier { font-family: "JLT Carrier", var(--font-thai); }
.letter-roman { font-size: clamp(.58rem, 3.6cqw, .74rem); font-weight: 800; color: var(--navy); line-height: 1.15; }
.letter-meaning { font-size: clamp(.54rem, 3.3cqw, .68rem); font-weight: 700; color: var(--ink-soft); line-height: 1.15; }
.letter-chip {
  margin-top: auto; padding: 2px min(7px, 1.8cqw); border-radius: 999px;
  font-size: clamp(.42rem, 2.4cqw, .58rem); font-weight: 800; letter-spacing: .3px;
  text-transform: uppercase; text-align: center; line-height: 1.5; max-width: 100%;
  /* "Kelas Menengah" breaks at its space on a small phone; never mid-word
     (the card's overflow-wrap: anywhere inherits in, so undo it here). */
  overflow-wrap: normal;
}
.chip-menengah  { background: #D9D8F5; color: var(--navy); }
.chip-tinggi    { background: #FFD2D8; color: var(--red-deep); }
.chip-rendah    { background: #CDF2DD; color: var(--green-deep); }
.chip-pendek    { background: #FFD2D8; color: var(--red-deep); }
.chip-panjang   { background: #CDF2DD; color: var(--green-deep); }
.chip-fleksibel { background: #D9D8F5; color: var(--navy); }

/* The whole box wears its group's color (blue = menengah/fleksibel,
   red = tinggi/pendek, green = rendah/panjang), chips deepened to stay
   readable on the tinted fill. */
.letter-card.tone-menengah, .letter-card.tone-fleksibel {
  background: var(--navy-soft); border-color: #C3C2EC; box-shadow: 0 3px 0 0 #C3C2EC;
}
.letter-card.tone-tinggi, .letter-card.tone-pendek {
  background: var(--red-soft); border-color: #F7C1C9; box-shadow: 0 3px 0 0 #F7C1C9;
}
.letter-card.tone-rendah, .letter-card.tone-panjang {
  background: var(--green-soft); border-color: #B6E8CC; box-shadow: 0 3px 0 0 #B6E8CC;
}
.letter-card[class*="tone-"]:active { box-shadow: 0 0 0 0 transparent; }

/* ======================= The Kosakata word board ========================
   The "content" stage kind: every word of the stage on one scrolling column
   (Thai + reading + arti), each card tap-to-hear, and a yellow (i) inside the
   card left of the speaker on the ones carrying a usage note. Same 3D face as
   the letter table. */
.word-list { display: flex; flex-direction: column; gap: 10px; }
/* Cards rise in one after another (--i set per card), like the journey map. */
.word-card { position: relative; animation: rise .32s ease both; animation-delay: calc(var(--i, 0) * .05s); }
.wc-say {
  width: 100%; display: flex; align-items: center; gap: 12px; text-align: left;
  padding: 12px 14px; cursor: pointer; font-family: var(--font-body); color: var(--ink);
  -webkit-appearance: none; appearance: none;
  background: var(--surface); border: 2px solid var(--line); border-radius: var(--r-md);
  box-shadow: 0 3px 0 0 var(--line);
  transition: transform .08s ease, box-shadow .08s ease, border-color .15s ease;
}
.wc-say:hover { border-color: var(--navy); box-shadow: 0 3px 0 0 var(--navy); }
.wc-say:active { transform: translateY(3px); box-shadow: 0 0 0 0 var(--line); }
/* The card that is talking wears the audio accent. */
.word-card:has(.playing) .wc-say { border-color: var(--sky-deep); box-shadow: 0 3px 0 0 var(--sky-deep); }
/* The running number doubles as the met/unmet marker: navy while new, green
   once the word has been heard. Not a score; just what is left to tap. */
.wc-num {
  flex: none; width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center;
  font-family: var(--font-display); font-weight: 800; font-size: .85rem;
  color: var(--navy); background: var(--navy-soft); border: 2px solid #C3C2EC;
  transition: background .2s ease, color .2s ease, border-color .2s ease;
}
.word-card.heard .wc-num { color: var(--green-deep); background: var(--green-soft); border-color: var(--green); }
.wc-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1 1 auto; }
.wc-thai { font-family: var(--font-thai); font-weight: 700; font-size: 1.5rem; line-height: 1.3; color: var(--ink); }
/* Green is the house reading voice (.r-roman on the answer reveal). */
.wc-roman { font-size: .8rem; font-weight: 800; color: var(--green-deep); }
.wc-meaning { font-size: .88rem; font-weight: 700; color: var(--ink-soft); }
/* The speaker chip is what .playing lands on, so the equalizer (position
   absolute, inset 0) dances inside the chip rather than across the whole row;
   same treatment as the matching grid's audio tile. */
.wc-audio {
  position: relative; flex: none; width: 34px; height: 34px;
  display: grid; place-items: center; color: var(--sky-deep);
  --eq-min: 7px; --eq-max: 22px;
}
.wc-audio img { width: 28px; height: 28px; transition: opacity .15s ease; }
.wc-audio.playing img { opacity: 0; }
.wc-audio .eq { gap: 3px; }
.wc-audio .eq i { width: 4px; }
@media (prefers-reduced-motion: reduce) { .wc-audio.playing img { opacity: 1; } }
/* Rides inside the card face, just left of the speaker chip: 14px card padding
   + 34px chip + 8px gap = 56px in from the right. It is a sibling of .wc-say
   (no button inside a button), so .wc-text reserves the room it covers.
   The chip is info.svg, the yellow twin of sound.svg: the coin and its centred
   glyph are drawn in the icon, so the button is just a frame around it. */
.wc-why {
  position: absolute; right: 56px; top: 50%; transform: translateY(-50%);
  width: 28px; height: 28px; cursor: pointer; display: grid; place-items: center;
  background: none; border: 0; padding: 0;
}
.wc-why img { width: 28px; height: 28px; display: block; }
.wc-why:hover img { filter: brightness(1.06); }
.wc-why:active { transform: translateY(calc(-50% + 2px)); }
.word-card:has(.wc-why) .wc-text { margin-right: 38px; }
/* The note itself is the point of the sheet, so it sits highlighted in the
   same yellow voice as the Penjelasan chip that opened it. */
.wc-note {
  margin: 0; padding: 12px 14px; font-weight: 600; line-height: 1.6; color: var(--ink-2);
  background: var(--canvas-2); border-left: 4px solid var(--yellow); border-radius: var(--r-sm);
}

/* --- Letter drills --- */
/* Same full-height frame as the class-tab practice loop (.class-practice): the
   top bar stays put, the round's content flexes to fill, and Lanjut sits on the
   bottom edge, so the layout does not jump as cards change height. */
.drill { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.drill-top { display: flex; align-items: center; gap: 12px; margin: 4px 0 12px; }
.drill-close {
  flex: 0 0 auto; width: 38px; height: 38px; border-radius: var(--r-sm); cursor: pointer;
  background: var(--surface); border: 2px solid var(--line); box-shadow: 0 3px 0 0 var(--line);
  color: var(--ink-soft); font-size: 1.15rem; line-height: 1;
}
.drill-close:active { transform: translateY(3px); box-shadow: 0 0 0 0 var(--line); }
.drill-bar { flex: 1 1 auto; height: 12px; border-radius: 999px; background: var(--line); overflow: hidden; }
.drill-bar i { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--green), var(--green-deep)); transition: width .25s ease; }
.drill-count { flex: 0 0 auto; font-family: var(--font-display); font-weight: 800; font-size: .85rem; color: var(--ink-soft); }
.drill-body { display: flex; flex-direction: column; align-items: center; gap: 12px; flex: 1 1 auto; min-height: 0; }
.drill-hint { font-size: .78rem; font-weight: 700; color: var(--ink-soft); text-align: center; margin: 0; }

/* The escape hatch on a question that needs an ear or a mic. Quiet next to the
   thing it opts out of: the learner should notice it exactly when the card in
   front of them is one they cannot do here. */
.quiet-out {
  align-self: center; margin-top: 10px; padding: 7px 14px; border-radius: 999px;
  background: transparent; border: 2px dashed var(--line); cursor: pointer;
  font-family: var(--font-display); font-weight: 800; font-size: .78rem;
  color: var(--ink-soft); transition: color .16s ease, border-color .16s ease;
}
.quiet-out:hover { color: var(--navy); border-color: var(--navy); }

/* The rounds sit on the practice question-card chassis; only the drill-specific
   controls inside it need their own rules. */
.drill-qcard { width: 100%; min-height: 0; } /* shrink to content so pad + tools fit one screen */
.drill-qcard .drill-play { align-self: center; margin-top: 6px; }

/* The equalizer wave every audio control shares: an overlay of bars that
   dance while the control is .playing, replacing its idle speaker glyph.
   Bar size scales per context through --eq-min/--eq-max. */
.eq {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center; gap: 6px;
  opacity: 0; transition: opacity .15s ease; pointer-events: none;
}
.eq i { width: 7px; height: var(--eq-min, 12px); border-radius: 4px; background: currentColor; }
.playing > .eq { opacity: 1; }
.playing > .eq i { animation: eq-dance .8s ease-in-out infinite; }
.playing > .eq i:nth-child(1) { animation-delay: -.35s; }
.playing > .eq i:nth-child(2) { animation-delay: -.6s; }
.playing > .eq i:nth-child(3) { animation-delay: -.1s; }
.playing > .eq i:nth-child(4) { animation-delay: -.5s; }
.playing > .eq i:nth-child(5) { animation-delay: -.25s; }
@keyframes eq-dance { 0%, 100% { height: var(--eq-min, 12px); } 50% { height: var(--eq-max, 44px); } }

/* One flat glyph on one 3D surface (the old icon was a shaded button inside a
   second box); while the clip plays the glyph gives way to the wave. */
.drill-play {
  position: relative;
  width: 96px; height: 96px; border-radius: 50%; cursor: pointer; color: #fff;
  background: linear-gradient(180deg, #67C8FF, #3EADEF);
  border: 2px solid var(--sky-deep); box-shadow: 0 5px 0 0 var(--sky-deep);
  display: grid; place-items: center;
}
.drill-play:active { transform: translateY(5px); box-shadow: 0 0 0 0 var(--sky-deep); }
/* The class card's prompt column is a flex column, so the round button needs
   centring the way the journey card's own layout already gives it. */
.prompt-wrap > .drill-play { margin: 4px auto 0; }
.drill-play > svg { width: 50px; height: 50px; display: block; transition: opacity .15s ease; }
.drill-play.playing > svg { opacity: 0; }

/* The class card mounts the listening controls in a box of its own, and its
   audio board mounts the escape hatch in another (the journey card builds its
   whole prompt instead), so both boxes carry the column those controls get from
   .prompt-wrap: rocker first, escape hatch under it, each centered by its own
   align-self. */
.prompt-listen, .board-quiet { display: flex; flex-direction: column; }

/* The speed rocker under a listening prompt: a quiet pill track where the
   chosen tempo sits raised on its own sky chip (the press signature run in
   reverse: chosen = up). Pelan arms every play on the prompt at 0.65x. */
.speed-toggle {
  align-self: center; display: inline-flex; align-items: center; gap: 5px;
  margin-top: 16px; padding: 5px; border-radius: 999px;
  background: var(--surface); border: 2px solid var(--line);
}
.speed-seg {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 14px; border: 2px solid transparent; border-radius: 999px;
  background: transparent; color: var(--ink-soft);
  font-family: var(--font-display); font-weight: 800; font-size: .8rem; letter-spacing: .2px;
  cursor: pointer;
  transition: transform .1s ease, box-shadow .1s ease, background .15s ease, color .15s ease, border-color .15s ease;
}
.speed-seg svg { width: 17px; height: auto; display: block; }
.speed-seg[aria-pressed="false"]:hover { color: var(--navy); }
.speed-seg[aria-pressed="true"] {
  color: #fff;
  background: linear-gradient(180deg, #67C8FF, #3EADEF);
  border-color: var(--sky-deep);
  box-shadow: 0 3px 0 0 var(--sky-deep);
  transform: translateY(-3px);
}
.speed-seg[aria-pressed="true"]:active { transform: translateY(0); box-shadow: 0 0 0 0 var(--sky-deep); }

/* The armed-slow cue: a turtle badge perched on the play button's rim (the way
   the mascot peeks over the card corner), and a half-tempo wave while the slow
   clip plays. */
.slow-badge {
  position: absolute; right: -7px; bottom: -4px;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--surface); border: 2px solid var(--sky-deep); color: var(--sky-deep);
  display: grid; place-items: center;
  transform: scale(0); transition: transform .18s cubic-bezier(.2,1.4,.4,1);
  pointer-events: none;
}
.slow-badge svg { width: 17px; height: auto; display: block; }
.drill-play.slow .slow-badge { transform: scale(1); }
.playing.slow > .eq i { animation-duration: 1.7s; }

@media (prefers-reduced-motion: reduce) {
  /* No dancing: the idle speaker glyph simply stays put while the clip plays. */
  .drill-play.playing > svg, .mtile.is-audio.playing img { opacity: 1; }
  .playing > .eq { opacity: 0; }
  .playing > .eq i { animation: none; }
  .slow-badge { transition: none; }
}
@keyframes pulse { 50% { transform: scale(1.05); } }

/* Everything under the card is exactly the card's width (the frame's), like the
   class-tab practice loop: no cap here, or the column goes ragged against the
   uncapped card above it. */
.drill-body .answer-scroll,
.drill-body .options,
.drill-body .afterthought,
.drill-body .example,
.drill-body .drill-actions,
.drill-body .match { width: 100%; }
/* The fill-the-frame rule for multiple choice is not repeated here: the
   exercise's options grid is built inside an .answer-scroll like the class
   card's, so one copy of it governs both surfaces (see .answer-scroll
   .options:not(.answered) above). */
/* Single-glyph answers (a letter card) read better as a grid of big glyphs than
   as four rows; every other card keeps the numbered single column. */
.drill-opts { grid-template-columns: 1fr 1fr; }
.drill-opts .opt { text-align: center; padding: 14px 12px; font-size: 2.1rem; line-height: 1.3; }
.drill-opts .opt .badge { display: none; }
.drill-opts .opt .opt-roman { font-size: .8rem; line-height: 1.4; }

/* Susun kalimat: the answer line the chunks are dropped into, and the bank they
   come from. Tapping a chunk copies it into the line and empties its seat in
   the bank; tapping it in the line fills the seat back in. Neither box ever
   changes size while the sentence is built. */
.ord-line, .ord-bank { display: flex; flex-wrap: wrap; gap: 8px; align-content: flex-start; }
.ord-line {
  min-height: 62px; padding: 11px;
  border: 2px dashed var(--line); border-radius: var(--r-md); background: var(--surface);
}
/* Same padding and an invisible border of the line's width, so the two boxes
   are geometrically identical and the height the line reserves for the finished
   sentence is exactly the bank's. */
.ord-bank { justify-content: center; padding: 11px; border: 2px solid transparent; }
/* A chunk that has been picked keeps its seat in the bank as an empty slot: the
   bank never resizes, the chunks left in it never move under the finger, and
   the gap reads as "that word is up there" instead of as a hole. */
.ord-chip.taken {
  background: var(--surface-2); border-style: dashed; box-shadow: none; pointer-events: none;
}
.ord-chip.taken, .ord-chip.taken * { color: transparent; }
.ord-line.ok { border-style: solid; border-color: var(--green); background: var(--green-soft); }
.ord-line.wrong { border-style: solid; border-color: var(--red); background: var(--red-soft); }
.ord-chip {
  padding: 10px 14px; border-radius: var(--r-sm); cursor: pointer;
  border: 2px solid var(--line); background: var(--surface); box-shadow: 0 3px 0 0 var(--line);
  font-family: var(--font-thai); font-size: 1.15rem; font-weight: 600; color: var(--navy);
}
.ord-chip:active { transform: translateY(3px); box-shadow: 0 0 0 0 var(--line); }
/* Dragging a chunk: the chip under the finger is a ghost that floats above the
   page; the chunk itself waits in the line as a dashed slot, which is the
   marker for where it will land. touch-action keeps a touch drag from being
   taken as a page scroll. */
.ord-line .ord-chip, .ord-bank:not(.conv-tray) .ord-chip { touch-action: none; user-select: none; -webkit-user-select: none; }
.ord-ghost { position: fixed; z-index: 1000; margin: 0; pointer-events: none;
  box-shadow: var(--lift-md); border-color: var(--sky-deep); }
.ord-chip.ord-slot { background: var(--surface-2); border-style: dashed; border-color: var(--sky-deep); box-shadow: none; }
.ord-chip.ord-slot, .ord-chip.ord-slot * { color: transparent; }

/* Lengkapi percakapan: the dialog thread and its chip tray. The thread is the
   prompt, so it gets the room; the tray reuses the bank's chassis (.ord-chip,
   .taken) because it is the same gesture as Susun kalimat. Speaker A leans
   left in the cool navy tint, speaker B right in the warm canvas tint: the two
   sides of the conversation are told apart by side AND by temperature, so the
   thread reads at a glance without avatars. */
/* A dialog reads from the top and grows downward, so this round opts out of the
   vertical centring the option lists want: a three-turn thread centred leaves a
   hole above it, and a long one would grow in both directions at once. */
/* The dialog rungs' header: one row instead of a question card, so the thread
   opens in view. The mascot stays because it is the answer feedback, but it sits
   IN the row at chip size rather than hanging off the top of a card. */
.conv-head {
  position: relative; width: 100%; display: flex; align-items: center; gap: 10px;
  min-height: 42px; padding: 0 2px;
}
.conv-head .prompt-tag { margin: 0; text-align: left; flex: 1 1 auto; }
.conv-head .conv-role {
  flex: none; padding: 4px 10px; border-radius: 999px;
  background: var(--navy-soft); color: var(--navy);
  font-family: var(--font-display); font-size: .7rem; font-weight: 800; letter-spacing: .06em;
}
.conv-head .mascot { position: relative; top: auto; right: auto; width: 46px; height: 48px; flex: none; }

.conv-scroll { justify-content: flex-start; }
.conv-title { font-size: .95rem; font-weight: 800; color: var(--ink-2); }
.conv-thread { display: flex; flex-direction: column; gap: 12px; }
/* The replay used to be a sibling of the bubble, so speaker B's row-reverse put
   its button LEFT of a right-aligned bubble: 42px gone from every bubble on both
   sides, and two columns that never lined up. It lives inside the bubble now,
   on the bubble's own last line, and the bubble gets the width back. */
.conv-turn { display: flex; align-items: flex-end; max-width: 96%; }
.conv-turn.b { align-self: flex-end; }
.conv-bubble {
  padding: 11px 14px; border-radius: var(--r-md);
  background: var(--navy-soft); border: 2px solid transparent;
}
/* Thai, reading, Indonesian: three lines that have to read in that order at a
   glance, so each step down is a real one in both size and weight. The reading
   was rendering at prompt size in bold green, level with the Thai it supports. */
.conv-bubble .prompt-roman {
  margin-top: 2px; font-size: .84rem; font-weight: 600; line-height: 1.35;
  color: var(--green-deep);
}
.conv-foot { display: flex; align-items: center; gap: 8px; margin-top: 4px; }
.conv-foot .conv-arti { margin-top: 0; flex: 1 1 auto; }
.conv-turn.b .conv-foot { flex-direction: row-reverse; }
/* The tail corner points at its speaker's side, which is what makes the two
   columns read as one conversation rather than two lists. */
.conv-turn.a .conv-bubble { border-bottom-left-radius: 6px; }
.conv-turn.b .conv-bubble { background: var(--canvas-2); border-bottom-right-radius: 6px; }
.conv-line {
  display: flex; flex-wrap: wrap; align-items: center; gap: 2px 4px;
  font-family: var(--font-thai); font-size: 1.15rem; font-weight: 600; color: var(--navy);
  line-height: 1.9;
}
.conv-arti { margin-top: 4px; font-size: .82rem; font-weight: 700; color: var(--ink-soft); }
/* An empty gap is the same dashed invitation as the ordering line, sized to a
   chip so filling one never reflows the sentence around it. */
.conv-gap {
  min-width: 62px; min-height: 34px; padding: 2px 10px;
  border: 2px dashed var(--ink-faint); border-radius: var(--r-sm);
  background: var(--surface); cursor: pointer;
  font-family: var(--font-thai); font-size: 1.05rem; font-weight: 600; color: var(--navy);
}
.conv-gap:hover { border-color: var(--navy); }
.conv-gap.ok { border-style: solid; border-color: var(--green); background: var(--green-soft); }
.conv-gap.wrong { border-style: solid; border-color: var(--red); background: var(--red-soft); text-decoration: line-through; }
.conv-fix {
  margin-left: 4px; padding: 2px 8px; border-radius: var(--r-sm);
  background: var(--green-soft); color: var(--green-deep);
  font-family: var(--font-thai); font-size: 1.05rem; font-weight: 700;
}
/* The per-turn replay, in the sky the whole app uses for audio. */
.conv-say {
  flex: 0 0 auto; width: 30px; height: 30px; padding: 0;
  border: 2px solid var(--line); border-radius: 50%;
  background: var(--surface); color: var(--sky-deep); cursor: pointer;
}
.conv-say svg { width: 18px; height: 18px; display: block; margin: auto; }
.conv-say.playing { color: var(--navy); border-color: var(--sky); background: #F2FAFE; }
/* The tray is pinned under the thread, not inside its scroller: the chips have
   to be on screen for the card to be answerable at all, and on a 375px phone a
   four-turn thread with its readings put the whole tray below the fold. Only
   the thread scrolls, like a chat. flex:none keeps the scroller from eating it. */
.drill-body .conv-tray { width: 100%; flex: none; margin-top: 0; padding: 4px 0 2px; }
/* Pick the reply: the turn under study is an empty bubble rather than a bubble
   with holes in it, so it is dashed like a gap and sized like one. */
.conv-bubble.ask {
  min-width: 108px; min-height: 46px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  border: 2px dashed var(--ink-faint); background: var(--surface);
}
.conv-bubble.ask .conv-arti { margin-top: 2px; text-align: center; }
.conv-bubble.ask span {
  font-family: var(--font-display); font-size: 1.4rem; font-weight: 800; color: var(--ink-faint);
}
/* Say your turn: your own line is the prompt, so its bubble is the lit one and
   the partner's turns stay quiet behind it. */
.conv-turn.mine .conv-bubble { background: var(--yellow); box-shadow: 0 3px 0 0 var(--yellow-deep); }
.conv-turn.mine .conv-line { font-size: 1.34rem; }
.conv-you {
  margin-bottom: 3px; font-size: .66rem; font-weight: 800; letter-spacing: .14em;
  color: var(--navy); opacity: .7;
}
@media (max-width: 380px) {
  .conv-line { font-size: 1.05rem; }
  .conv-turn { max-width: 100%; }
  /* A small phone gives the thread the room: the chips tighten up so eight of
     them fit in two rows, and a gap is sized to the smaller chip so it forces
     fewer line breaks inside the bubble. */
  .conv-tray { gap: 6px; }
  .conv-tray .ord-chip { padding: 7px 10px; font-size: 1rem; }
  .conv-tray .ord-chip .opt-roman { font-size: .7rem; margin-top: 0; }
  .conv-gap { min-width: 52px; min-height: 30px; }
}

.drill-loading { padding: 60px 10px; text-align: center; font-weight: 700; color: var(--ink-soft); }
.mtile.is-audio { position: relative; color: var(--sky-deep); --eq-min: 7px; --eq-max: 26px; }
.mtile.is-audio img { width: 30px; height: 30px; transition: opacity .15s ease; }
.mtile.is-audio .eq { gap: 4px; }
.mtile.is-audio .eq i { width: 4.5px; }
.mtile.is-audio.playing img { opacity: 0; }
/* Framed inside the curriculum console: the screen is the whole page there. */
body.preview .tabbar, body.preview .drill-close { display: none; }
body.preview .app-main { padding-bottom: 12px; }

/* An admin walking a draft version's journey (?pv= on boot, see state.previewVersion). */
.draft-banner {
  background: #fbbf24; color: #422006; font-weight: 600; font-size: 13px;
  text-align: center; padding: 7px 12px; position: sticky; top: 0; z-index: 50;
}

.write-sub { display: flex; align-items: center; justify-content: center; gap: 9px; margin-top: 7px; }
.write-sub .write-meaning { font-size: .9rem; font-weight: 700; color: var(--ink-soft); }
.write-sub .letter-chip { margin-top: 0; }
/* The writing space is the biggest tile on screen (capped by viewport height so
   the tools stay above the fold) and wears the same chassis as the option
   tiles: solid border + 3D bottom edge, not a dashed placeholder. */
.pad-wrap {
  position: relative; width: 100%; max-width: min(380px, 42vh); aspect-ratio: 1 / 1;
  background: var(--surface); border: 2px solid var(--line); border-radius: var(--r-lg);
  box-shadow: 0 4px 0 0 var(--line);
  overflow: hidden;
}
.pad-ghost {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-family: "JLT Carrier", var(--font-thai); /* dotted vowel carrier, like the table */
  font-size: 16rem; line-height: 1; color: var(--ink);
  opacity: .18; pointer-events: none;
}
/* Stroke guide: the pulsing dot is where the pen starts, the dashes run along
   the letter's own centreline (they march in the writing direction) and each
   arrowhead ends a stroke. It sits under the ink canvas, over the ghost. */
.pad-guide { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.pad-guide .pg-halo { fill: none; stroke: var(--surface); stroke-width: 10; stroke-linecap: round; stroke-linejoin: round; opacity: .5; }
.pad-guide .pg-line {
  fill: none; stroke: var(--green-deep); stroke-width: 5; stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 2 16; animation: pgMarch 1.4s linear infinite;
}
.pad-guide .pg-line.pg-solid { stroke-dasharray: none; animation: none; }
.pad-guide .pg-head { fill: var(--green-deep); stroke: var(--surface); stroke-width: 2; }
.pad-guide .pg-dot { fill: var(--green-deep); stroke: var(--surface); stroke-width: 3; transform-box: fill-box; transform-origin: center; animation: pgPulse 1.5s ease-in-out infinite; }
@keyframes pgPulse { 50% { transform: scale(1.4); } }
@keyframes pgMarch { to { stroke-dashoffset: -18; } }
@media (prefers-reduced-motion: reduce) {
  .pad-guide .pg-dot, .pad-guide .pg-line { animation: none; }
}
.pad { position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; cursor: crosshair; }
/* Same score chip as Berbicara, sized to take over the hint's line so the
   buttons never get pushed off the bottom of the screen. */
.write-out { min-height: 0; justify-content: center; }
.write-out:empty { display: none; }
.write-out .speak-score { font-size: 1.4rem; }
.pad-tools { display: flex; gap: 10px; align-items: center; }
.pad-clear, .pj-no, .drill-exit {
  padding: 12px 20px; border-radius: var(--r-sm); cursor: pointer;
  background: var(--surface); border: 2px solid var(--line); box-shadow: 0 3px 0 0 var(--line);
  font-family: var(--font-display); font-weight: 800; font-size: .9rem; color: var(--ink-2);
}
.pad-clear:active, .pj-no:active, .drill-exit:active { transform: translateY(3px); box-shadow: 0 0 0 0 var(--line); }
.pad-judge { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.pad-judge p { margin: 0; font-weight: 800; color: var(--navy); }
.pad-judge-btns { display: flex; gap: 10px; }
.pj-yes {
  padding: 12px 24px; border-radius: var(--r-sm); cursor: pointer;
  background: var(--green); border: 2px solid var(--green-deep); box-shadow: 0 3px 0 0 var(--green-deep);
  font-family: var(--font-display); font-weight: 800; font-size: .9rem; color: #fff;
}
.pj-yes:active { transform: translateY(3px); box-shadow: 0 0 0 0 var(--green-deep); }

.drill-qcard .speak-word { align-self: center; }
.rec-btn {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; max-width: 340px; padding: 15px 18px; border-radius: var(--r-md); cursor: pointer;
  background: var(--navy); border: 2px solid var(--navy-deep); box-shadow: 0 5px 0 0 var(--navy-deep);
  font-family: var(--font-display); font-weight: 800; font-size: .95rem; color: #fff;
  user-select: none; -webkit-user-select: none; touch-action: none;
}
.rec-btn img { width: 22px; height: 22px; filter: brightness(0) invert(1); }
.rec-btn:active { transform: translateY(5px); box-shadow: 0 0 0 0 var(--navy-deep); }
.rec-btn.is-recording { background: var(--red); border-color: var(--red-deep); box-shadow: 0 5px 0 0 var(--red-deep); animation: pulse 1s ease-in-out infinite; }
.speak-out { min-height: 34px; display: flex; align-items: baseline; gap: 7px; font-weight: 700; color: var(--ink-soft); text-align: center; }
.speak-score { font-family: var(--font-display); font-weight: 800; font-size: 2rem; line-height: 1; }
.speak-score.s-ok { color: var(--green-deep); }
.speak-score.s-mid { color: var(--yellow-deep); }
.speak-score.s-bad { color: var(--red-deep); }
.speak-verdict { font-size: .85rem; }
/* Bottom band: pinned to the frame's bottom edge, the width of the column it
   closes (the shared .drill-body rule above sets that width). */
.drill-actions { width: 100%; display: grid; margin-top: auto; }

/* The finish screen owns the whole drill column, so it centers in it instead of
   hanging off the top with dead space below; both buttons share one width. */
.drill-done { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center;
  flex: 1 1 auto; gap: 12px; padding: 24px 0 32px; text-align: center; overflow: hidden; }
.drill-done > * { position: relative; z-index: 2; }
/* Re-asserts absolute over the z-index rule above; vh, not %, because a
   translateY percentage would resolve against the 12px confetti chip itself. */
.drill-done .rain { position: absolute; inset: 0; --fall: 92vh; z-index: 1; }
.drill-done img { width: 150px; height: auto; }
.drill-done h2 { margin: 4px 0 0; font-family: var(--font-display); font-weight: 800; font-size: 1.75rem; color: var(--navy); }
.drill-done p { margin: 0 0 14px; font-weight: 700; color: var(--ink-soft); }
.drill-done .btn-primary, .drill-done .drill-exit { width: 100%; max-width: 320px; }
/* Ulangi and Lanjut ke siklus each carry the glass they will cost; the size
   override is needed because .drill-done img is the 150px mascot rule. */
.drill-done .btn-primary, .drill-done .drill-exit { display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
.drill-done .btn-primary img, .drill-done .drill-exit img { width: 20px; height: 20px; }

/* Mascot on a warm halo that breathes; the win/soft tint keeps a shaky pass
   from getting the same gold fanfare as a clean one. */
.drill-fig { position: relative; display: grid; place-items: center; }
.drill-fig::before {
  content: ""; position: absolute; width: 200px; height: 200px; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,225,0,.42), rgba(255,225,0,0) 68%);
  animation: welcome-halo-breathe 3.6s ease-in-out infinite;
}
.soft .drill-fig::before { background: radial-gradient(circle, rgba(142,202,230,.5), rgba(142,202,230,0) 68%); }

/* The result reads as a chip, not a caption, with the numbers carrying weight. */
.drill-stat {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 9px 18px; border-radius: 999px;
  background: var(--surface); border: 2px solid var(--line); box-shadow: var(--lift-sm);
  font-weight: 700; color: var(--ink-2);
}
.drill-stat b { font-family: var(--font-display); font-weight: 800; font-size: 1.15rem; color: var(--navy); font-variant-numeric: tabular-nums; }
.win .drill-stat { border-color: var(--green); background: var(--green-soft); color: var(--green-deep); }
.win .drill-stat b { color: var(--green-deep); }
/* Under the score on a failed run: what the pass mark was and what to do. */
.drill-retry { max-width: 30ch; margin: 0 auto; color: var(--ink-2); font-weight: 600; line-height: 1.5; }
.drill-retry b { color: var(--navy); font-variant-numeric: tabular-nums; }

/* Entrance cascade: mascot lands and floats, then the copy and buttons rise
   behind it. The reduced-motion block near the keyframes kills all of it. */
.fin .drill-fig img { animation: welcome-mascot-in .6s cubic-bezier(.2,.9,.3,1.15) both, welcome-mascot-float 4.6s ease-in-out 1s infinite; }
.fin h2 { animation: pop-bounce .5s cubic-bezier(.2,.9,.3,1.4) .22s both; }
.fin .drill-stat { animation: pop .45s ease-out .4s both; }
.fin .drill-retry { animation: pop .45s ease-out .48s both; }
.fin .btn-primary { animation: pop .45s ease-out .55s both; }
.fin .drill-exit { animation: pop .45s ease-out .66s both; }

/* ---------- JLT Premium plan sheet (task 87, admin-only mockup) ---------- */
/* Plan sheet: three rows, one selected, the same edge-and-press language as
   the title tiers (.tl-tier) so it reads as the app, not as a checkout. */
.plan-sheet { display: flex; flex-direction: column; gap: 8px; }
.plan-row { display: flex; align-items: center; gap: 12px; width: 100%; padding: 12px 14px; text-align: left; cursor: pointer;
  background: var(--surface); border: 2px solid var(--line); border-radius: var(--r-md); box-shadow: 0 3px 0 0 var(--line); font: inherit; }
.plan-row.cur { background: var(--yellow); border-color: var(--yellow-deep); box-shadow: 0 3px 0 0 var(--yellow-deep); }
.plan-row:active { transform: translateY(2px); box-shadow: 0 1px 0 0 var(--line); }
.plan-dur { flex: 0 0 74px; font-family: var(--font-display); font-weight: 800; font-size: 1rem; color: var(--navy); line-height: 1.05; }
.plan-dur small { display: block; font-family: var(--font-body); font-size: .62rem; font-weight: 900; letter-spacing: .08em; text-transform: uppercase; color: var(--green-deep); margin-top: 3px; }
.plan-row.cur .plan-dur small { color: var(--navy-deep); }
.plan-price { flex: 1 1 auto; min-width: 0; }
.plan-was { display: block; font-size: .74rem; font-weight: 700; color: var(--ink-faint); text-decoration: line-through; }
.plan-row.cur .plan-was { color: rgba(35,33,95,.55); }
.plan-now { display: block; font-family: var(--font-display); font-weight: 800; font-size: 1.12rem; color: var(--navy); font-variant-numeric: tabular-nums; }
.plan-per { flex: 0 0 auto; text-align: right; font-size: .74rem; font-weight: 800; color: var(--ink-soft); line-height: 1.2; }
.plan-per b { display: block; font-family: var(--font-display); font-size: .95rem; color: var(--navy); font-variant-numeric: tabular-nums; }
.plan-save { flex: 0 0 auto; font-size: .64rem; font-weight: 900; padding: 4px 8px; border-radius: 999px; background: var(--green-soft); color: var(--green-deep); border: 1.5px solid #BFEBCF; }
.plan-row.cur .plan-save { background: var(--surface); border-color: var(--yellow-deep); color: var(--navy); }
.plan-perks { list-style: none; margin: 8px 0 6px; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.plan-perks li { display: flex; align-items: center; gap: 9px; font-weight: 700; color: var(--ink-2); font-size: .9rem; }
.plan-perks img, .plan-perks svg { width: 22px; height: 22px; flex: none; }
.plan-sheet .btn-primary { width: 100%; }
.plan-sheet .drill-exit { width: 100%; margin-top: 8px; }
.plan-sheet .drill-exit:disabled { opacity: .55; cursor: default; }
/* 5-second lock: the X becomes a countdown ring (yellow fills clockwise via
   --p) with the seconds left in the middle; pointer-events off until 0. */
.sheet-x.locked { pointer-events: none; font-family: var(--font-display); font-size: 1rem; padding: 0; color: var(--navy);
  background: radial-gradient(closest-side, var(--surface) 74%, transparent 76%), conic-gradient(var(--yellow) var(--p, 0%), var(--line) 0); }
/* Under the tea countdown: one line and a button into the plan sheet. */
.energy-sheet .energy-offer { margin: -6px 0 14px; padding: 14px 16px; border-radius: var(--r-lg); background: var(--navy-soft); border: 2px solid #DEDDF6; text-align: left; display: flex; align-items: center; gap: 12px; }
.energy-offer img { width: 40px; height: 40px; flex: none; }
.energy-offer b { display: block; font-family: var(--font-display); color: var(--navy); font-size: .95rem; }
.energy-offer small { color: var(--ink-2); font-weight: 600; font-size: .8rem; }
.energy-sheet .energy-offer-cta { width: 100%; margin: 0; }
.energy-offer-cta + .energy-refill { margin-top: 12px; }
/* Payment method sheet: grouped rows on the same edge-and-press tile. */
.pay-sheet { display: flex; flex-direction: column; gap: 8px; }
.pay-group { margin: 6px 0 0; font-size: .7rem; font-weight: 900; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-soft); }
.pay-group:first-child { margin-top: 0; }
.pay-row { display: flex; align-items: center; gap: 12px; width: 100%; padding: 12px 14px; text-align: left; cursor: pointer; font: inherit;
  background: var(--surface); border: 2px solid var(--line); border-radius: var(--r-md); box-shadow: 0 3px 0 0 var(--line); color: var(--navy); }
.pay-row:active { transform: translateY(2px); box-shadow: 0 1px 0 0 var(--line); }
.pay-row:disabled { opacity: .6; }
.pay-ic { flex: none; width: 36px; height: 36px; display: grid; place-items: center; border-radius: 10px; background: var(--navy-soft); color: var(--navy); }
.pay-ic svg { width: 20px; height: 20px; }
.pay-name { flex: 1 1 auto; font-family: var(--font-display); font-weight: 800; font-size: .95rem; }
.pay-chev { flex: none; width: 18px; height: 18px; color: var(--ink-faint); }
