@import url('https://fonts.googleapis.com/css2?family=Zilla+Slab:wght@600;700&family=Outfit:wght@400;500;600;700&family=Noto+Sans+Ethiopic:wght@400;500;600;700&display=swap');
/* ── theme tokens ──────────────────────────────────────────
   --ink itself is deliberately NOT a dark-mode token — it's the fixed
   near-black brand colour used for a handful of always-dark chrome
   (header, .btn.solid, toast, memory-card backs, the PIN pad's pressed
   state) that's meant to read as "dark" in BOTH themes, the same way it
   already sits dark against the light page today. --on-ink is its
   paired text colour (white/cream) for exactly the same reason — it
   never needs to change either, since it's always sitting on --ink.
   --text is the one that actually inverts: ordinary body/label/heading
   text, which needs to flip from near-black (light mode) to a warm
   off-white (dark mode) to stay readable as --sand/--shell/--sand2
   invert underneath it. --ink2/--ink3 are pure secondary/tertiary text
   (never used as a background anywhere in this file), so they redefine
   safely on their own without needing a separate --text-style split.
   Three-state pattern: :root is the light palette (also what an
   unstamped root — the default "match system" setting — falls back to
   outside the dark media query); prefers-color-scheme:dark redefines
   tokens for a device set to dark with no in-app override;
   [data-theme] redefines them again so an explicit in-app choice (see
   the Appearance control in Parent area) always wins over the device. */
:root{
 --o1:#1E7A5A;--o2:#C98A0E;--o3:#C4362A;--o4:#2E4A9E;--o5:#7B3F72;--o6:#A65A2E;--o7:#17798E;
 /* --shell was a pure #fff — a family asked for something gentler on a
    child's eyes, and a stark white card sitting on the warm --sand page
    behind it is exactly the kind of glare that request is about (most
    noticeable in a dim room). A very slightly warm off-white keeps
    cards clearly lighter than the page (still visually "raised", still
    distinct from --sand2) without the same harsh contrast. */
 --sand:#F7EDD9;--sand2:#FDF8EE;--shell:#FFFBF3;--ink:#241611;--ink2:#5C463C;--ink3:#8C7568;
 --line:#E3D3B8;--ok:#1E7A5A;--bad:#C4362A;
 --text:#241611;--on-ink:#FDF8EE;--ok-bg:#E4F1EA;--bad-bg:#FBE7E5;--splash-glow:#FBF3E0;
 /* The app's one "this is MY colour" accent — same 7 brand hues as
    everywhere else (never an arbitrary custom colour, so it always
    stays harmonious with pack tiles, the tibeb stripe, etc.), just
    picked rather than fixed. Two sources, same variable: a parent's own
    device-level pick (Appearance, see setAccent) for the shared screens
    (Welcome, Who's playing, Parent area), automatically swapped for
    whichever child's OWN colour (already chosen when their profile was
    set up — see CHILD_COLOURS/pickCol) the moment they're chosen to
    play — see chooseChild/showWho in app.js. Deliberately NOT applied
    to .btn.solid or the header (those stay the fixed --ink chrome
    established for dark mode) or the tibeb stripe (that's the
    traditional multi-colour motif, not a themeable UI accent) — this is
    for "active/selected" indicators, not the whole chrome. */
 --accent:var(--o1);
 --ge:'Noto Sans Ethiopic','Abyssinica SIL',Nyala,Kefa,'Noto Serif Ethiopic',serif;
 --dis:'Zilla Slab',Rockwell,Georgia,serif;--bd:'Outfit','Segoe UI',system-ui,sans-serif;--r:18px;
 --sb:env(safe-area-inset-bottom,0px);--st:env(safe-area-inset-top,0px);color-scheme:light;}
@media(prefers-color-scheme:dark){:root:not([data-theme="light"]){
 --sand:#17110B;--sand2:#221810;--shell:#2B1F16;--line:#3D2E20;
 --text:#F3E9D8;--ink2:#C9B8A3;--ink3:#9C8A76;
 --ok-bg:#1B3328;--bad-bg:#3A1F1C;--splash-glow:#241a10;
 color-scheme:dark;}}
:root[data-theme="dark"]{
 --sand:#17110B;--sand2:#221810;--shell:#2B1F16;--line:#3D2E20;
 --text:#F3E9D8;--ink2:#C9B8A3;--ink3:#9C8A76;
 --ok-bg:#1B3328;--bad-bg:#3A1F1C;--splash-glow:#241a10;
 color-scheme:dark;}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none}
html,body{margin:0;padding:0;overscroll-behavior:none}
/* Text size (setTextSize in app.js) — the whole stylesheet is written
   in fixed px, not rem, so a root font-size change wouldn't reach any
   of it; `zoom` scales the entire rendered page (layout, spacing, text,
   fixed-position chrome all together) the same way a browser's own
   page-zoom does, without needing every rule in this file rewritten to
   relative units. Medium (the default) sets no attribute at all. */
html[data-textsize="small"]{zoom:.92}
html[data-textsize="large"]{zoom:1.15}
body{background:var(--sand);color:var(--text);font-family:var(--bd);font-size:17px;line-height:1.45;
 -webkit-font-smoothing:antialiased}
body.app{padding-bottom:calc(104px + var(--sb))}
.ge{font-family:var(--ge)}
button{font:inherit;color:inherit;cursor:pointer;border:none;background:none;touch-action:manipulation}
:focus-visible{outline:3px solid var(--o4);outline-offset:2px;border-radius:8px}
.fkey{border-radius:10px;transition:transform .1s ease,background .15s ease}
.fkey:active{transform:scale(.82)}
.fkey.tapped{background:var(--sand2);box-shadow:inset 0 0 0 3px var(--ink)}
/* Same three-state pattern as the theme/accent tokens above (see
   setMotion in app.js): the OS setting is respected by default, but an
   explicit in-app choice — either direction — always wins over it,
   since not everyone who wants stillness (or wants full motion despite
   their OS default) knows how or is able to dig into their device's
   own accessibility settings to get it. */
@media(prefers-reduced-motion:reduce){:root:not([data-motion="normal"]) *{animation:none!important;transition:none!important}}
:root[data-motion="reduce"] *{animation:none!important;transition:none!important}
.confetti-piece{position:fixed;top:-24px;pointer-events:none;z-index:200;line-height:1;
 animation:confettiFall 1.7s ease-in forwards}
@keyframes confettiFall{
  0%{transform:translateY(0) rotate(0deg);opacity:1}
  100%{transform:translateY(108vh) rotate(340deg);opacity:0}
}
/* Full-screen video playback for a video recording — see playVideoOverlay
   in app.js. Above everything else (sheets included, z-index 400+) since
   it can be triggered from inside one (e.g. the record sheet's own ▶). */
.video-overlay{position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:400;
 display:flex;align-items:center;justify-content:center;padding:20px}
.video-overlay-card{position:relative;max-width:420px;width:100%}
.video-overlay-card video{width:100%;border-radius:16px;display:block;background:#000}
.video-overlay-card button{position:absolute;top:-16px;right:-16px;width:36px;height:36px;
 border-radius:50%;background:var(--ink);color:var(--on-ink);font-size:15px;
 display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,.3)}

/* The once-a-day "here's today" greeting (todayGreetingOverlay in
   app.js) — same fixed full-screen overlay convention as .video-overlay
   above, just a lighter/warmer backdrop (a friendly greeting, not a
   video player) and no close button, since tapping anywhere dismisses
   it and it also clears itself on a short timer regardless. */
.today-overlay{position:fixed;inset:0;background:rgba(36,22,17,.55);z-index:210;
 display:flex;align-items:center;justify-content:center;padding:20px;
 animation:todayFadeIn .25s ease}
@keyframes todayFadeIn{from{opacity:0}to{opacity:1}}
.today-overlay-card{background:var(--shell);border-radius:var(--r);padding:28px 24px;
 max-width:340px;width:100%;text-align:center;box-shadow:0 12px 40px rgba(0,0,0,.3)}
/* A plain "Tap anywhere to continue" read as flat for a kids' app — this
   button has no click handler of its own, it relies on the click
   bubbling up to .today-overlay's own onclick (same as tapping anywhere
   else on the card), the animation is what's meant to say "tap me". */
.today-cta{animation:todayCtaBounce 1.6s ease-in-out infinite}
@keyframes todayCtaBounce{0%,100%{transform:scale(1)}50%{transform:scale(1.045)}}

/* The curated-video player (openVideoPlayer) — YouTube's own controls
   are turned off (controls:0) and its iframe is pointer-events:none, so
   this overlay is the only interactive surface; a tap toggles play/pause
   through the Player API rather than anything on the iframe itself. */
