:root {
  /* Properly black, because he asked for it and because this is used in a dark bedroom with a
     three-year-old going to sleep in it — on an OLED phone a true black page emits nothing at
     all where it is black.
     The rest of the ramp came down with it. Against #000 the old panels read as pale blue
     rectangles floating on the page rather than as part of it, and the blue tint that looked
     like depth at #0f1115 looks like a mistake at #000. Neutral greys, lifted just enough to
     separate a card from the page. */
  --bg: #000000;
  --panel: #101114;
  --panel-2: #191b20;
  --line: #24262c;
  --text: #e9ecf1;
  --dim: #98a1b3;
  --accent: #e8b04b;
  --accent-soft: #3a2f16;
  --good: #6cc48f;
  --bad: #e2725b;
  /* tone colours, used on pinyin and characters alike */
  --t1: #64b5f6;  /* high flat  */
  --t2: #81c784;  /* rising     */
  --t3: #ffb74d;  /* dipping    */
  --t4: #e57373;  /* falling    */
  --t5: #90a4ae;  /* neutral    */
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
/* Nothing on a phone should ever scroll sideways. This is a backstop, not a fix: an overflow
   is still a bug and still gets found, but it will not hand someone a page they can drag off
   the side of the screen while we work out which rule did it. */
html { overflow-x: hidden; }
html, body { margin: 0; height: 100%; }
body {
  background: var(--bg); color: var(--text);
  /* Light text on a dark page renders bolder and softer than it should under macOS and iOS
     subpixel antialiasing — strokes bloom outward and thin type looks out of focus. Greyscale
     antialiasing is the fix, and it is why this only started mattering when the front door
     stopped being cream. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}
.hidden { display: none !important; }
[hidden] { display: none !important; }
/* dvh, not vh: on a phone vh counts the browser chrome that is not actually there, so the
   column is taller than the screen and everything centred inside it sits low. */
.screen { min-height: 100dvh; display: flex; flex-direction: column; }
.center { align-items: center; justify-content: center; gap: 12px; padding: 24px; }

/* The wordmark was two Chinese characters, which wanted wide letter-spacing; a latin name
   wants very little, or it reads as a sign rather than a name. */
.brand { font-size: 40px; margin: 0; letter-spacing: 0.5px; font-weight: 600; }
/* In the app's own header, where it sits beside the nav. Smaller than it was, because the
   name is three words now and the row has to hold both. */
.brand.small { font-size: 15px; letter-spacing: 0.2px; font-weight: 600; white-space: nowrap; }
/* One more nav item than there used to be. On a phone the wordmark must not wrap to fit it. */
/* Six tabs do not fit across a phone, and the ones that fall off the end are invisible rather
   than merely cramped. The nav drops to its own full-width row instead. */
/* Everything in here is the APP's header, and every selector is scoped to it.
   They were bare `header` and `nav`, which also matched the landing page's own header and
   nav — including the fade on the right, whose whole job is to say the app's tab row scrolls.
   On the front door the rightmost item is Start, so Start was being faded out. On a phone it
   looked blurred, because it was. Two earlier attempts fixed real but different faults and
   left this one in place. */
@media (max-width: 560px) {
  #app header { flex-wrap: wrap; padding: 10px 12px 6px; gap: 6px; }
  #app .brand.small { font-size: 18px; letter-spacing: 1px; }
  #app .fb-open { padding: 4px 8px; margin-left: auto; }
  /* Still tight at six. Let it scroll rather than silently hiding the last tab, and drop the
     scrollbar so it does not look broken. */
  /* Full-bleed, so the row gets the whole screen rather than the header's gutters, and it still
     scrolls if a future tab pushes it over. The fade on the right says so. */
  #app nav {
    order: 3; gap: 0; padding-top: 2px;
    overflow-x: auto; scrollbar-width: none;
    flex: 1 0 100%; justify-content: space-between;
    -webkit-mask-image: linear-gradient(to right, #000 92%, transparent);
            mask-image: linear-gradient(to right, #000 92%, transparent);
  }
  #app nav::-webkit-scrollbar { display: none; }
  #app .tab { padding: 8px 4px; font-size: 12px; flex: 0 0 auto; }

  /* The front door's own row, and no mask anywhere near it. All four together and centred
     under the wordmark: pushing Start to the far right with margin-left:auto left the three
     links huddled on one side and the button stranded on the other, which reads as two things
     that failed to meet rather than one row. */
  /* The wrap has to be stated here. It used to come from a bare `header` rule in this block,
     written for the app; scoping that to #app took it away from the front door, and a nav
     that is 100% wide and refuses to shrink does not wrap without it — it overflows, and the
     page scrolls sideways by exactly the width it overflows by.
     `1 1 100%` rather than `1 0 100%` for the same reason: if anything ever stops it wrapping
     again, it shrinks instead of pushing the page open. */
  .land-top { flex-wrap: wrap; padding-top: 12px; padding-bottom: 10px; }
  .land-nav { flex: 1 1 100%; justify-content: center; gap: 14px; padding-top: 4px; }
}
.tagline { color: var(--dim); margin: 0 0 16px; font-size: 14px; }

input, button { font: inherit; }
input {
  background: var(--panel); border: 1px solid var(--line); color: var(--text);
  padding: 14px 16px; border-radius: 12px; width: 240px; text-align: center;
}
button {
  background: var(--panel-2); color: var(--text); border: 1px solid var(--line);
  padding: 12px 18px; border-radius: 12px; cursor: pointer;
}
button:active { transform: translateY(1px); }
.primary { background: var(--accent); color: #1a1408; border: none; font-weight: 600; }
.big { width: 100%; padding: 20px; font-size: 18px; border-radius: 16px; }
.ghost { background: transparent; color: var(--dim); }
.error { color: var(--bad); min-height: 20px; font-size: 14px; }
.status { color: var(--dim); font-size: 14px; text-align: center; min-height: 22px; }
.hint { color: var(--dim); font-size: 13px; text-align: center; margin: 8px 0 20px; }

/* ---------- the front door's palette ----------
   Named separately from the app's own tokens, because the two are opposites: the app is pure
   black for a dark bedroom, the landing page is read in daylight. .land maps these onto the
   ordinary names below, so every .land-* rule follows whichever is set.
 */
:root {
  /* The front door is dark, like the app behind it. It was cream on the argument that people
     decide in daylight — but it meant signing in threw you from cream to black, and the gold
     had to be muddied to #8a5f0a to stay legible on pale. On black the brand gold can be the
     actual brand gold. Not pure #000 here: the app is black because it is read in a dark
     bedroom, whereas a page being skimmed in daylight wants a little warmth under it. */
  --land-bg: #0b0a09;         /* near-black, warmed a shade so it is not a void */
  --land-glow: #241a0c;       /* the gold, almost gone, pooled behind the hero */
  --land-panel: #15130f;
  --land-line: #2b2621;
  --land-text: #f2ece2;
  --land-dim: #a49a8c;
  --land-accent: #e8b04b;     /* the brand gold, at full strength at last */
  --land-soft: #1c1812;
  --land-soft-line: #352c1c;
  --land-cta: #e8b04b;
  --land-cta-ink: #231a05;
}
/* ---------- the front door ----------
   The signed-out screen. A daily user never sees it — boot() goes straight to the app — so it
   is written for someone who has just arrived and does not yet know what this is. It scrolls,
   unlike every other screen here, and it is the one place in the app with a page of prose. */
.land {
  /* Light, and only here. The app itself is pure black on purpose — it is used in a dark
     bedroom with a child going to sleep — but a page someone lands on in daylight to decide
     whether to try it wants the opposite. Every .land-* rule below reads these, so the whole
     front door changes with this block and nothing else in the app moves. */
  --bg: var(--land-bg);
  --panel: var(--land-panel);
  --panel-2: var(--land-soft);
  --line: var(--land-line);
  --text: var(--land-text);
  --dim: var(--land-dim);
  --accent: var(--land-accent);
  --accent-soft: var(--land-soft);

  /* No overflow here. It used to be `overflow-y: auto`, which makes this a scroll container
     and so silently stops `position: sticky` working for anything inside it — the header
     scrolled away with the page, and the half-gone state on the way out read as the Start
     link being cut in two. The window scrolls; this does not need to. */
  display: block; min-height: 100dvh;
  /* A glow at the top rather than a flat field, fading out before the cards, which gives the
     hero somewhere to sit. */
  background: radial-gradient(125% 55% at 50% -5%, var(--land-glow) 0%, var(--land-bg) 58%) no-repeat, var(--bg);
  color: var(--text);
}
/* The page behind it, so an over-scroll bounce does not show the app's black. */
body:has(.land:not(.hidden)) { background: var(--land-bg); }
.land > * { max-width: 620px; margin: 0 auto; padding: 0 20px; }
.land-top {
  display: flex; align-items: center; justify-content: space-between;
  padding-top: 16px; padding-bottom: 12px; border: none;
  /* No background of its own while it is at the top. The page has a glow behind the hero, and
     a flat band of --land-bg painted over it showed up as exactly that: a bar, 620px wide on
     a 1440px screen, stopping short of both edges. */
  position: sticky; top: 0; z-index: 5;
}
/* The bar it becomes once it has something behind it. Full-bleed, because the header itself is
   held to the 620px reading column and a background that stops with the column reads as a
   gap rather than as a header. */
.land-top::before {
  content: ''; position: absolute; top: 0; bottom: 0; left: 50%;
  width: 100vw; transform: translateX(-50%); z-index: -1;
  background: transparent; border-bottom: 1px solid transparent;
  transition: background-color .18s linear, border-color .18s linear;
}
.land-top.stuck::before {
  background: var(--land-bg); border-bottom-color: var(--land-line);
}

.land-jump { color: var(--accent); text-decoration: none; font-size: 15px; }
.land-nav { display: flex; align-items: center; gap: 16px; }
.land-nav a { color: var(--dim); text-decoration: none; font-size: 15px; }
.land-nav a.on { color: var(--text); }
/* Start is the one thing on this page anyone is meant to press, and it was a thin gold word
   that happened to sit on a half pixel — fifteen pixels of hairline type, light on black, at
   x.58. Solid type on a solid pill instead: nothing hairline left to soften, and it now looks
   like the button it always was. */
.land-nav a.land-jump {
  color: var(--land-cta-ink); background: var(--land-cta);
  font-weight: 600; padding: 7px 15px; border-radius: 999px; line-height: 1.2;
}
.land-nav a.land-jump:hover { filter: brightness(1.06); }

/* The illustrations.
   Each is drawn on its own cream, a few points off the page's own, so squared off they would
   read as faint rectangles. The edges are faded instead, which hides the seam whatever the
   drawing's own background happens to be and looks deliberate rather than patched. */
/* The illustrations were drawn on flat cream, which was invisible when the page was cream and
   is a lit rectangle now that it is not. They are faded out with a mask rather than cut out:
   the background is not quite flat enough to key cleanly — a flood fill walks straight through
   the father's shirt and the child's face — and a bad cutout is worse than none.
   The ellipse has to finish INSIDE the box on both axes or the corners survive, which is what
   the old 78%/94% did: at the left edge it was only two-thirds of the way along the ramp. */
.land-art {
  display: block; width: 100%; max-width: 380px; height: auto; margin: 4px auto 0;
  --art-fade: radial-gradient(ellipse 60% 62% at 50% 46%, #000 30%, transparent 70%);
  -webkit-mask-image: var(--art-fade);
  mask-image: var(--art-fade);
}
.land-art.wide { max-width: 440px; margin: 16px auto 4px; }
/* The closing argument, directly above the sign-up. Its heading joins the spine; its two
   paragraphs do not. */
.land-why b { display: block; text-align: center; font-size: 17px; margin-bottom: 8px; }
/* The hero is a silhouette against a sunset, which arrives at near-black by itself at the top
   and in the corners — so it wants almost no mask, only enough to lose the straight bottom
   edge and the two side edges into the page. The cream drawings needed the opposite. */
.land-art.hero-art {
  max-width: 520px;
  /* Its own corners are dark but not THIS dark, so a straight seam shows where the picture
     meets the page. The fade has to finish well inside the frame on both axes to lose it. */
  --art-fade: radial-gradient(ellipse 64% 72% at 50% 48%, #000 34%, transparent 84%);
  margin-bottom: -4px;
}
/* A pool of warmth under the art, so what is left of the cream reads as the picture being lit
   rather than as a patch that failed to fade. */
.land-figure { position: relative; }
.land-figure::before {
  content: ''; position: absolute; inset: -6% -4% 2%;
  background: radial-gradient(ellipse 48% 50% at 50% 46%, var(--land-glow) 0%, transparent 72%);
  z-index: -1; pointer-events: none;
}

.land-hero { text-align: center; padding-top: 30px; padding-bottom: 8px; }
/* "Dad Speaks Chinese" is twice the length of the name this was sized for, and at 40px it
   overruns a phone by a wide margin. Two lines rather than one shrunken one: three words on
   one line at phone width would have to be small enough to stop being a wordmark. The
   balance keeps it from breaking after a single short word. */
.land-hero .brand {
  font-size: clamp(27px, 7vw, 40px);
  max-width: 11em; margin-inline: auto; text-wrap: balance; line-height: 1.15;
}
.land-hero .tagline { margin-bottom: 20px; }
/* The pitch, read first, so it carries the weight. */
.land-lead { font-size: 20px; font-weight: 600; line-height: 1.45; margin: 0 auto; max-width: 20em; text-wrap: balance; }
.land-cta {
  /* The full-strength brand gold, which is a background here rather than text, so it keeps
     its warmth and still carries near-black type at about 9:1. */
  display: inline-block; background: var(--land-cta); color: var(--land-cta-ink); font-weight: 600;
  /* Wide and shallow rather than chunky: it reads as a bar across the column instead of a
     lozenge sitting in the middle of it. */
  text-decoration: none; padding: 12px 34px; border-radius: 999px; font-size: 17px;
  min-width: 280px; letter-spacing: .2px;
  box-shadow: 0 2px 10px rgba(180, 130, 30, .28);
}
.land-cta:active { transform: translateY(1px); }
.land-note { color: var(--dim); font-size: 13px; margin: 14px 0 0; }

.land-block { padding-top: 40px; }
/* A centred spine down the page: the hero, then every section's heading and its one-line
   sub-heading, then the button at the end. Everything that is actually read — prose, lists,
   form fields, the price cards — stays left, because centred text makes the eye hunt for the
   start of each line and the Why section is four paragraphs long.
   Before this the hero was centred and everything under it was left, with nothing to say the
   change was meant. */
.land-block h2 { font-size: 21px; margin: 0 0 6px; text-align: center; }
.land-sub { color: var(--dim); font-size: 14px; margin: 0 0 18px; text-align: center; }

/* The lesson, in four steps. The number sits in the margin so the words line up. */
.land-steps { list-style: none; counter-reset: step; margin: 0; padding: 0; }
.land-steps li { counter-increment: step; position: relative; padding: 0 0 22px 42px; }
.land-steps li::before {
  content: counter(step); position: absolute; left: 0; top: -1px;
  width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center;
  background: var(--accent-soft); color: var(--accent); font-size: 14px; font-weight: 600;
}
.land-steps b { display: block; margin-bottom: 2px; }
.land-steps span { color: var(--dim); font-size: 15px; }

/* One exchange, laid out as the conversation it is: his line, then hers back. */
.land-demo-lead { margin: 34px 0 2px; text-align: center; font-size: 17px; }
.land-demo-sub { margin: 0 auto; max-width: 30em; color: var(--dim); font-size: 14px; text-align: center; }
.land-chat { display: flex; flex-direction: column; gap: 10px; margin: 18px 0 30px; text-align: left; }
/* The exchange arrives as an exchange: his line, then a beat, then hers. It is the one thing
   on the page that shows what using this is like, so it is worth the two seconds. */
@keyframes bubbleIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.land-bubble { animation: bubbleIn .55s cubic-bezier(.2, .7, .3, 1) backwards; }
.land-bubble.you { animation-delay: .35s; }
.land-bubble.her { animation-delay: 1.45s; }
@media (prefers-reduced-motion: reduce) {
  .land-bubble { animation: none; }
}
.land-bubble {
  position: relative; align-self: flex-start; max-width: 82%;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 18px 18px 18px 4px; padding: 13px 16px;
  box-shadow: 0 1px 2px rgba(28, 24, 14, .05);
}
.land-bubble.her {
  align-self: flex-end; background: var(--accent-soft); border-color: var(--land-soft-line);
  border-radius: 18px 18px 4px 18px;
}
.land-bubble b { display: block; font-size: 25px; letter-spacing: 1px; }
.land-bubble i { display: block; color: var(--accent); font-style: normal; font-size: 15px; margin-top: 3px; }
.land-bubble span { display: block; color: var(--dim); font-size: 14px; margin-top: 5px; }
/* Who is speaking, under the bubble rather than inside it. */
.land-bubble em {
  position: absolute; bottom: -19px; left: 2px;
  font-style: normal; font-size: 11px; letter-spacing: .6px; text-transform: uppercase; color: var(--dim);
}
.land-bubble.her em { left: auto; right: 2px; }
.land-chat .land-bubble.her { margin-bottom: 8px; }

/* The three things it does, high up, for someone deciding whether to read on. */
/* The line above introduces these, so they sit closer to it than to the lead. */
/* Who it is for: a qualifier under the pitch, not a second claim competing with it. */
.land-who { margin: 14px auto 0; font-size: 15px; line-height: 1.6; color: var(--dim); max-width: 27em; text-wrap: balance; }
.land-feats { list-style: none; margin: 0; padding: 12px 0 4px; display: grid; gap: 8px; text-align: left; }
.land-feats li {
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
  padding: 13px 15px; display: flex; gap: 11px; align-items: flex-start;
  box-shadow: 0 1px 2px rgba(28, 24, 14, .04);
}
.land-feats .ficon { width: 20px; height: 20px; flex: 0 0 20px; color: var(--accent); margin-top: 1px; }
.land-feats b { flex: 0 0 88px; }   /* wide enough for 'Recordings' */
.land-feats span { color: var(--dim); font-size: 15px; }

/* Who it is for, and what it deliberately leaves out. */
.land-why { margin-top: 30px; }
.land-why > div { background: var(--accent-soft); border-radius: 16px; padding: 18px 20px; }
.land-why p { margin: 0 0 10px; font-size: 15px; line-height: 1.6; color: var(--text); }
.land-why p + p { color: var(--dim); }
.land-why p:last-child { margin-bottom: 0; }
.land-why b { color: var(--accent); }


.land-ticks { list-style: none; margin: 0; padding: 0; }
.land-ticks li { position: relative; padding: 0 0 12px 26px; color: var(--dim); font-size: 15px; }
.land-ticks li::before { content: '\2713'; position: absolute; left: 0; color: var(--accent); }
.land-ticks b { color: var(--text); font-weight: 600; }

/* The passcode box, which is where the page has been leading. */
#start form { display: flex; gap: 8px; flex-wrap: wrap; }
/* Smaller than the app's own inputs, which are sized for a thumb in the dark. These are
   typed once, on a page, and full-height boxes made the whole section look like a form to
   fill in rather than a way in. */
.land-auth { flex-direction: column; gap: 8px; max-width: 340px; margin: 0 auto; }
.land-auth input { flex: 0 0 auto; width: 100%; text-align: left; padding: 11px 13px; border-radius: 10px; font-size: 15px; }
.land-auth button { width: 100%; padding: 12px; }

/* The way in most people will take, so it comes first and looks like itself. */
.land-google {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  max-width: 340px; margin: 0 auto; padding: 12px 16px;
  background: #fff; color: #1f1f1f; border: 1px solid #dadce0; border-radius: 10px;
  font-size: 15px; font-weight: 500; text-decoration: none;
  box-shadow: 0 1px 2px rgba(60, 64, 67, .12);
}
.land-google:active { background: #f7f8f8; }
.land-or {
  max-width: 340px; margin: 14px auto; color: var(--dim); font-size: 13px; text-align: center;
  display: flex; align-items: center; gap: 12px;
}
.land-or::before, .land-or::after { content: ''; flex: 1; height: 1px; background: var(--line); }
.land-alt { color: var(--dim); font-size: 14px; margin: 12px 0 0; }
.linky {
  background: none; border: none; padding: 0; font: inherit; color: var(--accent);
  text-decoration: underline; cursor: pointer;
}
/* The passcode is for the private test and should not be the first thing anyone sees. */
.land-passcode { margin-top: 22px; }
.land-passcode summary { color: var(--dim); font-size: 14px; cursor: pointer; }
.land-passcode form { margin-top: 12px; }
#start button { background: var(--land-cta); color: var(--land-cta-ink); border: none; font-weight: 600; padding: 14px 22px; }
.land-passcode input { width: auto; flex: 1 1 180px; text-align: left; }
#start .error { margin-top: 4px; }

/* Pinyin only. The characters are the biggest thing on the line and most of the people this
   is for cannot read them, so they are optional — which the settings table has said all along
   without anything acting on it. */
/* Every .hant, not only the ones inside a .zh: the phrase list and the drill's answer card
   put theirs straight into .say and .answer. */
.no-hant .hant { display: none; }

/* The policy pages. Real pages rather than panels inside the app, because a payment reviewer
   and a search engine should both be able to read a privacy policy without running any
   JavaScript — and because a signed-in user following a link to one should not find the app
   has simply hidden itself. */
.legal { min-height: 100vh; }
.legal-body { max-width: 640px; margin: 0 auto; padding: 8px 16px 48px; }
.legal-body h1 { font-size: 30px; line-height: 1.2; margin: 18px 0 4px; }
.legal-body h2 { font-size: 18px; margin: 30px 0 8px; }
.legal-body p, .legal-body li { line-height: 1.65; color: var(--ink); }
.legal-body ul { padding-left: 20px; }
.legal-body li { margin: 7px 0; }
.legal-date { color: var(--dim); font-size: 13px; margin-top: 0; }
.legal-email { font-size: 19px; margin: 6px 0 14px; }
.legal-nav { margin-top: 40px; padding-top: 18px; border-top: 1px solid var(--line); font-size: 14px; }
.land-mark { font-weight: 600; text-decoration: none; color: var(--land-accent, var(--accent)); }

/* The way past the paywall. Shown with two sessions to go rather than at zero, so the first
   he hears of it is not the evening it stops working. */
.pay b { display: block; margin-bottom: 4px; }
.pay-buttons { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0 2px; }
.pay-buttons button { flex: 1 1 auto; }
.pay-buttons button:disabled { opacity: .6; }

/* Deleting a recording of someone's child should not look like any other button. */
.toggle.danger { color: #b3261e; border-color: #e7b4ae; }
.toggle.danger:disabled { opacity: .6; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .toggle.danger { color: #f2b8b5; border-color: #6b3b37; }
}
.rec-foot { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-top: 10px; }
/* A button that reads as a link: destructive, so it should not invite a stray tap. */
.linky {
  background: none; border: 0; padding: 4px 0; font: inherit; font-size: 13px;
  color: var(--dim); text-decoration: underline; cursor: pointer;
}
.linky:hover { color: #b3261e; }
.linky:disabled { opacity: .6; text-decoration: none; }

.choice { display: flex; gap: 6px; flex: 0 0 auto; }
.choice button {
  padding: 9px 12px; border-radius: 999px; font-size: 14px;
  background: var(--panel-2); color: var(--dim); border: 1px solid var(--line);
}
.choice button[aria-checked="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }

.toggle {
  flex: 0 0 auto; min-width: 62px; padding: 9px 14px; border-radius: 999px;
  background: var(--panel-2); color: var(--text); border: 1px solid var(--line); font: inherit;
}
.toggle[aria-checked="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }

/* A recording. It opens when tapped, which nothing about it used to suggest. */
.rec { cursor: pointer; }
.rec.busy { cursor: default; opacity: .75; }
.rec-top { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
.rec-state { color: var(--dim); font-size: 12px; white-space: nowrap; }
.rec.busy .rec-state { color: var(--accent); }
.rec-sum { color: var(--dim); font-size: 13px; margin: 8px 0 0; }
.rec-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.rec-badges:empty { display: none; }
.rec-more { color: var(--accent); font-size: 12px; }
.rec .detail + .rec-more, .rec:has(.detail) .rec-more { display: none; }

/* How many free sessions are left. Quiet until it matters, then not. */
.standing { color: var(--dim); font-size: 14px; text-align: center; margin: 0 0 14px; }
.standing.out { color: var(--accent); }

/* The reminder switch, on Today. */
.reminder-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
.reminder-row b { display: block; }
.reminder-note { color: var(--dim); font-size: 13px; }
.reminder select {
  background: var(--panel-2); color: var(--text); border: 1px solid var(--line);
  border-radius: 12px; padding: 10px 12px; font: inherit; flex: 0 0 auto;
}

/* The moments, drawn. The page was boxes of text all the way down, and these are also the
   only place it says which parts of the day it covers. */
.land-moments ul {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px;
}
.land-moments li {
  background: var(--panel); border: 1px solid var(--line); border-radius: 16px;
  padding: 18px 12px 16px; text-align: center; box-shadow: 0 1px 2px rgba(28, 24, 14, .04);
}
.land-moments svg {
  width: 30px; height: 30px; color: var(--accent); display: block; margin: 0 auto 10px;
}
.land-moments b { display: block; font-size: 15px; }
.land-moments span { display: block; color: var(--dim); font-size: 13px; line-height: 1.45; margin-top: 4px; }

/* Why it exists, in his own voice, so it is set as prose rather than as another card. */
.land-about p { font-size: 15.5px; line-height: 1.65; margin: 0 0 14px; }
.land-about p:last-child { margin-bottom: 0; color: var(--dim); }

.land-prices {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px;
}
.land-prices li {
  background: var(--panel); border: 1px solid var(--line); border-radius: 16px;
  padding: 18px 14px; text-align: center; box-shadow: 0 1px 2px rgba(28, 24, 14, .04);
}
.land-prices .best { background: var(--accent-soft); border-color: var(--land-soft-line); }
.land-prices b { display: block; font-size: 25px; letter-spacing: -.5px; }
.land-prices span { display: block; color: var(--dim); font-size: 14px; margin-top: 2px; }
.land-prices em { display: block; font-style: normal; color: var(--accent); font-size: 12.5px; margin-top: 8px; }

.land-install {
  background: var(--land-cta); color: var(--land-cta-ink); border: none; font-weight: 600;
  padding: 14px 22px; border-radius: 14px;
}
.land-installed { color: var(--dim); font-size: 15px; margin: 0; }

.land-foot {
  color: var(--dim); font-size: 13px; text-align: center;
  padding-top: 44px; padding-bottom: 36px;
}
.land-foot p { margin: 0; }
.land-legal { margin-top: 10px !important; }
.land-legal a { color: var(--dim); }

.hint.left { text-align: left; }
.panel-title { font-size: 20px; margin: 0 0 6px; }
/* Recording is a state you are in, not a button you are holding. */
#recBtn.recording { background: var(--bad); color: #fff; }

header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px; border-bottom: 1px solid var(--line); position: sticky; top: 0;
  background: var(--bg); z-index: 5;
}
nav { display: flex; gap: 4px; }
.tab { padding: 8px 12px; font-size: 14px; background: transparent; border: none; color: var(--dim); }
.tab.active { color: var(--accent); }
.tab-panel { padding: 16px; flex: 1; max-width: 720px; width: 100%; margin: 0 auto; }
/* A panel that fills the column has to be allowed to shrink inside it, or its own scrolling
   areas push the page taller instead of scrolling. */
.tab-panel { min-height: 0; }

.stat-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 20px; }
.stat { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 12px 8px; text-align: center; }
.stat b { display: block; font-size: 22px; color: var(--accent); }
.stat span { font-size: 11px; color: var(--dim); }

.card { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 14px; margin-bottom: 12px; }
.card.consent { border-color: var(--accent-soft); background: #15120a; }
.card.consent p { margin: 6px 0 0; color: var(--dim); font-size: 13px; }

/* ---------- the transcript ---------- */
.transcript { flex: 1; display: flex; flex-direction: column; gap: 14px; padding-bottom: 12px; }
.bubble { padding: 12px 14px; border-radius: 14px; max-width: 92%; }
.bubble.tutor { background: var(--panel); border: 1px solid var(--line); align-self: flex-start; line-height: 2.1; }
.bubble.me { background: var(--panel-2); align-self: flex-end; color: var(--dim); }
/* His own line comes back as characters and pinyin rather than plain text, so that hiding the
   characters leaves the pinyin instead of an empty bubble. Smaller than the tutor's: he has
   just said it, and it is an echo rather than something to read. */
.bubble.me .zh .hant { font-size: 20px; letter-spacing: 1px; }
.bubble.me .zh .py { font-size: 14px; }
/* One per sentence, so the pinyin and gloss can be swapped in once they arrive. It takes no box
   of its own: the sentences flow together as if they were still appended to the bubble. */
.line { display: contents; }

.zh { display: inline-block; margin: 6px 4px 0 0; vertical-align: bottom; cursor: pointer; }
.zh .hant { font-size: 26px; line-height: 1.25; letter-spacing: 1px; }
.zh .py { font-size: 15px; color: var(--dim); letter-spacing: .3px; }
.zh:active .hant { opacity: .6; }
.t1 { color: var(--t1); } .t2 { color: var(--t2); } .t3 { color: var(--t3); }
.t4 { color: var(--t4); } .t5 { color: var(--t5); }
.gloss-row { margin-top: 6px; font-size: 12px; color: var(--dim); }
.gloss-row span { margin-right: 10px; }

/* ---------- the face ---------- */
/* There is no button to hold any more, so the face has to say what is happening: whether it is
   listening to you, thinking, or talking. */
/* It used to be calc(100vh - 56px). The header is 104px on a phone — two rows, because the tabs
   wrap onto their own line — so the box was 48px taller than the room it had and everything
   centred in it sat half that too low. That is the "dragon is too low". Take the height from the
   column instead of guessing it. */
.lesson { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.stage { display: flex; flex-direction: column; align-items: center; padding: 8px 0 4px; flex: 0 0 auto; }

.face { position: relative; width: 148px; height: 148px; padding: 0; border: none; background: none; border-radius: 50%; cursor: pointer; }
.face img { width: 100%; height: 100%; border-radius: 50%; display: block; }
.face .ring {
  position: absolute; inset: -8px; border-radius: 50%;
  border: 3px solid transparent; transition: border-color .25s ease;
}

/* listening: a slow breathing ring */
[data-state="listening"] .ring { border-color: var(--good); animation: breathe 1.9s ease-in-out infinite; }
@keyframes breathe { 0%,100% { transform: scale(1);   opacity: .9 }
                     50%     { transform: scale(1.06); opacity: .45 } }

/* thinking: a quiet sweep */
[data-state="thinking"] .ring { border-color: var(--line); border-top-color: var(--accent); animation: spin 1.1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg) } }

/* speaking: the ring glows and the mouth moves */
[data-state="speaking"] .ring { border-color: var(--accent); animation: pulse .55s ease-in-out infinite; }
@keyframes pulse { 0%,100% { transform: scale(1.01) } 50% { transform: scale(1.05) } }
[data-state="speaking"] .face img { animation: talk .34s ease-in-out infinite alternate; }
@keyframes talk { from { transform: scaleY(1) } to { transform: scaleY(1.012) } }

[data-state="idle"] .ring { border-color: var(--line); }
.face:active { transform: scale(.97); }

.lesson .status { margin: 10px 0 2px; min-height: 24px; font-size: 15px; }
.lesson .transcript { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 4px 0 8px; }

/* The dragon stays put and the conversation scrolls under it.
   The rules above always meant this, and it never happened: .screen is a MIN-height, so the
   page simply grew with the conversation and the whole thing scrolled — dragon and all. After
   a few turns the face was off the top, and tapping it to recover from an error meant scrolling
   all the way back up to find it. While the Talk tab is open the page is exactly the height of
   the screen, so the only thing with anywhere to scroll is the transcript. */
.screen:has(#tab-lesson:not(.hidden)) { height: 100dvh; min-height: 0; overflow: hidden; }
/* The front door is not a lesson and must never be cut off on a short phone. */
.lesson.waiting { overflow-y: auto; }
/* On a short phone a full-size face takes half the room the conversation has. */
@media (max-height: 720px) {
  .lesson:not(.waiting) .face { width: 104px; height: 104px; }
  .lesson:not(.waiting) .status { margin-top: 6px; }
}
.lesson-bar { display: flex; justify-content: center; padding: 6px 0 10px; }

/* Before a lesson exists this screen is the front door: the dragon, centred, and nothing else.
   No transcript to scroll and nothing to end. */
.lesson.waiting .lesson-bar,
.lesson.waiting .transcript { display: none; }
/* Centred, then lifted. Dead centre reads as low: the eye puts the middle of a page a little
   above the middle of the box, and the chips hanging under the dragon weigh the group down
   further.
   The lift is a share of the LEFT-OVER space, not of the screen. A fixed 7vh looked right on a
   tall phone and wrong on a short one — 33px of air above the dragon and 72px below it, which is
   the same complaint in the other direction. Two empty flex items either side of the group split
   whatever room there is, 1 to 1.45, so the proportion holds at any height. */
.lesson.waiting .stage { flex: 1; justify-content: flex-start; }
.lesson.waiting .stage::before { content: ''; flex: 1 0 0; }
.lesson.waiting .stage::after { content: ''; flex: 1.45 0 0; }
.lesson.waiting .status { font-size: 17px; margin-top: 18px; }
/* One size. There were two of these rules, 208 then 176, and only the second ever applied. */
.lesson.waiting .face { width: 176px; height: 176px; }

/* Topics: only ever shown on the front door. */
.topics { display: none; }
/* Wrapping seven centred pills gives 3 / 3 / 1, and the orphan on the last row is most of why
   this screen looked untidy. A two-column grid has no orphan: the odd one out spans the row on
   purpose. Equal widths also stop "Play" and "Daily events" looking like different kinds of
   thing. */
.lesson.waiting .topics {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px;
  margin-top: 26px; width: 100%; max-width: 340px; padding: 0 12px;
}
.lesson.waiting .topics button:nth-child(odd):last-child { grid-column: 1 / -1; }
.topics button {
  padding: 11px 14px; border-radius: 999px; font-size: 14px;
  background: var(--panel); border: 1px solid var(--line); color: var(--text);
}
.topics button:active { background: var(--panel-2); }
/* A topic with no sentence patterns cannot do the swap-a-word drill. Say so quietly rather
   than offering twelve buttons that look identical and are not. */
.topics button.thin { color: var(--dim); border-style: dashed; }
/* The one he chose, and — while a lesson is being built — that it is happening. */
.topics button.on { background: var(--accent); color: #1a1408; border-color: var(--accent); }
.topics button.busy { animation: chipwait 1s ease-in-out infinite; }
@keyframes chipwait { 0%,100% { opacity: 1 } 50% { opacity: .55 } }

/* ---------- listening scenes ---------- */
.ep { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 14px; margin-bottom: 10px; cursor: pointer; }
.ep h3 { margin: 0 0 4px; font-size: 17px; }
.ep p { margin: 0; color: var(--dim); font-size: 13px; }
.ep .who { font-size: 12px; color: var(--accent); margin-top: 8px; }
.ep:active { background: var(--panel-2); }

.player { padding-bottom: 24px; }
.player .step { color: var(--dim); font-size: 13px; margin: 14px 0 8px; letter-spacing: .4px; text-transform: uppercase; }
/* The line being spoken. Everything else stays dim so his eye is in the right place. */
.turn { display: flex; gap: 10px; padding: 10px 0; opacity: .38; transition: opacity .2s; }
.turn.on { opacity: 1; }
/* Playing one part: the rest of the episode stays in view but steps right back. */
.turn.dim { opacity: .14; }
/* Part 1 · Part 2 · Part 3 · Whole episode. The one playing is lit. */
.parts { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 4px; }
.parts button {
  padding: 9px 14px; border-radius: 999px; font-size: 14px;
  background: var(--panel); border: 1px solid var(--line); color: var(--text);
}
.parts button.on { border-color: var(--accent); color: var(--accent); }
.turn .role { flex: 0 0 auto; font-size: 11px; color: var(--dim); padding-top: 6px; width: 46px; }
.turn .zh .hant { font-size: 23px; letter-spacing: 1px; }
.turn .zh .py { font-size: 14px; color: var(--dim); }
.turn .en { color: var(--dim); font-size: 14px; margin-top: 2px; }
.player .key { display: flex; justify-content: space-between; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--line); }
.player .key .w { font-size: 19px; }
.player .key .g { color: var(--dim); font-size: 13px; text-align: right; flex: 0 1 62%; }

/* ---------- asking for a sentence ---------- */
.ask { display: flex; gap: 8px; margin-bottom: 14px; }
.ask input { flex: 1; width: auto; text-align: left; padding: 12px 14px; }
.ask button { padding: 12px 18px; }
.answer { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 14px; margin-bottom: 16px; }
.answer .hant { font-size: 30px; letter-spacing: 1px; line-height: 1.3; }
.answer .py { font-size: 16px; color: var(--dim); margin-top: 2px; }
.answer .mean-en { color: var(--text); font-size: 15px; margin-top: 8px; }
.answer .note { color: var(--dim); font-size: 13px; margin-top: 10px; }
.answer .parts span { cursor: pointer; }
/* A word he has not been taught yet, so he knows why he does not recognise it. */
.answer .parts span.unseen { opacity: .65; }
.answer .parts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.answer .parts span { font-size: 12px; color: var(--dim); background: var(--panel-2); border-radius: 999px; padding: 4px 9px; }
/* A word the dictionary does not have is not the same as one it does, and it should not look it. */
.answer .parts span.new { color: var(--bad); border: 1px solid var(--bad); }
.answer .caveat { color: var(--bad); font-size: 12px; margin-top: 10px; }

/* ---------- the phrasebook ---------- */
/* Topic chips here are a filter, not a way in, so they are always shown and one is selected. */
.filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.filters button.on { background: var(--accent); color: #1a1408; border-color: var(--accent); }
.phrase {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 12px 0; border-bottom: 1px solid var(--line); cursor: pointer;
}
.phrase:last-child { border-bottom: none; }
.phrase .say .hant { font-size: 24px; letter-spacing: 1px; }
.phrase .say .py { font-size: 14px; color: var(--dim); }
.phrase .mean { color: var(--dim); font-size: 14px; text-align: right; flex: 0 1 46%; }
.phrase .badge { margin-left: 0; }
.phrase .when { font-size: 12px; color: var(--dim); opacity: .7; margin-top: 2px; }
.phrase:active { opacity: .6; }
/* The line whose card is showing above. */
.phrase.on { border-left: 3px solid var(--accent); padding-left: 10px; }
/* Still ahead of him: readable, tappable, hearable — but clearly not his yet. */
.ahead-head {
  margin: 22px 0 4px; padding-top: 14px; border-top: 1px solid var(--line);
  font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--dim);
}
.phrase.ahead .say .hant { color: var(--dim); }
.phrase.ahead .mean { opacity: .75; }
.phrase.on .say .hant { color: var(--accent); }

.start-cta { display: flex; flex-direction: column; align-items: center; gap: 10px; margin-top: 24px; }

.word-row { display: flex; justify-content: space-between; align-items: baseline; padding: 10px 0; border-bottom: 1px solid var(--line); }
.word-row:last-child { border-bottom: none; }
.word-row .m { font-size: 19px; }
.word-row .g { color: var(--dim); font-size: 13px; text-align: right; margin-left: 12px; }
.badge { display: inline-block; font-size: 11px; padding: 2px 7px; border-radius: 999px; background: var(--accent-soft); color: var(--accent); margin-left: 6px; }

.bar { height: 6px; background: var(--panel-2); border-radius: 999px; overflow: hidden; margin-top: 6px; }
.bar i { display: block; height: 100%; background: var(--accent); }

/* ---------- feedback ---------- */
/* Available on every screen, not just at the end of a session: the moment something feels wrong
   is the moment worth capturing, and by the summary screen he has forgotten what it was. */
.fb-open {
  font-size: 12px; padding: 5px 10px; border-radius: 999px;
  background: transparent; border: 1px solid var(--line); color: var(--dim);
}
.sheet {
  position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 50;
  display: flex; align-items: flex-end; justify-content: center;
}
.sheet-inner {
  background: var(--panel); border: 1px solid var(--line); border-radius: 18px 18px 0 0;
  padding: 18px 16px calc(18px + env(safe-area-inset-bottom)); width: 100%; max-width: 520px;
}
.sheet-inner textarea {
  width: 100%; background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  border-radius: 12px; padding: 12px; font: inherit; resize: vertical;
}
.fb-ratings { display: flex; gap: 8px; margin-bottom: 10px; }
.chip { font-size: 13px; padding: 7px 14px; border-radius: 999px; }
.chip.on { background: var(--accent); color: #1a1408; border-color: transparent; }
.sheet-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }

/* ---------- letters: a character with its picture drawn over it ---------- */
/* The glyph is rendered by the FONT and then scaled so its INK fills a known square, measured
   on a canvas before anything is drawn on top. A stroke drawn by hand is a stroke he could learn
   wrong; the gold is a drawing over the real thing, never a replacement for it. */
.glyphs { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
.glyph-card { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 14px; }
.glyph-card svg { display: block; margin: 0 auto; width: 200px; height: 200px; }
.glyph-card .ink { fill: none; stroke: var(--line); stroke-dasharray: 2 6; opacity: .5; }
.glyph-card .strokes { fill: #3c434f; }
.glyph-card .draw { fill: none; stroke: var(--accent); stroke-width: 2.8;
                    stroke-linecap: round; stroke-linejoin: round; }
.glyph-card .draw .dash { stroke-dasharray: 2.5 6; }
.glyph-card .dot { fill: var(--accent); stroke: none; }
.glyph-head { display: flex; align-items: baseline; gap: 10px; margin-top: 6px; }
.glyph-head b { font-size: 26px; font-weight: 400; letter-spacing: 2px; }
.glyph-head span { color: var(--dim); font-size: 14px; }
.glyph-head .badge { margin-left: auto; }
.glyph-part { color: var(--accent); font-size: 13px; margin-top: 4px; }
.glyph-story { font-size: 14px; margin: 8px 0 0; }
.glyph-origin { color: var(--dim); font-size: 13px; margin: 8px 0 0; }
.glyph-origin span { display: block; font-size: 11px; letter-spacing: .07em; text-transform: uppercase; }
.glyph-origin .true-origin { color: var(--good); }
.glyph-origin .other-origin { color: var(--dim); opacity: .8; }

/* The words of one line, under it. They were a flat list at the bottom of the episode. */
.turn .bits { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.turn .bit {
  display: flex; align-items: baseline; gap: 5px; padding: 4px 9px; border-radius: 999px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text); font: inherit;
}
.turn .bit .bh { font-size: 15px; letter-spacing: .2px; }   /* pinyin, not characters */
.turn .bit .bg { font-size: 12px; color: var(--dim); }
.turn .bit:active { background: var(--panel); }

/* Search, and the two things he can do with a line once he has found it. */
.ask.find { margin-top: 10px; }
.phrase-do { display: flex; gap: 8px; margin-top: 10px; }
.phrase-do button {
  padding: 7px 13px; border-radius: 999px; font-size: 13px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
}
.phrase-do button.go { background: var(--accent); color: #140f04; border-color: var(--accent); }
.phrase-do button:disabled { opacity: .45; }
.phrase .near { font-size: 11px; color: var(--dim); opacity: .7; margin-left: 6px; }