.yt-wrap{position:relative;border-radius:16px;overflow:hidden;background:#000;aspect-ratio:16/9}
/* #ytHost starts as a plain div, but new YT.Player('ytHost',...) REPLACES
   it with the actual <iframe>, carrying the same id — there is no nested
   iframe to select. Without an explicit width/height here the replaced
   iframe falls back to YouTube's own default pixel size (a fixed ~640
   x390) instead of filling this box, which is exactly the "tiny video
   in the corner of a big black rectangle" bug this fixes. */
.yt-wrap #ytHost{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
.yt-overlay{position:absolute;inset:0;cursor:pointer}
.yt-center-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
 font-size:46px;color:#fff;text-shadow:0 2px 10px rgba(0,0,0,.6);transition:opacity .15s;pointer-events:none}
.yt-controls{position:absolute;left:0;right:0;bottom:0;padding:10px 12px;display:flex;align-items:center;gap:10px;
 background:linear-gradient(transparent,rgba(0,0,0,.65))}
.yt-controls span{color:#fff;font-size:12px;font-family:var(--dis);font-weight:600}
.yt-controls .btn{background:rgba(255,255,255,.18);color:#fff;border-color:rgba(255,255,255,.35)}
/* Shown only during the timed ad-skip unlock (see ytUnlockForAd) — an
   L-shaped pair of opaque panels covering everything except the
   bottom-right corner, where YouTube always puts its Skip button. The
   real iframe underneath is only reachable through that one gap, so a
   tap can land on Skip but never on the YouTube logo, video title, or
   "Watch on YouTube" link, wherever an ad happens to place them. */
.yt-skip-guard{position:absolute;background:rgba(0,0,0,.001);cursor:not-allowed;z-index:2}
.yt-skip-hint{position:absolute;right:10px;bottom:10px;width:min(44%,220px);text-align:right;
 color:#fff;font-size:11px;font-family:var(--dis);font-weight:600;text-shadow:0 1px 4px rgba(0,0,0,.8);
 pointer-events:none;z-index:2}
.tibeb{position:relative;height:14px;overflow:hidden;flex:none;background:repeating-linear-gradient(90deg,
 var(--o1) 0 16px,var(--o2) 16px 32px,var(--o3) 32px 48px,var(--o4) 48px 64px,
 var(--o5) 64px 80px,var(--o6) 80px 96px,var(--o7) 96px 112px)}
.tibeb::after{content:'';position:absolute;inset:0;background:
 repeating-linear-gradient(135deg,rgba(255,255,255,.5) 0 2px,transparent 2px 9px),
 repeating-linear-gradient(45deg,rgba(0,0,0,.14) 0 2px,transparent 2px 9px)}

/* screens */
.screen{display:none;min-height:100vh}
.screen.on{display:block}
.mid{max-width:560px;margin:0 auto;padding:24px 20px calc(40px + var(--sb))}
/* min-height:100vh, not 100svh — welcome screen only, see below for why
   that distinction matters here specifically. */
#s-welcome .mid{min-height:100vh;min-height:100svh;display:flex;flex-direction:column;justify-content:safe center}
/* Same soft glow the splash screen already uses behind its logo — ties
   the two "entry" screens (sign in, then who's playing) to the same
   quiet atmosphere the splash establishes, rather than dropping
   straight into a flat page after that one staged moment ends. */
#s-welcome, #s-who{background:radial-gradient(circle at 50% 0%,var(--splash-glow) 0%,var(--sand) 60%)}
.hero{text-align:center;padding:34px 0 22px}
.hero p{color:var(--ink2);font-size:15px;margin:0}

/* logo — a wordmark, not a separate icon: the existing ሓደራ lettering,
   given a proper lockup (accent rule + tracked-out Latin name) instead of
   being rendered as plain paragraph text. hadera-lockup.png is a real
   transparent PNG (verified directly — no baked-in white anywhere in
   it), so it just sits on whatever background is actually behind it,
   no card needed. (A previous version of this comment claimed the PNG
   had an opaque white background and used --logo-badge to paper over
   it with a matching card — that was stale relative to the actual
   file and is exactly what was producing a visible white box on the
   welcome screen's cream background; removed rather than fixed
   forward, since a transparent image never needed a card at all.) */
.brand-logo{display:block;margin:0 auto;width:100%;max-width:220px;height:auto}
/* The splash's own emblem — hadera-lockup.png's family/heart/book
   artwork cropped down to just that (see brand/hadera-emblem.png),
   with no logo-badge card behind it (unlike .brand-logo above): it's
   illustration, not a wordmark needing a background to sit on. Sized
   smaller than the old full-width lockup was, since the animated
   wordmark below now carries part of the visual weight the lockup's
   own baked-in text used to. */
/* left:-14px is a deliberate optical correction, not a mistake. The
   emblem's outer bounding box (sound waves included) already centers
   correctly, but the heart shape itself — the actual symmetric element
   a viewer's eye locks onto — sits well right of that box's own centre
   inside the artwork (measured directly off the heart's two peaks:
   consistently ~30px right of the canvas centre in the 409px source,
   across every row checked). A first attempt nudged the whole box
   right to compensate, on the mistaken assumption the visual weight
   sat left — a family caught that it hadn't actually fixed it; this
   value is re-measured off the heart's own symmetry, not a redraw of
   the earlier guess. position:relative shifts it after layout/
   centering is computed, so it doesn't fight .pulse's own
   transform-based animation the way nudging via margin or transform
   here would. */
.brand-emblem{display:block;margin:0 auto;width:auto;height:auto;max-width:190px;max-height:180px;position:relative;left:-14px}
/* Two animations chained on one element: a one-shot entrance (rises in
   from a slight dip while fading up) handing off to an infinite gentle
   pulse. Comma-separated animation values run in parallel, not in
   sequence — logoPulse's own .8s delay is what makes the handoff land
   right as the entrance finishes, instead of both fighting over opacity
   from frame one. Shared by both the splash emblem and (on the welcome
   screen) the full lockup, whichever image happens to carry .pulse. */
.pulse{animation:splashLogoIn .8s cubic-bezier(.2,.8,.2,1) both,
 logoPulse 2.4s ease-in-out .8s infinite}
@keyframes splashLogoIn{0%{opacity:0;transform:translateY(14px) scale(.9)}100%{opacity:1;transform:none}}
@keyframes logoPulse{0%,100%{opacity:1}50%{opacity:.84}}
/* The app's actual name, animating in one letter at a time right after
   the emblem — real text, not baked into an image, specifically so this
   is possible. Each letter is declared fully visible (opacity:1) at
   rest, with the animation only ever adding a temporary entrance on top
   (fill-mode "both") — same reasoning as every other splash element
   above: prefers-reduced-motion strips the animation globally (see that
   rule near the top of this file) and must never leave a letter stuck
   invisible because its *starting* keyframe was the only place opacity
   was ever set. */
.splash-word{margin-top:12px;display:flex;align-items:baseline;justify-content:center;
 gap:9px;flex-wrap:wrap}
.sw-en{font-family:var(--dis);font-weight:700;font-size:38px;color:#0B4756;letter-spacing:.01em}
.sw-ge{font-family:var(--ge);font-weight:700;font-size:32px;color:#AD8A3E}
.splash-word span span{display:inline-block;opacity:1;animation:letterIn .45s cubic-bezier(.2,.8,.2,1) both}
.sw-en span:nth-child(1){animation-delay:.15s}
.sw-en span:nth-child(2){animation-delay:.20s}
.sw-en span:nth-child(3){animation-delay:.25s}
.sw-en span:nth-child(4){animation-delay:.30s}
.sw-en span:nth-child(5){animation-delay:.35s}
.sw-en span:nth-child(6){animation-delay:.40s}
.sw-ge span:nth-child(1){animation-delay:.45s}
.sw-ge span:nth-child(2){animation-delay:.50s}
.sw-ge span:nth-child(3){animation-delay:.55s}
@keyframes letterIn{0%{opacity:0;transform:translateY(10px)}100%{opacity:1;transform:none}}
/* Sits below the animated "Hadera ሓደራ" lockup (a new sibling block, not
   part of that flex row, so it naturally lands on its own line) — a
   small, letter-spaced caption rather than a third same-size word, so it
   reads as "this is a family app" without competing with the wordmark
   above it. Timed to fade in right after ሓደራ's own last letter finishes
   (see .sw-ge's animation-delay above) and just before .splash-rule. */
.splash-family{margin-top:3px;font-family:var(--dis);font-weight:600;font-size:14px;
  letter-spacing:.22em;text-transform:uppercase;color:var(--ink3);opacity:0;
  animation:letterIn .5s cubic-bezier(.2,.8,.2,1) .82s both}

/* footer — sits right under the sign-in form as part of one centred
   group (.mid uses justify-content:center), not pinned to the very
   bottom of the viewport — that older approach left a large dead gap
   above it on any screen taller than the form itself, and needed
   scrolling to reach on mobile whenever 100vh over-counted the visible
   area (address bar overlap). */
.site-footer{margin-top:28px;padding-top:0;text-align:center;font-size:12.5px;color:var(--ink3)}
.site-footer a{color:var(--ink2);text-decoration:none;font-weight:600}
.site-footer a:hover{text-decoration:underline}
.site-footer .links{margin-bottom:6px}
.site-footer .links span{margin:0 8px;opacity:.6}
/* the publisher credit, one notch quieter than the copyright line above it
   — a small badge rather than a second logo, since the app's own brand
   (Hadera) already carries the screen. */
.maker-credit{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:8px;opacity:.6;font-size:11px}
.maker-credit img{width:18px;height:18px;border-radius:50%}

/* splash — full-viewport, centred, no chrome (no header/tibeb bar/nav) so
   it reads as a moment on its own rather than a half-loaded app screen.
   The entrance is staged (logo → colour flourish → tagline → loading
   dots), each waiting its turn via animation-delay — that staging, more
   than any single effect, is what reads as "designed" rather than
   "thrown together". prefers-reduced-motion already strips every
   animation app-wide (see the *{animation:none} rule near the top of
   this file), so this degrades to a plain static splash for anyone who
   asked for that. */
#s-splash{background:radial-gradient(circle at 50% 38%,var(--splash-glow) 0%,var(--sand) 62%);overflow:hidden}
.splash-mid{min-height:100vh;display:flex;flex-direction:column;align-items:center;
 justify-content:center;text-align:center;padding:24px}
/* the same seven brand colours as .tibeb, drawn as a thin rule that grows
   in under the logo rather than just appearing — a small flourish tying
   the splash to the rest of the app's colour language. */
/* width/opacity below are declared at their FINAL (fully revealed)
   values, not the animation's starting point — a running animation
   overrides them for its own duration regardless (including during
   animation-delay, via fill-mode "both"), so this doesn't change the
   normal animated appearance at all. What it fixes: anyone with
   prefers-reduced-motion set strips the animation entirely (see the
   global *{animation:none!important} rule near the top of this file),
   and was left with these elements permanently stuck at their old
   starting values (width:0 / opacity:0) — invisible forever, not just
   unanimated. That's exactly what "the line and text don't show up"
   turned out to be: not a desktop-vs-mobile rendering difference, a
   reduced-motion setting on that machine. */
.splash-rule{width:120px;height:3px;margin:22px auto 0;border-radius:3px;overflow:hidden;
 background:repeating-linear-gradient(90deg,var(--o1) 0 16px,var(--o2) 16px 32px,var(--o3) 32px 48px,
 var(--o4) 48px 64px,var(--o5) 64px 80px,var(--o6) 80px 96px,var(--o7) 96px 112px);
 animation:splashRuleIn .6s .8s cubic-bezier(.2,.8,.2,1) both}
/* max-width used to be a fixed 260px, which wrapped the longer English
   tagline ("...in our family's own voices") onto a second line on most
   phone widths — max-width:94vw + white-space:nowrap keeps it one line,
   and the clamp() shrinks the type to fit narrower screens instead of
   letting it wrap or run off the edge. */
.splash-tag{color:var(--text);font-weight:700;font-size:clamp(10px,3.4vw,13px);margin:18px 0 0;
 max-width:94vw;white-space:nowrap;opacity:1;animation:splashFadeUp .6s ease both}
.splash-tag.ge{animation-delay:1.05s}
.splash-tag:not(.ge){animation-delay:1.2s}
.splash-dots{display:flex;gap:7px;margin-top:28px;opacity:1;animation:splashFadeUp .5s 1.4s ease both}
.splash-dots span{width:7px;height:7px;border-radius:50%;background:var(--o1);
 animation:splashDot 1.1s ease-in-out infinite}
.splash-dots span:nth-child(2){background:var(--o2);animation-delay:.15s}
.splash-dots span:nth-child(3){background:var(--o4);animation-delay:.3s}
@keyframes splashRuleIn{0%{width:0;opacity:0}100%{width:120px;opacity:1}}
@keyframes splashFadeUp{0%{opacity:0;transform:translateY(6px)}100%{opacity:1;transform:none}}
@keyframes splashDot{0%,60%,100%{transform:translateY(0);opacity:.4}30%{transform:translateY(-7px);opacity:1}}

header{background:var(--ink);color:var(--on-ink);position:sticky;top:0;z-index:40;padding-top:var(--st)}
.bar{display:flex;align-items:center;gap:12px;padding:11px 18px;max-width:1100px;margin:0 auto}
.who{display:flex;align-items:center;gap:9px;background:rgba(255,255,255,.12);border-radius:99px;
 padding:5px 13px 5px 7px;min-height:42px}
.who .av{font-size:21px}
/* A small badge overlaid on the header avatar (not the whole .who
   button, which still just navigates back to Who is playing?) — a
   family asked for kids to be able to pick their own avatar any time,
   without a parent PIN. event.stopPropagation() on the badge itself
   (see index.html) keeps a tap on it from also triggering .who's own
   navigate-away. */
.who-av-wrap{position:relative;display:inline-flex}
.who-av-edit{position:absolute;right:-4px;bottom:-4px;width:15px;height:15px;border-radius:50%;
  background:var(--o1);display:flex;align-items:center;justify-content:center;font-size:9px;
  border:1.5px solid var(--ink);line-height:1}
.who .nm{font-size:14px;font-weight:600;max-width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.brand{flex:1;min-width:0;display:flex;align-items:center;gap:9px}
.brand-icon{width:28px;height:28px;border-radius:8px;flex:none}
.brand b{font-family:var(--ge);font-size:17px;display:block}
.brand span{font-size:11px;color:#C3AC98;text-transform:uppercase;letter-spacing:.07em}
.hlock{background:rgba(255,255,255,.12);border:none;color:var(--on-ink);width:34px;height:34px;
 border-radius:99px;font-size:16px;flex:none;cursor:pointer;display:flex;align-items:center;justify-content:center}
.hlock:active{transform:scale(.94)}
.syncdot{width:10px;height:10px;border-radius:9px;background:#6B5B50;flex:none}
.syncdot.ok{background:#4CAF7D}.syncdot.busy{background:#E0B23C;animation:blink 1s infinite}
.syncdot.err{background:#E0705F}
@keyframes blink{50%{opacity:.3}}

main{max-width:1100px;margin:0 auto;padding:20px}
.view{display:none}.view.on{display:block;animation:rise .25s ease both}
@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
h2{font-family:var(--dis);font-size:27px;margin:0 0 3px}
.sub{color:var(--ink2);font-size:15px;margin:0 0 18px}
h3{font-family:var(--dis);font-size:18px;margin:24px 0 10px}
.card{background:var(--shell);border:1px solid var(--line);border-radius:var(--r);padding:18px}
.note{background:var(--sand2);border:1px dashed var(--line);border-radius:12px;padding:12px 14px;
 font-size:14px;color:var(--ink2);display:flex;gap:11px;margin-bottom:16px}
.note b{color:var(--text)}
.note.warn{border-style:solid;border-color:var(--o3)}
.note.good{border-style:solid;border-color:var(--o1)}

nav{position:fixed;bottom:0;left:0;right:0;z-index:40;background:var(--shell);
 border-top:1px solid var(--line);box-shadow:0 -6px 26px rgba(36,22,17,.1);padding-bottom:var(--sb)}
.tabs{display:flex;max-width:1100px;margin:0 auto}
.tabs button{flex:1;padding:11px 2px 12px;display:flex;flex-direction:column;align-items:center;gap:4px;
 color:var(--ink3);font-size:11.5px;font-weight:600;border-top:3px solid transparent;margin-top:-1px;min-height:60px}
.tabs button .t{font-family:var(--ge);font-size:23px;line-height:1.1}
.tabs button[aria-pressed=true]{color:var(--text);border-top-color:var(--accent)}

.btn{border:1px solid var(--line);background:var(--sand2);border-radius:13px;padding:12px 17px;
 font-size:15px;font-weight:600;display:inline-flex;align-items:center;gap:8px;min-height:48px;justify-content:center}
.btn:active{transform:scale(.97)}
.btn.solid{background:var(--ink);color:var(--on-ink);border-color:var(--ink)}
.btn.rec{background:var(--o3);color:#fff;border-color:var(--o3)}
.btn.rec.live{animation:pulse 1.1s infinite}
@keyframes pulse{50%{box-shadow:0 0 0 11px rgba(196,54,42,.16)}}
.btn.mini{padding:8px 12px;font-size:13px;border-radius:11px;min-height:40px;white-space:nowrap}
.btn.big{font-size:19px;padding:17px 26px;border-radius:16px;min-height:60px}
.btn.wide{width:100%}
.btn[disabled]{opacity:.42;pointer-events:none}
.row{display:flex;gap:10px;flex-wrap:wrap}
/* A sub-tab strip nested inside a parent-area panel — Settings' own
   Appearance/Sync/Account/Feedback row is the one this was built for
   (see #pp-account in app.js). Same .btn.mini shape as the top-level
   Parent-area tabs above it, but visually contained in its own shaded
   strip and using the accent colour for its selected state instead of
   .solid's near-black — .solid is exactly what the top-level tab row
   above it ALSO uses for "this one's selected", so a sub-tab and a
   top-level tab could both be lit up black in the same colour, in what
   read as one long flat run of pills instead of two nested levels. */
.subtabs{display:inline-flex;gap:4px;flex-wrap:wrap;background:var(--sand2);
  border:1px solid var(--line);border-radius:15px;padding:4px;margin-bottom:14px}
.subtabs .btn{background:transparent;border-color:transparent}
.subtabs .btn.solid{background:var(--o4);border-color:var(--o4);color:#fff}
label{display:block;font-size:13px;font-weight:600;margin:14px 0 6px;color:var(--ink2)}
input,select,textarea{width:100%;padding:13px 14px;border:1px solid var(--line);border-radius:12px;
 background:var(--sand2);font:inherit;font-size:17px;color:var(--text);min-height:50px}
input.ge{font-family:var(--ge);font-size:21px}
.pw-field{position:relative}
.pw-field input{padding-right:46px}
/* Edge (and old IE) draws its own built-in reveal/clear icons inside
   type="password" fields — right on top of our own .pw-toggle eye,
   showing as a second, non-interactive icon that never changes when
   you type. Ours is the only one meant to be there. */
.pw-field input::-ms-reveal, .pw-field input::-ms-clear{display:none}
.pw-toggle{position:absolute;right:5px;top:50%;transform:translateY(-50%);width:38px;height:38px;
 border-radius:10px;background:transparent;font-size:18px;display:flex;align-items:center;justify-content:center}
.pw-toggle:active{background:var(--line)}
.two{display:grid;grid-template-columns:1fr 1fr;gap:12px}

/* child picker */
.kids{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}
.kid{background:var(--shell);border:1px solid var(--line);border-top:6px solid var(--c);
 border-radius:var(--r);padding:13px 12px;text-align:center;min-height:104px}
.kid:active{transform:scale(.96)}
.kid .av{font-size:34px;line-height:1}
.av img{width:1em;height:1em;border-radius:50%;object-fit:cover;vertical-align:middle}
.kid .nm{font-family:var(--dis);font-weight:700;font-size:16px;margin-top:5px}
.kid .st{font-size:11.5px;color:var(--ink3);margin-top:2px}
.kid.add{border-top-color:var(--accent);border-style:dashed;color:var(--ink3)}

/* This screen's own compact hero — the shared .hero rule (used on the
   Welcome screen too) is sized for a page that's mostly empty below it;
   here there's a kid grid AND the Parent area row still to fit above
   the fold, so this trims the padding a plain override of .hero's own
   34px/22px was already shrinking (28/18) down further still. */
.who-hero{padding:10px 0 6px}
#whoTitle{font-family:var(--dis);font-size:22px;margin:0}
#whoSub{margin-top:4px}
/* "Who is playing?"'s badge above the heading — a real emoji in a soft
   circle, not hand-drawn art: emoji rendering is handled by the
   platform's own emoji font, so this is guaranteed to look considered
   rather than gambling on custom shapes nobody has actually seen. Sized
   down from an earlier 64px — it was costing more vertical space than a
   purely decorative badge above a heading actually needed, on a screen
   that's otherwise tight on room before the fold. */
.who-badge{display:flex;align-items:center;justify-content:center;width:44px;height:44px;
 margin:0 auto 6px;border-radius:50%;background:var(--shell);border:1px solid var(--line);
 font-size:22px;box-shadow:0 6px 16px rgba(0,0,0,.08);overflow:hidden}
/* A family's own uploaded photo (see todayGreetingOverlay-adjacent
   handleFamilyPhoto in app.js) fills the whole badge like a real
   profile picture, not sized-to-match-the-emoji the way avHtml's
   generic .av img rule does elsewhere — this badge isn't a .av. */
.who-badge img{width:100%;height:100%;object-fit:cover}
/* The parent-access row, redesigned from a plain full-width button into
   the same icon+title+subtitle+chevron shape a phone's own Settings app
   uses for "go somewhere else" rows — reads as a deliberate, different
   kind of action from the kid name buttons above it (whose whole job is
   "tap me to start playing"), not just another button in the same list. */
.parent-card{display:flex;align-items:center;gap:14px;width:100%;text-align:left;
 background:var(--shell);border:1px solid var(--line);border-radius:var(--r);
 padding:13px 16px;margin-top:14px}
.parent-card:active{transform:scale(.98)}
.parent-card-icon{flex:none;width:42px;height:42px;border-radius:50%;background:var(--sand2);
 display:flex;align-items:center;justify-content:center;font-size:20px}
.parent-card-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
.parent-card-text b{font-family:var(--dis);font-size:15.5px;color:var(--text)}
.parent-card-text span{font-size:12.5px;color:var(--ink3)}
.parent-card-arrow{flex:none;font-size:24px;color:var(--ink3)}

.packs{display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:13px}
.pack{border:1px solid var(--line);border-radius:var(--r);background:var(--shell);padding:16px;
 text-align:left;border-left:6px solid var(--c);min-height:118px;position:relative;cursor:pointer;display:block;width:100%}
.pack:active{transform:scale(.97)}
.pack .e{font-size:31px;line-height:1}
.pack .en{font-family:var(--dis);font-weight:700;font-size:16px;margin-top:5px}
.pack .n{font-family:var(--ge);font-size:16px;color:var(--ink2)}
.pack .p{font-size:12.5px;color:var(--ink3);margin-top:5px}
.pack .p b{color:var(--o1)}
/* pin toggle — a real <button> layered on the pack tile (which is now a
   div, not a button, precisely so this can be its own separate control) */
.pin-btn{position:absolute;top:8px;right:8px;width:28px;height:28px;border-radius:99px;
 background:rgba(255,255,255,.88);box-shadow:0 1px 3px rgba(0,0,0,.15);font-size:13px;
 display:flex;align-items:center;justify-content:center;opacity:.4}
.pin-btn.on{opacity:1;background:var(--o2)}
/* "NEW" flag on a word-pack or game tile (.pack itself is already
   position:relative — see above) — corner is a variant, not baked in,
   since a pack tile's pin toggle already owns top-right. */
.new-badge{position:absolute;top:8px;font-size:10px;font-weight:800;color:#fff;background:var(--o3);
 border-radius:99px;padding:2px 8px;letter-spacing:.03em;box-shadow:0 1px 3px rgba(0,0,0,.18)}
.new-badge.right{right:8px}
.new-badge.left{left:8px}
.bar-mini{height:5px;border-radius:3px;background:var(--line);margin-top:8px;overflow:hidden}
.bar-mini i{display:block;height:100%;background:var(--c)}

/* Letters page: mode-toggle buttons default to their full sentence-length
   label; the short one only shows once the mobile breakpoint below hides
   the long one. Two labels rather than JS measuring the viewport, so it
   stays correct across resizes/orientation changes for free. */
.lbl-short{display:none}
.fidel-orders{display:flex;gap:6px;overflow-x:auto;padding-bottom:10px}
.fidel-order-chip{flex:none;border-radius:9px;padding:6px 10px;color:#fff;min-width:62px;text-align:center}
.fidel-order-chip .ge{font-size:11px;opacity:.9;display:block}
.fidel-order-chip .rom{font-family:var(--dis);font-size:15px;font-weight:700}

.flash{border:1px solid var(--line);border-radius:var(--r);background:var(--shell);overflow:hidden}
.face{padding:34px 20px;text-align:center;background:var(--sand2);border-bottom:1px solid var(--line)}
.face .emoji{font-size:82px;line-height:1}
/* A few number-pack words (thousand, million, …) use a run of keycap
   digit emoji (1️⃣0️⃣0️⃣0️⃣0️⃣0️⃣0️⃣, not one single emoji) to actually show
   the number, which is a nice idea but is 7+ "characters" wide, not
   1 — at the normal 82px size that overflowed every real phone screen
   sideways. Long here means "this is a run of several glyphs, not a
   single emoji" (see the >6-char check in renderCard), not the .small
   used for word-length text elsewhere. */
.face .emoji.long{font-size:28px;line-height:1.4;word-break:break-all}
.face .ti{font-family:var(--ge);font-size:56px;margin:12px 0 4px;line-height:1.25}
.face .tr{font-family:var(--dis);font-size:22px;font-weight:700;color:var(--o3)}
.face .en{font-size:16px;color:var(--ink2)}
.voices{padding:14px 16px;border-bottom:1px solid var(--line)}
.vchips{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px}
.vchip{border:1px solid var(--line);background:var(--sand2);border-radius:99px;padding:8px 14px;
 font-size:13.5px;font-weight:600;min-height:40px}
.vchip[aria-pressed=true]{background:var(--ink);color:var(--on-ink);border-color:var(--ink)}
.credit{font-size:13.5px;color:var(--ink3)}.credit b{color:var(--ink2)}
.ctl{padding:16px;display:flex;gap:10px;flex-wrap:wrap;justify-content:center}
.dots{display:flex;gap:6px;justify-content:center;margin-top:14px;flex-wrap:wrap}
.dots i{width:8px;height:8px;border-radius:9px;background:var(--line)}
.dots i.on{background:var(--ink);width:22px}
.dots i.has{background:var(--o1)}
.flash-nav{margin-top:16px}

.wrow{display:grid;grid-template-columns:auto 1fr auto;gap:13px;align-items:center;background:var(--shell);
 border:1px solid var(--line);border-radius:13px;padding:12px 14px;margin-bottom:8px}
.wrow .g{font-family:var(--ge);font-size:24px}
.wrow .m{font-size:13px;color:var(--ink3)}
.wrow.has{border-left:5px solid var(--o1)}
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:11px;margin-bottom:16px}
.stat{background:var(--shell);border:1px solid var(--line);border-radius:var(--r);padding:15px;
 text-align:center;border-bottom:5px solid var(--c)}
.stat b{font-family:var(--dis);font-size:30px;display:block;line-height:1.1}
.stat span{font-size:11.5px;color:var(--ink3);text-transform:uppercase;letter-spacing:.05em}

.drop{border:2px dashed var(--line);border-radius:var(--r);background:var(--sand2);padding:30px 18px;
 text-align:center;color:var(--ink2);font-size:15px}
.drop.over{border-color:var(--o1);background:var(--ok-bg)}
.prev{display:grid;grid-template-columns:1fr auto auto;gap:10px;align-items:center;padding:11px 13px;
 border:1px solid var(--line);border-radius:12px;background:var(--shell);margin-bottom:8px}
.prev .fn{font-size:12px;color:var(--ink3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.prev .tw{font-family:var(--ge);font-size:20px}

.opts{display:grid;grid-template-columns:1fr 1fr;gap:13px}
.opt{background:var(--shell);border:3px solid var(--line);border-radius:var(--r);padding:26px 10px;
 font-family:var(--ge);font-size:36px;min-height:96px;overflow-wrap:break-word;word-break:break-word}
.opt.small{font-size:26px}
/* A genuinely long answer (a compound number phrase like the "current
   year" word — see todayGameRound — is a full sentence-length string,
   not a single short word) needed to actually shrink to fit instead of
   overflowing its own button; min-height above lets the button grow
   taller for wrapped text, but font-size doesn't shrink on its own. */
.opt.tiny{font-size:16px;line-height:1.3}
.opt.eng{font-family:var(--bd);font-size:20px;font-weight:600}
.opt:active{transform:scale(.96)}
.opt.right{background:var(--ok-bg);border-color:var(--ok);animation:optPop .4s ease}
@keyframes optPop{0%{transform:scale(1)}35%{transform:scale(1.07)}100%{transform:scale(1)}}
.opt.wrong{background:var(--bad-bg);border-color:var(--bad);animation:shake .32s}
/* A multi-select .opt (Kitchen Chef's pick-every-ingredient screen) used
   to toggle a "solid" class on tap that had no matching .opt rule at
   all — nothing visibly changed, so a family couldn't tell what they'd
   already tapped. .picked gives it a real selected look, distinct from
   .right/.wrong (those only ever appear after the round ends). */
.opt.picked{background:var(--o1);border-color:var(--o1);color:#fff}
@keyframes shake{25%{transform:translateX(-7px)}75%{transform:translateX(7px)}}
/* The per-answer version of celebrate()'s full-screen confetti (see
   sparkleBurst in app.js) — a handful of stars bursting from the actual
   button just tapped, contained and quick enough to run on every
   correct answer without the fatigue a full confetti burst would cause
   at that frequency. Full confetti stays reserved for a perfect run. */
.sparkle-piece{position:fixed;pointer-events:none;z-index:200;font-size:16px;
 transform:translate(-50%,-50%);animation:sparkleOut .6s ease-out forwards}
@keyframes sparkleOut{
  0%{transform:translate(-50%,-50%) scale(.4);opacity:1}
  100%{transform:translate(calc(-50% + var(--dx)),calc(-50% + var(--dy))) scale(1.15);opacity:0}
}

/* Water the Baobab (baobabStart/baobabRound in app.js). The jug's fill
   height and the tree's #bbCanopy/#bbFlowers opacity are set inline per
   round/update in app.js, not here — this only owns the shapes and the
   transitions/animations those inline values then play against. */
.bb-jug{width:42px;height:120px;border:3px solid var(--line);border-radius:10px 10px 22px 22px;
 background:var(--sand2);position:relative;overflow:hidden;flex:none}
.bb-jug-fill{position:absolute;left:0;right:0;bottom:0;height:0%;
 background:linear-gradient(180deg,#7BD1EE,#2E86C1);transition:height .6s cubic-bezier(.34,1.3,.64,1)}
.bb-timerTrack{height:6px;border-radius:3px;background:var(--sand2);overflow:hidden;margin:8px 0}
.bb-timerBar{height:100%;width:100%;background:var(--o4);transform-origin:left;
 animation-name:bbTimerShrink;animation-timing-function:linear;animation-fill-mode:forwards}
@keyframes bbTimerShrink{from{transform:scaleX(1)}to{transform:scaleX(0)}}
/* The full-bloom payoff's falling seeds (baobabBloom) — same small
   fixed-position-piece technique as .confetti-piece/.sparkle-piece, just
   scoped inside #bbScene (position:relative) instead of the fixed
   viewport, since these fall FROM the tree, not from off top-of-screen. */
.bb-seed{position:absolute;bottom:40px;font-size:20px;pointer-events:none;
 animation:bbSeedFall 1.6s ease-in forwards}
@keyframes bbSeedFall{0%{transform:translateY(0) rotate(0deg);opacity:1}100%{transform:translateY(90px) rotate(160deg);opacity:0}}

/* Spin & Find's prize wheel (wheelStart/spinWheel in app.js). The wheel
   is two layered conic-gradients computed inline per round (segment
   count varies with how many words are available) — thin divider lines
   over the colour wedges — no canvas, no SVG path data. Each label is
   placed with the classic clock-hand trick (rotate to the segment's
   angle, translate out to its radius, rotate back to stay upright) and
   rides along with #wheelEl's own spin transform, so the labels visibly
   spin with their segments exactly like a real prize wheel — including
   landing tilted mid-spin, which is the point, not a bug. The SPIN
   button sits in the wheel's own hub, like an arcade cabinet's, rather
   than as a plain button underneath it. */
.wheelWrap{position:relative;width:240px;height:240px;margin:26px auto 6px}
.wheelPointer{position:absolute;top:-4px;left:50%;transform:translateX(-50%);
 width:0;height:0;border-left:15px solid transparent;border-right:15px solid transparent;
 border-top:24px solid var(--ink);z-index:3;filter:drop-shadow(0 2px 3px rgba(0,0,0,.35))}
.wheel{position:relative;width:240px;height:240px;border-radius:50%;
 border:7px solid var(--shell);box-shadow:0 10px 26px rgba(0,0,0,.3),inset 0 0 0 2px rgba(0,0,0,.07)}
.wheel::before{content:'';position:absolute;inset:0;border-radius:50%;pointer-events:none;
 background:radial-gradient(circle at 32% 26%,rgba(255,255,255,.4),transparent 55%)}
.wheelLabel{position:absolute;top:50%;left:50%;width:0;height:0;
 display:flex;align-items:center;justify-content:center;font-size:27px;
 filter:drop-shadow(0 1px 2px rgba(0,0,0,.3))}
/* The hub doubles as the SPIN button — centred on the wheel via
   top/left/transform rather than flex, since it's a sibling of .wheel
   inside .wheelWrap, not a child of it (staying outside .wheel means it
   never inherits the wheel's own spin transform). */
.wheelHub{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
 width:68px;height:68px;border-radius:50%;background:var(--ink);color:var(--on-ink);
 font-family:var(--dis);font-weight:700;font-size:15px;letter-spacing:.05em;
 border:4px solid var(--shell);box-shadow:0 5px 14px rgba(0,0,0,.4);z-index:2;
 display:flex;align-items:center;justify-content:center;cursor:pointer}
.wheelHub:active{transform:translate(-50%,-50%) scale(.92)}
.wheelHub:disabled{opacity:.55;cursor:default}
.prompt{border-radius:var(--r);padding:32px 20px;text-align:center;color:#fff;background:var(--c);margin-bottom:15px}
.prompt .lbl{font-size:12.5px;text-transform:uppercase;letter-spacing:.11em;opacity:.85}
/* A plain .btn (e.g. "Play again" inside the Who said what prompt) has
   a light sand background but no color of its own, so it was inheriting
   .prompt's white text — white on light sand, unreadable. Buttons
   nested in a colour prompt need their own readable colour back;
   .solid/.rec already set their own colour and are unaffected. */
.prompt .btn:not(.solid):not(.rec){color:var(--ink)}
/* Same problem as .btn just above, one line down: .sub has its own
   fixed dark colour (meant for the plain sand background almost every
   other use of it sits on), which silently overrides the white text a
   colour prompt otherwise inherits — dark ink on a dark accent colour,
   unreadable. Only inside .prompt does .sub need this override. */
.prompt .sub{color:#fff;opacity:.85}
.prompt .q{font-family:var(--dis);font-size:48px;font-weight:700;line-height:1.1}
.prompt .qe{font-size:76px;line-height:1.1}
.prompt .qg{font-family:var(--ge);font-size:52px;line-height:1.3}

/* memory match */
.mem{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
.mcard{aspect-ratio:1;border-radius:14px;background:var(--ink);color:var(--on-ink);display:flex;
 align-items:center;justify-content:center;font-size:30px;border:2px solid var(--ink);min-height:70px}
.mcard.up{background:var(--shell);color:var(--text);border-color:var(--line)}
.mcard.ge{font-family:var(--ge);font-size:24px}
.mcard.done{background:var(--ok-bg);border-color:var(--ok);opacity:.55}

/* spell it */
.slots{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin:16px 0}
.slot{width:56px;height:64px;border-radius:12px;border:2px dashed var(--line);background:var(--sand2);
 display:flex;align-items:center;justify-content:center;font-family:var(--ge);font-size:28px}
.slot.full{border-style:solid;border-color:var(--o4);background:var(--shell)}
/* Take Turns' currently-live slot — whoever's turn it is, the blank
   they're about to fill gets a pulsing highlight so it's obvious which
   one's live, not just what's already been filled in. */
.slot.active{border-color:var(--o3);animation:slotPulse 1s ease-in-out infinite}
@keyframes slotPulse{50%{box-shadow:0 0 0 4px rgba(196,54,42,.18)}}
.tiles{display:flex;gap:9px;justify-content:center;flex-wrap:wrap}
.tile{width:60px;height:66px;border-radius:12px;border:2px solid var(--line);background:var(--shell);
 font-family:var(--ge);font-size:28px}
.tile.used{opacity:.28;pointer-events:none}
/* .slot/.tile above are sized for exactly one glyph or digit (Put them
   in order's fidel forms, Sort the Numbers) — a fixed box just wide
   enough for one character. Sentence Builder's and Chat Buddy's
   Advanced tiers reuse the same tap-into-place mechanic for whole
   Tigrinya WORDS instead, which don't fit that box — this variant
   grows with its content (min-width/min-height, not a fixed size)
   instead of clipping or overflowing it. */
.slot.wide{width:auto;min-width:68px;height:auto;min-height:52px;padding:8px 12px;font-size:20px}
.tile.wide{width:auto;min-width:68px;height:auto;min-height:52px;padding:8px 12px;font-size:20px;white-space:nowrap}

/* build the word — each piece/space is an SVG silhouette (see
   PUZZLE_SHAPES) with the letter as a plain overlay; the shape itself,
   not just the letter, is what a child matches up. */
.puz-slots,.puz-tiles{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin:14px 0}
.puz-slot,.puz-tile{position:relative;width:64px;height:64px;padding:0;border:none;background:none;flex:none}
.puz-slot svg,.puz-tile svg{width:100%;height:100%;overflow:visible}
.puz-slot svg{fill:var(--sand2);stroke:var(--line);stroke-width:2;stroke-dasharray:4 3}
.puz-slot.filled svg{fill:var(--shell);stroke:var(--o4);stroke-width:2.5;stroke-dasharray:none}
.puz-slot.wrong svg{stroke:var(--bad);animation:puzShake .5s ease}
@keyframes puzShake{20%{transform:translateX(-4px)}40%{transform:translateX(4px)}60%{transform:translateX(-3px)}80%{transform:translateX(3px)}}
.puz-tile svg{fill:var(--shell);stroke:var(--o1);stroke-width:2.5}
.puz-tile.sel svg{fill:var(--o1);stroke:var(--o1)}
.puz-tile.sel .puz-ch{color:#fff}
.puz-ch{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
 font-family:var(--ge);font-size:24px;pointer-events:none;color:var(--text)}

/* The Fidel Silo (buildwordRender in app.js) — a block for each letter
   correctly placed this word, stacked bottom-up (column-reverse: the
   first block placed renders first in the DOM, which column-reverse
   then puts at the BOTTOM, with every later block piling visually on
   top of it). Only the block just added carries .new — see
   G.siloAnimateIdx in app.js for why every other render leaves it off,
   otherwise every re-render (just selecting a different tile, say)
   would replay the whole stack's drop-in. */
.silo-tube{width:52px;min-height:150px;border:3px solid var(--line);border-radius:8px 8px 14px 14px;
 background:var(--sand2);display:flex;flex-direction:column-reverse;align-items:center;
 padding:5px;gap:3px;flex:none}
.silo-block{width:38px;height:26px;border-radius:6px;background:var(--o1);color:#fff;
 display:flex;align-items:center;justify-content:center;font-family:var(--ge);font-size:15px;
 font-weight:700;flex:none;box-shadow:0 1px 3px rgba(0,0,0,.25)}
.silo-block.new{animation:siloDrop .5s cubic-bezier(.34,1.56,.64,1) both}
@keyframes siloDrop{
  0%{transform:translateY(-130px) scale(.8);opacity:0}
  70%{transform:translateY(6px) scale(1.05);opacity:1}
  100%{transform:translateY(0) scale(1);opacity:1}
}

/* Load the Caravan (Drag & match's reward layer — caravanLoad in
   app.js). The tile-drag mechanic below is unchanged; this just sits
   above it. Every correct match loads one bag onto the camel — no
   .new-class re-render trick needed here, caravanLoad appends the bag
   straight into the already-on-screen track same as every other match
   in this game already mutates the DOM directly. On the last bag
   .walk is added once and the camel's own transition carries it off. */
.caravan-wrap{margin:0 0 4px;overflow:hidden}
.caravan-track{position:relative;height:60px;border-radius:14px;
 background:linear-gradient(180deg,color-mix(in srgb,var(--o6) 14%,var(--sand2)) 0%,var(--sand2) 100%);
 border:1px solid var(--line);overflow:hidden}
.caravan-animal{position:absolute;left:10px;bottom:6px;font-size:32px;line-height:1;
 transition:transform 1s ease-in}
.caravan-animal.walk{transform:translateX(280px)}
.caravan-bags{position:absolute;top:-13px;left:2px;display:flex;gap:1px}
.caravan-bag{font-size:13px;display:inline-block}
.caravan-bag.new{animation:bagPop .3s ease-out both}
@keyframes bagPop{from{transform:scale(0) translateY(6px);opacity:0}to{transform:scale(1) translateY(0);opacity:1}}

.timer{height:8px;border-radius:5px;background:var(--line);overflow:hidden;margin-bottom:14px}
.timer i{display:block;height:100%;background:var(--o3);transition:width .95s linear}

.empty{text-align:center;padding:40px 18px;color:var(--ink3);font-size:15px}
.empty .e{font-size:42px;display:block;margin-bottom:9px}
.toast{position:fixed;left:50%;bottom:calc(120px + var(--sb));transform:translateX(-50%) translateY(20px);
 z-index:90;background:var(--ink);color:var(--on-ink);padding:13px 20px;border-radius:99px;font-size:15px;
 font-weight:600;opacity:0;pointer-events:none;transition:.25s;max-width:88vw;text-align:center}
.toast.on{opacity:1;transform:translateX(-50%)}
/* A tab open across a deploy is still running the old JS — see
   watchForAppUpdate in app.js. Fixed to the bottom, above everything
   including an open sheet (z-index higher than .sheet's 80), and never
   auto-dismisses on its own — a stale tab should stay flagged until
   someone actually taps Update, not quietly disappear after a few
   seconds like a toast would. */
.update-banner{position:fixed;left:0;right:0;bottom:0;z-index:95;background:var(--ink);color:var(--on-ink);
 padding:12px 16px calc(12px + var(--sb));display:flex;align-items:center;justify-content:center;gap:12px;
 flex-wrap:wrap;font-size:14px;font-weight:600;box-shadow:0 -4px 14px rgba(0,0,0,.2)}
.sheet{position:fixed;inset:0;z-index:80;background:rgba(36,22,17,.45);display:none;align-items:flex-end}
.sheet.on{display:flex}
.sheetbox{background:var(--shell);width:100%;max-width:1100px;margin:0 auto;border-radius:22px 22px 0 0;
 padding:20px 20px calc(24px + var(--sb));max-height:88vh;overflow:auto}
.sheet h3{margin-top:0}
/* Printing/"Save as PDF" a report (openPrintReport in app.js) — the
   browser's own print dialog otherwise renders the whole page as it
   sits on screen: the app header behind the modal, the dark backdrop,
   the sheet clipped to 88vh. This unwraps just the sheet's content to
   print full-length on a plain page, and .no-print (the Print/Close
   buttons themselves, no reason to print a button) disappears. No
   PDF library needed — every phone and desktop browser's print dialog
   already offers "Save as PDF" as one of the printers to pick from. */
@media print {
  body > *:not(.sheet) { display: none !important; }
  .sheet { position: static !important; background: none !important; display: block !important; }
  .sheetbox { max-height: none !important; overflow: visible !important; box-shadow: none !important;
    border-radius: 0 !important; max-width: 100% !important; padding: 0 !important; }
  .no-print { display: none !important; }
}
/* Family Roots' tabs (renderFR in app.js) — same idea as EDIT_TABS, just
   scoped to one sheet instead of the whole Edit view. .frSaveBar is what
   actually fixes "Save was all the way at the bottom": sticky to the
   bottom of .sheetbox's own scroll area (not the page), with negative
   margins cancelling .sheetbox's own padding so it reads as a proper
   full-width action bar, not just a button sitting in the flow — visible
   no matter which tab you're on or how far down you've scrolled. */
.frTabPane{margin-top:4px}
.frSaveBar{position:sticky;bottom:0;z-index:5;display:flex;gap:10px;
 margin:18px -20px calc(-24px - var(--sb));padding:12px 20px calc(12px + var(--sb));
 background:var(--shell);border-top:1px solid var(--line);box-shadow:0 -6px 16px rgba(0,0,0,.08)}
/* Catch it! (app.js, catchRound) — a small "sky" the candidates fall
   through. Fixed height with overflow hidden so a button animating to
   top:100% visually lands right at the bottom edge, not off the page.
   Reduced-motion families never see this block at all (catchRound
   renders a plain .opts row instead) — see prefersReducedMotion. */
.catchSky{position:relative;height:220px;border-radius:16px;border:1px solid var(--line);
 background:linear-gradient(180deg,color-mix(in srgb,var(--o7) 12%,var(--sand2)) 0%,var(--sand2) 100%);
 overflow:hidden;margin-top:4px}
.catchDrop{position:absolute;top:-15%;transform:translateX(-50%);
 background:var(--shell);border:1px solid var(--line);border-radius:12px;padding:6px 9px;
 font-family:var(--ge);font-size:16px;font-weight:600;box-shadow:0 3px 8px rgba(36,22,17,.12);
 white-space:nowrap;animation-name:catchFall;animation-timing-function:linear;animation-fill-mode:forwards}
/* A real Tigrinya word (unlike a single fidel letter) can be several
   glyphs wide — shrinking it here is what gives layoutCatchLanes (see
   app.js) enough room to actually fit everyone without overlap. */
.catchDrop.long{font-size:13px;padding:5px 7px}
@keyframes catchFall{from{top:-15%}to{top:100%}}

/* Fill the Mesob (Category Drop's reward layer — catDropRound/mesobGrow
   in app.js). The falling item and the bins above stay exactly as they
   were; this basket just sits underneath them. Each correct sort adds
   one woven ring (capacity = G.total, so the basket only completes on a
   flawless run) — the ring is mutated straight into the live DOM during
   the pause before the next round, same technique as Water the Baobab's
   jug fill, so a fresh CSS animation plays on it regardless of the next
   full re-render. The lid only ever drops on the final round of a
   perfect run — see the comment above catDropTimer in app.js. */
.mesob-wrap{width:118px;margin:6px auto 0;position:relative}
.mesob-basket{height:96px;border:3px solid var(--o3);border-top:none;border-radius:0 0 40px 40px;
 background:var(--sand2);overflow:hidden;display:flex;flex-direction:column-reverse;
 align-items:stretch;padding:4px 6px;gap:2px}
.mesob-ring{height:7px;border-radius:4px;flex:none}
.mesob-ring:nth-child(odd){background:var(--o1)}
.mesob-ring:nth-child(even){background:var(--o2)}
.mesob-ring.new{animation:mesobWeave .4s ease-out both}
@keyframes mesobWeave{from{transform:scaleX(0);opacity:0}to{transform:scaleX(1);opacity:1}}
.mesob-lid{height:16px;margin:0 -3px;border-radius:50%;background:var(--o3);
 box-shadow:0 2px 4px rgba(0,0,0,.2);opacity:0;transform:translateY(-26px);
 transition:transform .5s cubic-bezier(.34,1.3,.64,1),opacity .3s}
.mesob-lid.closed{opacity:1;transform:translateY(4px)}
.pgrid{display:grid;grid-template-columns:repeat(7,1fr);gap:5px;margin-bottom:10px}
.pgrid button{font-family:var(--ge);font-size:23px;padding:11px 0;border-radius:10px;background:var(--sand2);min-height:50px}
.prows{display:flex;flex-wrap:wrap;gap:6px;padding:5px 0 11px}
.pin-pad{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin:8px auto 0;max-width:190px}
.pin-pad button{aspect-ratio:1;border-radius:99px;background:var(--sand2);border:1px solid var(--line);
 font-family:var(--dis);font-size:17px;font-weight:700;color:var(--text);transition:transform .1s ease,background .12s ease}
.pin-pad button:active{background:var(--o4);color:#fff;transform:scale(.93)}
.prows button{flex:none;font-family:var(--ge);font-size:22px;width:50px;height:50px;border-radius:11px;background:var(--sand2)}
.prows button[aria-pressed=true]{background:var(--ink);color:var(--on-ink)}
.avpick{display:grid;grid-template-columns:repeat(8,1fr);gap:7px}
.avpick button{font-size:24px;padding:9px 0;border-radius:11px;background:var(--sand2);min-height:46px}
.avpick button[aria-pressed=true]{background:var(--ink)}
.cpick{display:flex;gap:8px;flex-wrap:wrap}
.cpick button{width:42px;height:42px;border-radius:11px;background:var(--c);border:3px solid transparent}
.cpick button[aria-pressed=true]{border-color:var(--ink)}
/* Appearance's Light/Dark/Match device buttons (renderThemeButtons in
   app.js) — a literal miniature of what each choice actually looks like
   (a little sand-and-ink card), not just a label, since a change made
   from inside this small sheet — sitting over a dimming overlay, on top
   of the real page — is otherwise easy to miss at a glance. The choice
   still applies live to the whole app the instant it's tapped (see
   setTheme) — this is a preview that's always visible, not a
   replacement for that; "Match device" gets a half-and-half swatch
   since it isn't really "showing" one fixed look. */
.theme-opt{display:flex;flex-direction:column;align-items:center;gap:7px;
 background:var(--sand2);border:2px solid var(--line);border-radius:14px;
 padding:10px 12px;font-size:12px;font-weight:600;color:var(--text);min-width:82px}
.theme-opt[aria-pressed=true]{border-color:var(--accent)}
.theme-swatch{display:block;width:52px;height:34px;border-radius:8px;overflow:hidden;
 position:relative;border:1px solid rgba(0,0,0,.15)}
.theme-swatch i{display:block;position:absolute;left:6px;top:6px;width:60%;height:6px;border-radius:3px;font-style:normal}
.repcard{background:var(--shell);border:1px solid var(--line);border-left:6px solid var(--c);
 border-radius:14px;padding:14px;margin-bottom:9px;display:flex;flex-wrap:wrap;gap:13px;align-items:center}
.repcard .av{font-size:32px}
.repcard .n{font-family:var(--dis);font-weight:700;font-size:16px}
.repcard .d{font-size:13px;color:var(--ink3)}

/* Family Roots tree preview (buildFamilyTree in app.js) — a static,
   recomputed-every-render generational diagram, not a draggable canvas:
   great-grandparents (if any), grandparents, parents — linked by a
   marriage line — then the child + siblings, plus a collapsed strip for
   aunts/uncles/cousins underneath. No stored x/y per person, so it can't
   drift or overlap differently at a different screen width — it just
   re-flows like any other flex row. Shared by two contexts: editable
   (Parent area, empty nodes are tap-to-jump) and read-only (the My
   Family Roots game's intro, empty nodes are just a quiet "—"). */
.frTree{background:var(--sand2);border:1px solid var(--line);border-radius:var(--r);
 padding:18px 12px;margin-bottom:16px}
.frRow{display:flex;justify-content:center;gap:22px;flex-wrap:wrap}
/* The grandparent row and (when present) the great-grandparent row are
   always meant to read as father's-side-left, mother's-side-right — the
   whole point of grouping them that way. Plain .frRow's flex-wrap:wrap
   let them stack top/bottom instead once the row got too narrow to fit
   both side by side, which is exactly what confused kids on small
   screens: "left/right" turned into "top/bottom" with no warning. The
   narrow-screen rule below shrinks nodes enough that the (normal, two-
   cluster) case never needs to wrap OR scroll; overflow-x:auto is only
   a safety net for the rarer three-cluster great-grandparent case
   (father's side + mother's side + unspecified) on the narrowest phones. */
.frRow-sides{flex-wrap:nowrap;overflow-x:auto}
.frSide{display:flex;flex-direction:column;align-items:center;gap:6px;flex:none}
.frSideLbl{font-size:10.5px;color:var(--ink3);text-transform:uppercase;letter-spacing:.06em}
.frCluster{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;max-width:160px}
.frWire{width:2px;height:16px;background:var(--line);margin:0 auto}
/* Any couple on the tree — parents, and now grandparents too — linked by
   a short gold bar at avatar-centre height (22px ≈ half the 44px avatar)
   rather than the plain grey generational wires — a small deliberate
   distinction for "these two are married", not just "one generation
   flows into the next". */
.frCouple{display:flex;align-items:flex-start;justify-content:center;gap:0}
.frCoupleLine{width:26px;height:3px;border-radius:3px;background:var(--o2);margin-top:21px;flex:none}
.frNode{display:flex;flex-direction:column;align-items:center;gap:3px;width:64px;cursor:default}
.frNodeAv{position:relative;width:44px;height:44px;border-radius:50%;background:var(--shell);border:2px solid var(--line);
 display:flex;align-items:center;justify-content:center;font-size:21px;overflow:hidden}
.frNodeAv img{width:100%;height:100%;object-fit:cover}
/* A small badge marking "this face has a recorded name" before you even
   tap it — not just a bare cursor:pointer, which isn't a visible cue on
   a touchscreen the way it is with a mouse. */
.frNodeSnd{position:absolute;right:-3px;bottom:-3px;width:17px;height:17px;border-radius:50%;
 background:var(--o1);display:flex;align-items:center;justify-content:center;font-size:10px;
 font-style:normal;box-shadow:0 0 0 2px var(--sand2)}
.frNodeName{font-size:11px;font-weight:600;color:var(--ink2);text-align:center;max-width:64px;
 overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.frNodeSub{font-size:9.5px;color:var(--ink3);text-align:center;max-width:64px;
 overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.frNode-empty .frNodeAv{border-style:dashed;color:var(--ink3);background:transparent}
.frNode-empty .frNodeName{color:var(--ink3);font-weight:400}
/* Only the editable tree's empty nodes and any node with a recording are
   actually clickable (see buildFamilyTree) — cursor:pointer lives on
   those inline styles, not here, so a plain unrecorded node doesn't
   look tappable when it's not. */
.frNode-child .frNodeAv{width:56px;height:56px;font-size:27px;border-color:var(--o1);border-width:3px}
.frNode-child .frNodeName{font-size:12.5px;font-weight:700;color:var(--text)}
/* Photo picker row inside each relation/sibling/extended/ancestor card
   in the editable form (frMemberExtras in app.js) — separate from the
   tree's own .frNodeAv, this one's just a plain circular thumbnail next
   to an "Add/Change photo" button, no tap-to-hear affordance (that only
   applies out on the tree itself). */
.frPhotoRow{display:flex;align-items:center;gap:8px;margin-top:10px;flex-wrap:wrap}
.frPhotoThumb{width:36px;height:36px;border-radius:50%;overflow:hidden;background:var(--sand2);
 border:1px solid var(--line);display:flex;align-items:center;justify-content:center;font-size:17px;flex:none}
.frPhotoThumb img{width:100%;height:100%;object-fit:cover}
.frExtStrip{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-top:16px;
 padding-top:14px;border-top:1px dashed var(--line)}
.frExtChip{background:var(--shell);border:1px solid var(--line);border-radius:99px;
 padding:6px 12px;font-size:12.5px;color:var(--ink2)}
@media(max-width:560px){.two,.opts,.stats{grid-template-columns:1fr}
 .stats{grid-template-columns:repeat(2,1fr)}
 h2{font-size:23px}
 .mem{grid-template-columns:repeat(3,1fr)}
 /* .prompt is the question card shared by every quiz-style game (Point
    to it, Find the letter, word games, …). Its desktop-sized padding
    and 52px Ge'ez font were tall enough on a phone to push the actual
    answer buttons below the fold — the game "worked" but you had to
    scroll down to find anything to tap. Shrinking it here keeps the
    question fully readable while leaving real room for the answers. */
 .prompt{padding:20px 14px;margin-bottom:10px}
 .prompt .q{font-size:36px}
 .prompt .qe{font-size:56px}
 .prompt .qg{font-size:38px}
 /* Letters page: all 7 vowel-order chips need to read as one row, not
    two — a wrapped 6+1 (tried first) looked like a mistake, not a
    design. Sized down enough that even the widest chip's own content
    (the "ie" vowel needs more room than a single-letter one) no longer
    pushes the row past a real narrow phone's width — verified at
    320/360/375/390px, one line at all four. overflow-x:auto stays on
    only as a safety net for anything narrower than that, not the
    normal case anymore. */
 .fidel-orders{gap:2px;padding-bottom:6px}
 .fidel-order-chip{padding:4px 2px;min-width:38px;border-radius:6px}
 .fidel-order-chip .ge{font-size:8.5px}
 .fidel-order-chip .rom{font-size:12px}
 /* The word flashcard (Words tab → open a pack). Its desktop sizing —
    an 82px emoji, 56px Ge'ez text, generous padding everywhere, two
    full-size .btn.big actions that don't fit side by side so they
    stack — added up to taller than a real phone screen: the Back/Next
    row at the very bottom needed a scroll to even reach. Shrunk
    everything here to what's actually needed to stay readable and
    tappable, not what desktop's extra width afforded. */
 .face{padding:16px 16px}
 .face .emoji{font-size:48px}
 .face .ti{font-size:32px;margin:6px 0 3px}
 .face .tr{font-size:17px}
 .face .en{font-size:14px}
 .voices{padding:8px 14px}
 .ctl{padding:10px;gap:8px}
 .ctl .btn.big{min-height:46px;padding:11px 18px;font-size:15.5px}
 .dots{margin-top:8px}
 .flash-nav{margin-top:10px}
 .flash-nav .btn.big{min-height:46px;padding:11px 18px;font-size:15.5px}
 /* .packs/.pack is the shared tile grid for word packs, games, stories
    and videos. Its 168px minimum meant most phones (anything under
    ~356px of content width, i.e. nearly all of them once .mid's own
    padding is subtracted) could only ever fit ONE column — every tile
    stretched to the full screen width, tall and oversized, with lots of
    scrolling to see the rest. Shrinking the minimum reliably fits two
    per row on real phone widths instead. The description line is
    clamped to two lines so a long game description (Games tab) can't
    still blow a tile back out tall even at the smaller width. */
 .packs{grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:9px}
 .pack{padding:12px 10px}
 .pack .e{font-size:25px}
 .pack .en,.pack .n{font-size:13.5px;margin-top:3px}
 .pack .p{font-size:11px;margin-top:4px;display:-webkit-box;-webkit-line-clamp:2;
  -webkit-box-orient:vertical;overflow:hidden}
 /* .opts/.opt is the answer-choice grid used by most quiz games. It
    already drops to one column per row below this width (see the
    .opts rule at the very top of this block) — sized generously
    because on desktop each option still has a whole row's width to
    itself. On a real phone that generosity (96px tall, 36px Ge'ez
    text) meant 3-4 answer options plus the question above rarely all
    fit without scrolling past the fold. Shrunk here to what a single
    full-width row actually needs, not what a half-width one did.
    Shrunk again — a family found even the first pass still meant
    scrolling to see every option, especially now that Family quiz
    mode's player picker and Word cross's tile tray sit above these on
    the same screen. */
 .opts{gap:6px}
 .opt{padding:9px 8px;min-height:44px;font-size:19px}
 .opt.small{font-size:15px}
 .opt.eng{font-size:13px}}

/* Shrinks the tree's nodes just enough that the grandparent row (and the
   great-grandparent row, when present) fits both sides without needing
   .frRow-sides' overflow-x:auto scroll at all — verified against a
   320px-wide viewport, the narrowest realistic phone. Below 560px
   because that's this file's one existing "small screen" threshold, not
   because the tree needs the room right up to it. */
@media(max-width:560px){
 .frTree{padding:14px 8px}
 .frRow-sides{gap:12px}
 .frNode{width:46px}
 .frNodeAv{width:34px;height:34px;font-size:16px}
 .frCoupleLine{width:16px;margin-top:16px}
 .frNodeName,.frNodeSub{max-width:46px}
 .frNode-child .frNodeAv{width:44px;height:44px;font-size:21px}
}
