:root {
  color-scheme: light;
  --ground: #EEF3F8;
  --surface: #FFFFFF;
  --surface-soft: #E7EFF6;
  --border: rgba(34, 51, 69, 0.16);
  --ink: #223345;
  --ink-soft: #5B7086;
  --accent: #3E7CB8;
  --accent-strong: #2C5C8A;
  --accent-warm: #B9791F;
  --accent-warm-soft: #F4E9CE;
  --state-safe: #2E8F63;
  --state-safe-bg: #E3F3EA;
  --state-caution: #B9791F;
  --state-caution-bg: #F7ECD8;
  --state-danger: #C24B3F;
  --state-danger-bg: #FBE6E3;
  /* The "this one is chosen" pair, used by the menu button and the selected
     text size. It cannot just be `--accent` + white: in dark mode --accent is
     a pale blue and white on it measures 2.3:1, which would make the CURRENTLY
     CHOSEN text size the least readable thing in a menu whose entire job is
     readability. So the pair flips — dark ink on pale blue in the dark theme,
     white on deep blue in the light one. Both land near 7:1 or better. */
  --selected-bg: #2C5C8A;
  --selected-ink: #FFFFFF;
  /* THE PRIMARY BUTTON HAS TO FLIP THE SAME WAY, and for a while it did not.
     It was white on --accent in both themes, which is 4.3:1 in the light one
     and 2.3:1 in the dark — so "Check This Message", the single most important
     control in the app, was the least readable thing on the page for anybody
     who chose dark. Same reasoning as the pair above, so the same shape: deep
     blue with white on it in the light theme, pale blue with dark ink on it in
     the dark one. Both land above 7:1.
     Kept separate from --selected-* on purpose: one is "this is the option you
     picked", the other is "this is the thing to press", and they should be
     free to diverge. */
  --button-bg: #2C5C8A;
  --button-ink: #FFFFFF;
  --shadow: 0 1px 2px rgba(44, 92, 138, 0.08), 0 8px 24px rgba(44, 92, 138, 0.10);
  /* Warm lamp-glow used on hover — the porch light coming on. */
  --glow: 0 2px 4px rgba(44, 92, 138, 0.10), 0 14px 30px rgba(185, 121, 31, 0.22);
  --radius: 14px;
  --font-display: 'Fredoka', system-ui, sans-serif;
  --font-body: 'Atkinson Hyperlegible', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;

  /* ---- The porch ---------------------------------------------------------
     The app is called Porch Light and its tagline is "we'll keep the porch
     light on for you". None of that was anywhere in the design, which was a
     cool grey admin panel with a warm sentence at the bottom. These are the
     materials that metaphor is built from. Each one is drawn rather than
     loaded, so there is no image to fetch and they recolour with the theme.

     There is no drawn woodgrain here, and there was: literal boards behind
     the wordmark read as a rendering fault rather than as a porch. The
     metaphor carries better through light than through planks. */

  /* WEAVE — a coir doormat, for the GROUND the cards sit on.
     It was on the cards first, where even at 2% it read as graph paper: a
     grid inside a rectangle looks like a table. On the page behind them it
     reads as a floor, which is what it is for. Two sets of dashes at right
     angles; the offset on the second row is what stops it looking like graph
     paper. It replaced a "circuit maze" that meant nothing and read as
     printer noise. */
  --tex-weave:
    repeating-linear-gradient(90deg,
      rgba(34, 51, 69, 0.022) 0px, rgba(34, 51, 69, 0.022) 3px,
      transparent 3px, transparent 16px),
    repeating-linear-gradient(0deg,
      rgba(34, 51, 69, 0.018) 0px, rgba(34, 51, 69, 0.018) 3px,
      transparent 3px, transparent 16px);

  /* LAMPLIGHT — the glow that falls from the header into the page. Warm,
     wide, and almost not there; you should notice the room is lit, not the
     lamp. */
  --glow-lamp: radial-gradient(
    90% 100% at 22% -14%,
    rgba(255, 196, 74, 0.42) 0%,
    rgba(255, 201, 92, 0.20) 34%,
    rgba(255, 209, 102, 0.06) 60%,
    rgba(255, 209, 102, 0) 78%
  );
  --lamp-core: #FFD166;
  --lamp-halo: rgba(255, 209, 102, 0.55);
  --header-ground: #E4EDF6;
  --hairline-warm: rgba(185, 121, 31, 0.45);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --ground: #121A22;
    --surface: #1B2530;
    --surface-soft: #212D3A;
    --border: rgba(231, 238, 245, 0.16);
    --ink: #E7EEF5;
    --ink-soft: #A9BBCB;
    --accent: #7FB2D6;
    --accent-strong: #9FC6E2;
    --accent-warm: #E7B355;
    --accent-warm-soft: #3A3020;
    --state-safe: #6FCB7F;
    --state-safe-bg: #1F3326;
    --state-caution: #E7B355;
    --state-caution-bg: #332912;
    --state-danger: #E37A6A;
    --state-danger-bg: #3A231F;
    --selected-bg: #7FB2D6;
    --selected-ink: #121A22;
    --button-bg: #7FB2D6;
    --button-ink: #121A22;
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 24px rgba(0, 0, 0, 0.35);
    --glow: 0 2px 4px rgba(0, 0, 0, 0.35), 0 14px 34px rgba(231, 179, 85, 0.26);
    --glow-lamp: radial-gradient(
      90% 100% at 22% -14%,
      rgba(255, 190, 70, 0.20) 0%,
      rgba(255, 196, 74, 0.10) 34%,
      rgba(255, 209, 102, 0.035) 60%,
      rgba(255, 209, 102, 0) 78%
    );
    /* On a dark ground the same white highlight that reads as a lit board
       reads as a scratch, so the siding is drawn with light instead. */
      --tex-weave:
      repeating-linear-gradient(90deg,
        rgba(255, 255, 255, 0.020) 0px, rgba(255, 255, 255, 0.020) 3px,
        transparent 3px, transparent 16px),
      repeating-linear-gradient(0deg,
        rgba(255, 255, 255, 0.015) 0px, rgba(255, 255, 255, 0.015) 3px,
        transparent 3px, transparent 16px);
    --header-ground: #0D141B;
    --hairline-warm: rgba(231, 179, 85, 0.40);
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --ground: #121A22;
  --surface: #1B2530;
  --surface-soft: #212D3A;
  --border: rgba(231, 238, 245, 0.16);
  --ink: #E7EEF5;
  --ink-soft: #A9BBCB;
  --accent: #7FB2D6;
  --accent-strong: #9FC6E2;
  --accent-warm: #E7B355;
  --accent-warm-soft: #3A3020;
  --state-safe: #6FCB7F;
  --state-safe-bg: #1F3326;
  --state-caution: #E7B355;
  --state-caution-bg: #332912;
  --state-danger: #E37A6A;
  --state-danger-bg: #3A231F;
  --selected-bg: #7FB2D6;
  --selected-ink: #121A22;
  --button-bg: #7FB2D6;
  --button-ink: #121A22;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 24px rgba(0, 0, 0, 0.35);
  --glow: 0 2px 4px rgba(0, 0, 0, 0.35), 0 14px 34px rgba(231, 179, 85, 0.26);
  --glow-lamp: radial-gradient(
    90% 100% at 22% -14%,
    rgba(255, 190, 70, 0.20) 0%,
    rgba(255, 196, 74, 0.10) 34%,
    rgba(255, 209, 102, 0.035) 60%,
    rgba(255, 209, 102, 0) 78%
  );
  /* On a dark ground the same white highlight that reads as a lit board
     reads as a scratch, so the siding is drawn with light instead. */
  --tex-weave:
    repeating-linear-gradient(90deg,
      rgba(255, 255, 255, 0.020) 0px, rgba(255, 255, 255, 0.020) 3px,
      transparent 3px, transparent 16px),
    repeating-linear-gradient(0deg,
      rgba(255, 255, 255, 0.015) 0px, rgba(255, 255, 255, 0.015) 3px,
      transparent 3px, transparent 16px);
  --header-ground: #0D141B;
  --hairline-warm: rgba(231, 179, 85, 0.40);
}

* { box-sizing: border-box; }

html { font-size: 20px; }

body {
  background-image: var(--tex-weave);
  background-size: 16px 16px;
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

main {
  max-width: 760px;
  margin: 0 auto;
  padding: 28px 24px 64px;
  position: relative;
}

h1, h2, h3 { font-family: var(--font-display); font-weight: 600; margin: 0; }

h1 {
  font-size: 1.7rem;
  margin: 0 0 4px;
  color: var(--accent);
}

.subtitle {
  margin: 0 0 28px;
  color: var(--ink-soft);
}

/* ------------------------------------------------------------- the porch --
   THE HEADER IS THE PORCH ROOF, and the page below it is lit by the lamp
   hanging under it.

   It was a thin grey strip with the wordmark floating at one end. Now it runs
   the full width of the window, carries the siding texture, and finishes in a
   warm hairline rather than a grey border — light escaping under the eaves.
   The glow beneath it is a real element rather than a shadow, so it can spill
   into the page without the bar itself having to be tall.

   It stays sticky, and it stays SHORT: the whole point of the text-size
   control is that somebody can set 1.36x, and a header that grew with it
   would eat a third of a phone screen. The bar's padding is in px and the
   wordmark uses clamp(), so it grows a little and then stops. */
header.top {
  position: sticky;
  top: 0;
  z-index: 5;
  background-color: var(--header-ground);
  /* A little light pooling under the roof, and the warm hairline where it
     escapes at the edge — instead of the flat grey border it had. */
  background-image: linear-gradient(180deg, rgba(255, 209, 102, 0.10), rgba(255, 209, 102, 0.02));
  border-bottom: 1px solid var(--hairline-warm);
  padding: 14px 0;
  margin-bottom: 0;
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.35) inset, 0 6px 18px rgba(20, 30, 40, 0.10);
}

/* The light falling from under the roof onto the page.
   Painted by the BODY rather than by the header: the header is sticky, which
   makes it a stacking context, so anything it drew behind itself was behind
   its own background and invisible. Fixed to the top of the window so the
   light stays where the lamp is when the page scrolls. */
body::before {
  content: "";
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  height: 420px;
  background: var(--glow-lamp);
  pointer-events: none;
  z-index: 0;
}

header.top,
main,
footer { position: relative; z-index: 1; }

.top-row {
  max-width: 760px;
  margin: 0 auto;
  padding: 0 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

/* The Menu button never shrinks and never wraps: it is the only way out of
   any page, and a half-visible one is worse than none. */
.top-row .menu-button { flex-shrink: 0; white-space: nowrap; }

/* The lamp itself, hanging at the left of the bar beside the wordmark. */
.brand-lamp {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  margin-right: 2px;
}

.brand-lamp svg {
  width: clamp(22px, 5.4vw, 34px);
  height: clamp(22px, 5.4vw, 34px);
  display: block;
  overflow: visible;
}

.brand-lamp .lamp-halo {
  transform-origin: 32px 30px;
  animation: lamp-breathe 7s ease-in-out infinite;
}

/* SLOW, SMALL, AND NEVER A FLASH. Seven seconds, and the halo only changes
   size by a twentieth. A porch light that pulsed would read as a warning
   light, which is the opposite of the feeling this app is for. */
@keyframes lamp-breathe {
  0%, 100% { opacity: 0.72; transform: scale(1); }
  50%      { opacity: 1;    transform: scale(1.05); }
}

a.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  font-family: 'Playfair Display', Georgia, 'Times New Roman', serif;
  /* THE WORDMARK IS THE PART THAT YIELDS.
     At Extra Large text on a 390px phone the header ran out of room and the
     Menu button was pushed 60px off the side of the screen — on the one text
     setting this app exists to support. Something has to give, and between a
     logo and the only control in the header it is the logo: the middle term
     here is viewport-driven, so the wordmark shrinks with the screen rather
     than growing with the text setting, while the Menu button keeps scaling
     normally because it is a thing people have to read and press. */
  font-size: clamp(17px, 5.2vw, 1.7rem);
  min-width: 0;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink);
}

/* Both words are carved from the same wood — same grain, same bevel — but
   keep their own voices: "Porch" is a rounded plank sign, "Light" stays the
   elegant italic. Shared base first, then each word's own board. */
.wood-text,
a.brand b {
  display: inline-block;
  font-weight: 700;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}

/* "Porch" — dark walnut plank, tilted like a hand-hung sign. */
.wood-text {
  font-family: var(--font-display); /* Fredoka */
  font-style: normal;
  padding-right: 2px;
  transform: rotate(-2deg);
  background-image: repeating-linear-gradient(
    100deg,
    #8a5a2f 0px,
    #9c6a37 4px,
    #7a4a24 7px,
    #8a5a2f 10px,
    #6b4620 14px
  );
  text-shadow:
    -1px -1px 0 rgba(255, 240, 220, 0.25),
    1px 1px 0 rgba(60, 35, 10, 0.45),
    2px 2px 0 rgba(60, 35, 10, 0.3),
    3px 3px 3px rgba(0, 0, 0, 0.25);
}

/* "Light" — the same grain in honey/golden oak, but still the Playfair
   italic. The trailing padding keeps the italic's overhang from being
   clipped by background-clip: text. */
a.brand b {
  font-family: 'Playfair Display', Georgia, 'Times New Roman', serif;
  font-style: italic;
  padding-right: 0.08em;
  background-image: repeating-linear-gradient(
    100deg,
    #C4913C 0px,
    #D9A94F 4px,
    #AE7C2C 7px,
    #C4913C 10px,
    #9C6C22 14px
  );
  text-shadow:
    -1px -1px 0 rgba(255, 248, 228, 0.4),
    1px 1px 0 rgba(92, 60, 14, 0.4),
    2px 2px 0 rgba(92, 60, 14, 0.24),
    3px 3px 3px rgba(0, 0, 0, 0.18);
}

/* ------------------------------------------------------------------ menu --
   The "☰ Menu" button sits where the A / A+ / A++ toggle used to, and the
   text-size control has moved inside the panel under Settings. The word
   "Menu" stays next to the glyph: a bare hamburger is not read as a button
   by a good share of the people this app is for.
   The panel itself is built by menu.js, so its markup exists in one place
   rather than four. */

.menu-button {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  width: auto;
  margin-top: 0;
  padding: 9px 15px;
  flex-shrink: 0;
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 600;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  cursor: pointer;
  transition: box-shadow 160ms ease, border-color 160ms ease;
}

.menu-button svg {
  width: 1.15rem;
  height: 1.15rem;
  flex-shrink: 0;
}

/* `background` looks redundant here — it is the same value as the base rule —
   but it is load-bearing. Without it the generic `button:hover` further down
   this file wins and repaints the button deep blue while its dark ink stays,
   leaving "Menu" at 1.9:1 on itself. Touch browsers latch :hover onto the
   last-tapped element, so on a phone it would stay that way. Do not remove. */
.menu-button:hover:not(:disabled) {
  background: var(--surface);
  color: var(--ink);
  border-color: var(--accent-warm);
  box-shadow: var(--glow);
}

/* The open state has to out-rank the hover rule above, and it does not get
   there on its own: after a mouse click the pointer is by definition sitting
   on the button, so :hover is live, and the hover rule's white background
   beat the open state's blue one while the open state's white text stayed —
   white text and a white glyph on a white button. The word "Menu" disappeared
   at the exact moment you pressed it. Caught in a screenshot, not by reading
   the file; leave the second selector in place. */
.menu-button[aria-expanded="true"],
.menu-button[aria-expanded="true"]:hover:not(:disabled) {
  background: var(--selected-bg);
  border-color: var(--selected-bg);
  color: var(--selected-ink);
  box-shadow: none;
}

/* Dims the page behind the open menu. Sits under the header (which is lifted
   above it while open) so the Menu button stays visible and can close what it
   opened. */
.menu-scrim {
  position: fixed;
  inset: 0;
  z-index: 30;
  background: rgba(20, 30, 40, 0.45);
}

body.menu-open { overflow: hidden; }
body.menu-open header.top { z-index: 35; }

/* The panel hangs off the bottom of the sticky header, so the space it has to
   play with is the window MINUS the header — and the header is 78px at normal
   text but 225px at Extra Large on a phone. A flat `max-height: 72vh` ignored
   that and pushed the bottom of the panel off the screen, taking the Close
   button with it. `100%` here is the header's own height, so this tracks it
   whatever the text size.
   The panel is then a column: the list scrolls, the Close button does not. */
.menu-panel {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  /* The 48px keeps a strip of the dimmed page showing beneath the panel, so
     there is always somewhere to tap to dismiss and always a visible hint
     that the app is still back there.
     Two declarations, not one: on phones `100vh` is the height with the
     browser's address bar collapsed, which is TALLER than what you can
     actually see, so a vh-only panel hangs off the bottom of a real iPhone.
     `dvh` is the honest number; the vh line stays underneath it for older
     browsers that would drop the dvh line entirely. */
  max-height: calc(100vh - 100% - 48px);
  max-height: calc(100dvh - 100% - 48px);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow);
}

/* Explicit, because the rules above would otherwise beat the [hidden]
   attribute's default display:none and leave the menu permanently open. */
.menu-panel[hidden],
.menu-scrim[hidden] { display: none; }

.menu-scroll {
  flex: 1 1 auto;
  overflow-y: auto;
  min-height: 0; /* without this a flex item refuses to shrink below its content */
}

.menu-inner {
  max-width: 700px;
  margin: 0 auto;
  padding: 4px 24px 20px;
}

/* Held out of the scrolling area on purpose. At every desktop size the panel's
   content is taller than the space available, so a Close button at the end of
   the list sat below the fold on every screen tested — in a panel that offers
   it as the way out. */
.menu-foot {
  flex: 0 0 auto;
  padding: 14px 24px;
  background: var(--surface);
  border-top: 1px solid var(--border);
}

.menu-foot-inner {
  max-width: 700px;
  margin: 0 auto;
}

.menu-heading {
  font-size: 0.9rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 22px 0 10px;
}

.menu-rows {
  display: grid;
  gap: 10px;
}

.menu-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 14px 16px;
  background: var(--surface-soft);
  border: 1px solid transparent;
  border-radius: 12px;
  text-decoration: none;
  color: var(--ink);
  transition: box-shadow 160ms ease, border-color 160ms ease;
}

a.menu-row:hover,
a.menu-row:focus-visible {
  border-color: var(--accent-warm);
  box-shadow: var(--glow);
}

.menu-row-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.menu-row-label {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--ink);
}

.menu-row-note {
  font-size: 0.82rem;
  color: var(--ink-soft);
}

.menu-row-arrow {
  display: flex;
  color: var(--accent);
  flex-shrink: 0;
}

.menu-row-arrow svg { width: 1.15rem; height: 1.15rem; }

/* The page you are already on. Marked, not narrated: the row used to say
   "You are here" in orange, which read as a statement about the menu you are
   looking at rather than the page underneath it.
   Two rules about how it IS marked, both learned the hard way:
   - The border is 2px dashed --accent-warm, not 1px dashed --border. The base
     row's border is `1px solid transparent`, so a --border dash was measured
     at 1.35:1 against the panel in light mode — under the 3:1 floor this repo
     holds every non-text boundary to. A border nobody can see is not a marker.
   - The row keeps its fill. It was briefly transparent, which made the page
     you are ON the only unfilled row in the list — to this audience that
     reads as "greyed out and unavailable", the opposite of what it means. */
.menu-row--here {
  border: 2px dashed var(--accent-warm);
  /* The 2px border would otherwise make this row 2px taller than its
     neighbours and nudge the whole list. */
  padding: 13px 15px;
}

/* Sits where the other rows carry their chevron. Sized against that chevron
   (1.15rem) rather than picked by eye — it is doing the same job. */
.menu-here-dot {
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 50%;
  background: var(--accent-warm);
  flex-shrink: 0;
}

.menu-setting { margin-top: 2px; }
/* Two settings groups now sit under one "Settings" heading, and without this
   the second label crowds the buttons of the first — they read as one control
   with a stray label. */
.menu-setting + .menu-setting { margin-top: 22px; }

.menu-setting-label {
  margin: 0 0 10px;
  font-weight: 700;
  color: var(--ink);
}

.menu-close {
  width: 100%;
  margin-top: 0;
  padding: 14px;
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
  background: var(--surface-soft);
  border: 1px solid var(--border);
  border-radius: 12px;
  cursor: pointer;
}

.menu-close:hover:not(:disabled) {
  background: var(--surface-soft);
  border-color: var(--accent-warm);
  color: var(--ink);
}

.menu-button:focus-visible,
.menu-close:focus-visible,
a.menu-row:focus-visible,
.text-scale--menu button:focus-visible,
.menu-choice button:focus-visible {
  outline: 3px solid var(--accent-warm);
  outline-offset: 2px;
}

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

.text-scale {
  display: flex;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  flex-shrink: 0;
}

.text-scale button {
  background: var(--surface);
  color: var(--ink);
  border: none;
  cursor: pointer;
  padding: 8px 12px;
  font-family: var(--font-body);
  font-size: 0.85rem;
  font-weight: 700;
  line-height: 1;
}

.text-scale button + button { border-left: 1px solid var(--border); }
.text-scale button[aria-pressed="true"] { background: var(--selected-bg); color: var(--selected-ink); }

/* The same control, in its new home inside the menu — three real choices with
   their names on them. "A++" is a guess you have to make by trying it;
   "Extra Large" is not. Same class and same data-scale attributes, so
   textscale.js drives this without knowing it moved.
   auto-fit means the three boxes drop onto separate lines by themselves once
   the text is scaled up, rather than squeezing "Extra Large" into a sliver. */
/* The brightness choice is the same control in a different hat: same shape,
   same sizes, same chosen-state colours. It gets its own class rather than
   borrowing `.text-scale--menu` because a group of light/dark buttons named
   after text scaling is the kind of thing that reads fine today and confuses
   somebody in six months. */
.text-scale--menu,
.menu-choice {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(6.4rem, 1fr));
  gap: 10px;
  border: none;
  border-radius: 0;
  overflow: visible;
}

.text-scale--menu button,
.menu-choice button {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 7px;
  width: auto;
  margin-top: 0;
  padding: 15px 8px;
  background: var(--surface-soft);
  border: 2px solid var(--border);
  border-radius: 12px;
  color: var(--ink);
  line-height: 1;
}

.text-scale--menu button + button,
.menu-choice button + button { border-left: 2px solid var(--border); }

.menu-size-sample {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--accent);
  line-height: 1;
}

.menu-size-label {
  font-size: 0.8rem;
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
}

/* The second selector is not decoration. `button:hover` later in this file has
   identical specificity to the first selector and would win on order alone,
   repainting the chosen size mid-hover — on the one control in the app whose
   whole purpose is being able to read it. */
.text-scale--menu button[aria-pressed="true"],
.text-scale--menu button[aria-pressed="true"]:hover:not(:disabled),
.menu-choice button[aria-pressed="true"],
.menu-choice button[aria-pressed="true"]:hover:not(:disabled) {
  background: var(--selected-bg);
  border-color: var(--selected-bg);
  color: var(--selected-ink);
}

.text-scale--menu button[aria-pressed="true"] .menu-size-sample,
.text-scale--menu button[aria-pressed="true"]:hover .menu-size-sample,
.menu-choice button[aria-pressed="true"] .menu-size-sample,
.menu-choice button[aria-pressed="true"]:hover .menu-size-sample { color: var(--selected-ink); }

.text-scale--menu button:hover:not([aria-pressed="true"]),
.menu-choice button:hover:not([aria-pressed="true"]) {
  background: var(--surface-soft);
  border-color: var(--accent-warm);
}

/* The sun and moon, sized to sit level with the A/A+/A++ samples opposite and
   coloured like them — including the flip to --selected-ink when chosen, which
   currentColor gives for free. */
.menu-theme-icon {
  display: block;
  color: var(--accent);
  line-height: 0;
}

.menu-theme-icon svg {
  width: 1.5rem;
  height: 1.5rem;
  display: block;
}

.menu-choice button[aria-pressed="true"] .menu-theme-icon,
.menu-choice button[aria-pressed="true"]:hover .menu-theme-icon { color: var(--selected-ink); }

/* First-visit text-size prompt */
.text-scale-overlay {
  position: fixed;
  inset: 0;
  background: rgba(20, 30, 40, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
  padding: 24px;
}

.text-scale-dialog {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 32px 28px;
  max-width: 440px;
  width: 100%;
  text-align: center;
}

.text-scale-dialog p {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 24px;
}

.text-scale-options {
  display: flex;
  gap: 12px;
  justify-content: center;
}

.text-scale-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  width: auto;
  flex: 1;
  margin-top: 0;
  padding: 20px 10px;
  border: 2px solid var(--border);
  border-radius: 12px;
  background: var(--surface-soft);
  color: var(--ink);
  cursor: pointer;
}

.text-scale-option:hover { border-color: var(--accent); background: var(--surface-soft); }

.text-scale-option .preview {
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--accent);
  line-height: 1;
}

.text-scale-option .label {
  font-family: var(--font-body);
  font-size: 0.85rem;
  font-weight: 700;
}

/* Lives inside the sticky header on sub-pages, so it stays reachable no
   matter how far down a long page (like the glossary) you've scrolled. */
.top-back {
  max-width: 700px;
  margin: 10px auto 0;
  padding: 0 24px;
}

a.back-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* --accent-strong, not --accent: at 18px bold this is just under the size
     that counts as "large text", so it needs 4.5:1 and --accent gives 3.9. */
  color: var(--accent-strong);
  font-size: 0.9rem;
  font-weight: 700;
  text-decoration: none;
}

a.back-link svg { width: 1.1rem; height: 1.1rem; }
a.back-link:hover { text-decoration: underline; }

label {
  display: block;
  font-weight: bold;
  margin-bottom: 10px;
}

textarea {
  width: 100%;
  min-height: 9rem;
  font-family: inherit;
  font-size: 1rem;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--ground);
  color: var(--ink);
  resize: vertical;
  line-height: 1.55;
}

textarea:focus {
  outline: 3px solid var(--accent);
  outline-offset: 1px;
}

button, a.button {
  display: block;
  width: 100%;
  margin-top: 18px;
  padding: 16px;
  font-size: 1.05rem;
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--button-ink);
  background: var(--button-bg);
  border: none;
  border-radius: 12px;
  cursor: pointer;
  text-align: center;
  text-decoration: none;
}

/* Hover darkens in the light theme and lightens in the dark one — either way
   it moves AWAY from the ink, so the label never gets harder to read on the
   way to being pressed. */
button:hover:not(:disabled), a.button:hover { background: var(--accent-strong); }
:root[data-theme="dark"] button:hover:not(:disabled),
:root[data-theme="dark"] a.button:hover { color: var(--button-ink); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) button:hover:not(:disabled),
  :root:not([data-theme="light"]) a.button:hover { color: var(--button-ink); }
}
button:disabled { opacity: 0.55; cursor: not-allowed; }

.status {
  margin-top: 18px;
  font-size: 0.95rem;
  color: var(--ink-soft);
}

/* THE STANDING NOTICE, shown with every result.
   Quieter than the answer above it, but NOT small print: this is the sentence
   that says the app can be wrong, on a product whose users are older and whose
   wrong answers cost money. Anything a 78-year-old cannot read is not a
   disclaimer, it is decoration. Same size as the body text, only in the softer
   ink, separated by a rule rather than by shrinking it. */
.result-notice {
  margin: 24px 0 0;
  padding-top: 18px;
  border-top: 1px solid var(--border);
  font-size: 1rem;
  line-height: 1.55;
  color: var(--ink-soft);
}

.result {
  margin-top: 28px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
  display: none;
}

.result.visible { display: block; }

/* THE QUICK-LOOK BLOCK HAS NO COLOURED BANNER, AND THAT IS THE DESIGN.
   The three banner colours on this site mean something specific: green is "we
   looked and found nothing", amber is "worth a closer look", red is "we think
   this is a scam". The quick look is entitled to none of those — it is a list
   of things the word-patterns recognized, with no judgement attached — so it
   gets a plain heading on the ordinary surface instead. Giving it a colour
   would be the whole problem: an amber bar reads as a verdict whatever the
   words underneath it say. */
.quick-head {
  padding: 18px 24px 0;
}

.quick-head h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.35rem;
  color: var(--ink);
}

.quick-sub {
  margin: 6px 0 0;
  color: var(--ink-soft);
}

.quick-cta {
  margin: 22px 0 0;
}

.verdict-banner {
  padding: 20px 24px;
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  color: #fff;
}

.verdict-banner.safe { background: var(--state-safe); }
.verdict-banner.caution { background: var(--state-caution); }
.verdict-banner.scam { background: var(--state-danger); }

.result-body {
  padding: 20px 24px;
  background: var(--surface);
}

.result-body h2 {
  font-size: 1.05rem;
  margin: 0 0 10px;
}

.result-body ul {
  margin: 0 0 20px;
  padding-left: 24px;
}

.result-body li {
  margin-bottom: 8px;
}

.advice {
  font-size: 1rem;
}

.error-banner {
  margin-top: 20px;
  padding: 16px 20px;
  background: var(--state-danger-bg);
  border: 1px solid var(--state-danger);
  border-radius: 12px;
  color: var(--state-danger);
  display: none;
}

.error-banner.visible { display: block; }

footer {
  margin-top: 40px;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

footer p { margin: 0 0 6px; }

footer .tagline {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 1rem;
  font-style: italic;
  color: var(--accent-strong);
}

.lamp-icon {
  width: 34px;
  height: 34px;
  flex-shrink: 0;
}

/* Dashboard nav cards */
/* --------------------------------------------------------- the dashboard --
   THREE IDENTICAL CARDS IN A COLUMN IS NOT A DASHBOARD, it is a list wearing
   a costume. "Check a Message" — the thing somebody opens this app at eleven
   at night to do — looked exactly like "Glossary". Hierarchy is the whole
   change: one card that is unmistakably the way in, and everything else
   arranged around it and clearly secondary. */

.hero {
  margin: 0 0 26px;
}

.hero h1 {
  font-size: clamp(1.7rem, 1.3rem + 2.2vw, 2.3rem);
  line-height: 1.15;
  margin: 0 0 10px;
  color: var(--ink);
}

.hero .subtitle {
  margin: 0;
  font-size: 1.05rem;
  color: var(--ink-soft);
  max-width: 34em;
}

.nav-cards {
  display: grid;
  gap: 16px;
  margin-top: 8px;
  /* Two across as soon as there is room, one column on a phone.
     `min(15rem, 100%)` rather than a bare `15rem`: at Extra Large text one rem
     is 27px, so 15rem is 408px — wider than a 390px phone — and a minmax whose
     minimum exceeds the container does not wrap, it overflows. It pushed 60px
     of the dashboard off the side of the screen at exactly the text size this
     app exists to support. */
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
}

/* The way in. Full width, warm, and lit — it should be obvious at arm's
   length which of these is the front door. */
.nav-card--primary {
  grid-column: 1 / -1;
  background-image:
    radial-gradient(120% 140% at 12% -10%, rgba(255, 209, 102, 0.22), rgba(255, 209, 102, 0) 62%);
  border-color: var(--hairline-warm);
  padding: 30px 26px;
}

.nav-card--primary h2 { font-size: 1.5rem; }

/* Specificity, on purpose: the generic `.nav-card .icon` rule is further down
   this file and would otherwise win on source order and paint the front door
   the same blue as everything else. */
.nav-card.nav-card--primary .icon {
  background: var(--accent-warm-soft);
  width: 3.2rem;
  height: 3.2rem;
}
.nav-card.nav-card--primary .icon svg { color: var(--accent-warm); width: 1.7rem; height: 1.7rem; }

.nav-card {
  display: block;
  padding: 24px 22px;
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  text-decoration: none;
  color: var(--ink);
  transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
}

/* The cards arrive rather than appearing. Small distance, quick, and
   staggered just enough to read as one movement instead of six. Everything
   here is switched off under prefers-reduced-motion, at the bottom of this
   file — motion sickness is common in exactly this age group and the
   setting exists to be obeyed. */
@keyframes card-rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

.nav-cards > * {
  animation: card-rise 420ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
}
.nav-cards > :nth-child(1) { animation-delay: 0ms; }
.nav-cards > :nth-child(2) { animation-delay: 60ms; }
.nav-cards > :nth-child(3) { animation-delay: 120ms; }
.nav-cards > :nth-child(4) { animation-delay: 180ms; }
.nav-cards > :nth-child(5) { animation-delay: 240ms; }
.nav-cards > :nth-child(6) { animation-delay: 300ms; }

/* Hovering or tabbing to a card turns the porch light on. */
.nav-card:hover,
.nav-card:focus-visible {
  transform: translateY(-2px);
  box-shadow: var(--glow);
  border-color: var(--accent-warm);
}

.nav-card:focus-visible {
  outline: 2px solid var(--accent-warm);
  outline-offset: 3px;
}

.nav-card:hover .icon,
.nav-card:focus-visible .icon {
  background: var(--accent-warm-soft);
}

.nav-card:hover .icon svg,
.nav-card:focus-visible .icon svg {
  color: var(--accent-warm);
}

/* EVERY PIECE OF MOTION IN THE APP TURNS OFF HERE.
   Not damped — off. Vestibular trouble is common in the age group this is
   built for, and somebody who has set this preference has already told their
   device that movement makes them unwell. The animations are decoration; the
   app must be identical without them. */
@media (prefers-reduced-motion: reduce) {
  .nav-card { transition: box-shadow 160ms ease, border-color 160ms ease; }
  .nav-card:hover,
  .nav-card:focus-visible { transform: none; }

  .nav-cards > *,
  .brand-lamp .lamp-halo,
  .scene-stage *,
  /* Named explicitly as well as by descendant. These live inside a `<use>`
     shadow tree, and while a class rule reaches in there, it is not worth
     betting somebody's comfort on a descendant selector crossing that
     boundary. Belt and braces, and the filmstrip test proves it. */
  .an-eye,
  .an-shine,
  .ch-alive,
  .ch-lean {
    animation: none !important;
  }
  .nav-cards > * { opacity: 1; transform: none; }
}

/* ----------------------------------------------------------- the cartoons --
   Each lesson opens with a short scene that PLAYS OUT THE SCAM in four beats,
   with one line of caption under each. Not a looping icon — a little story
   with a beginning and an end, which is how anybody actually learns what one
   of these calls feels like.

   THREE RULES DECIDED BEFORE ANY OF IT WAS DRAWN.

   1. IT PLAYS ONCE, when it scrolls into view, and then stops on the last
      beat. A scene looping forever beside a paragraph makes the paragraph
      harder to read, and this is a page people read slowly. There is a
      "Watch again" button for anybody who wants it.
   2. THE CAPTIONS ARE THE SCENE. Every beat's line is real text in the page,
      so the story survives with no JavaScript, no motion, and a screen
      reader — the pictures illustrate the words rather than carrying them.
   3. NOTHING FLASHES, and nothing moves faster than an eye can follow.

   With JavaScript off, or under prefers-reduced-motion, the figure shows its
   final frame and all four captions as a numbered list. That is a comic
   strip, which is a perfectly good way to tell the same story.  */

.lesson-scene {
  margin: 0 0 18px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface-soft);
  overflow: hidden;
}

.scene-stage {
  display: block;
  width: 100%;
  height: auto;
  background:
    radial-gradient(120% 90% at 50% -20%, rgba(255, 209, 102, 0.16), rgba(255, 209, 102, 0) 65%);
}

/* Every beat is stacked in the same place and hidden until it is the current
   one. `visibility` as well as opacity so a hidden beat is not focusable or
   readable while it is off. */
.scene-stage .beat {
  opacity: 0;
  visibility: hidden;
  transition: opacity 420ms ease;
}

/* One rule rather than a pair per beat. The first version matched
   `[data-current="3"] .beat[data-beat="3"]` and so on, which fixed the story
   at exactly four panels — and the longer lessons need six or eight. */
.scene-stage .beat.is-current {
  opacity: 1;
  visibility: visible;
}

.scene-captions {
  margin: 0;
  padding: 14px 18px 16px;
  border-top: 1px solid var(--border);
  display: flex;
  align-items: baseline;
  gap: 12px;
  min-height: 3.4em;
}

.scene-captions b {
  flex: 0 0 auto;
  font-family: var(--font-display);
  color: var(--accent-strong);
  font-size: 0.95rem;
}

.scene-captions span {
  display: none;
  font-size: 1rem;
  line-height: 1.45;
}

.scene-captions span.is-current { display: inline; }

/* The last beat is the lesson, so it is allowed to look like one. */
.scene-captions span.is-last.is-current {
  font-weight: 700;
  color: var(--accent-strong);
}

.scene-replay {
  display: none;
  width: auto;
  margin: 0 18px 16px;
  padding: 9px 16px;
  font-size: 0.9rem;
  font-family: var(--font-body);
  font-weight: 700;
  background: var(--surface);
  color: var(--accent-strong);
  border: 2px solid var(--border);
  border-radius: 10px;
  cursor: pointer;
}

.scene-replay:hover { border-color: var(--accent-warm); }
.scene-replay:focus-visible { outline: 3px solid var(--accent-warm); outline-offset: 2px; }
.lesson-scene[data-done="yes"] .scene-replay { display: inline-block; }

/* NO SCRIPT, OR NO MOTION WANTED: the strip, all at once.
   `:not([data-current])` is the no-script case — nothing ever set the
   attribute — and the media query covers somebody who asked for stillness. */
.lesson-scene:not([data-current]) .beat.is-last { opacity: 1; visibility: visible; }
.lesson-scene:not([data-current]) .scene-captions { display: block; }
.lesson-scene:not([data-current]) .scene-captions span { display: list-item; margin-left: 1.1em; }
.lesson-scene:not([data-current]) .scene-captions b { display: none; }

@media (prefers-reduced-motion: reduce) {
  .lesson-scene .beat { transition: none; }
  .lesson-scene .beat.is-last { opacity: 1; visibility: visible; }
  .lesson-scene .scene-captions { display: block; }
  .lesson-scene .scene-captions span { display: list-item; margin-left: 1.1em; font-weight: 400; }
  .lesson-scene .scene-captions b,
  .lesson-scene .scene-replay { display: none; }
}

/* --- movement inside a beat ------------------------------------------------
   A beat is not a still frame. The phone shakes while it rings, the cards
   slide out of the rack, the numbers lift off the card and leave. These only
   run while their beat is the current one — `animation-play-state` is paused
   by default and released by `.is-current`, so nothing is animating off-stage
   and every beat starts its motion from the beginning when it arrives. */
.scene-stage [class*="sc-anim"] { animation-play-state: paused; }
.scene-stage .beat.is-current [class*="sc-anim"] { animation-play-state: running; }

@keyframes sc-shake {
  0%, 100% { transform: rotate(0deg); }
  20% { transform: rotate(-4deg); }
  40% { transform: rotate(4deg); }
  60% { transform: rotate(-3deg); }
  80% { transform: rotate(2deg); }
}
@keyframes sc-pulse {
  0%, 100% { opacity: 0.25; }
  50%      { opacity: 1; }
}
@keyframes sc-rise {
  0%   { opacity: 0; transform: translateY(8px); }
  30%  { opacity: 1; transform: translateY(0); }
  100% { opacity: 1; transform: translateY(0); }
}
@keyframes sc-drift-away {
  0%   { opacity: 1; transform: translate(0, 0); }
  70%  { opacity: 0.5; transform: translate(34px, -16px); }
  100% { opacity: 0; transform: translate(52px, -26px); }
}
@keyframes sc-slide-out {
  0%   { transform: translateY(0); }
  55%  { transform: translateY(-14px); }
  100% { transform: translateY(-14px); }
}
@keyframes sc-glow-grow {
  0%   { opacity: 0; transform: scale(0.7); }
  60%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0.85; transform: scale(1.03); }
}
@keyframes sc-nod {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-3px); }
}

/* PLACEMENT AND ANIMATION GO ON DIFFERENT ELEMENTS.
   A `transform` in a keyframe REPLACES the element's `transform` attribute
   rather than composing with it, so `<g class="sc-anim-nod" transform="...">`
   loses its position the moment the animation starts. Wrap instead:
   `<g transform="..."><g class="sc-anim-nod">…</g></g>`. */
/* --- the choreography ------------------------------------------------------
   A cartoon is not a slideshow of stills with a fade between them. Things
   arrive, overshoot, settle, react and leave. Each of these is written to run
   ONCE when its beat becomes current (the play-state rule above releases it),
   so the timing inside a panel is real timing and not a loop that happened to
   be mid-cycle when the panel appeared. */

/* Anticipate, then arrive. The little dip before a rise is what makes a
   movement look intended rather than teleported. */
@keyframes sc-arrive-left {
  0%   { opacity: 0; transform: translateX(-40px) rotate(-6deg); }
  60%  { opacity: 1; transform: translateX(6px) rotate(2deg); }
  100% { opacity: 1; transform: translateX(0) rotate(0deg); }
}
@keyframes sc-arrive-right {
  0%   { opacity: 0; transform: translateX(40px) rotate(6deg); }
  60%  { opacity: 1; transform: translateX(-6px) rotate(-2deg); }
  100% { opacity: 1; transform: translateX(0) rotate(0deg); }
}
@keyframes sc-stamp {
  0%   { opacity: 0; transform: scale(1.9); }
  55%  { opacity: 1; transform: scale(0.92); }
  75%  { transform: scale(1.04); }
  100% { opacity: 1; transform: scale(1); }
}
@keyframes sc-slump {
  0%   { transform: translateY(0) rotate(0deg); }
  100% { transform: translateY(6px) rotate(-4deg); }
}
@keyframes sc-brighten {
  0%   { transform: translateY(4px) scale(0.96); }
  55%  { transform: translateY(-8px) scale(1.04); }
  100% { transform: translateY(0) scale(1); }
}
@keyframes sc-tick { to { transform: rotate(360deg); } }
@keyframes sc-arc-away {
  0%   { opacity: 1; transform: translate(0, 0) rotate(0deg); }
  45%  { opacity: 1; transform: translate(30px, -26px) rotate(120deg); }
  100% { opacity: 0; transform: translate(78px, 6px) rotate(300deg); }
}
@keyframes sc-jitter {
  0%, 100% { transform: translate(0, 0); }
  25% { transform: translate(-1.5px, 1px); }
  50% { transform: translate(1.5px, -1px); }
  75% { transform: translate(-1px, -1.5px); }
}

.sc-anim-arrive-l { animation: sc-arrive-left 620ms cubic-bezier(0.2, 0.8, 0.3, 1.1) both; }
.sc-anim-arrive-r { animation: sc-arrive-right 620ms cubic-bezier(0.2, 0.8, 0.3, 1.1) both; }
.sc-anim-stamp    { animation: sc-stamp 520ms cubic-bezier(0.2, 0.9, 0.3, 1.2) 180ms both; }
.sc-anim-slump    { animation: sc-slump 1400ms ease-in-out 600ms both; }
.sc-anim-bright   { animation: sc-brighten 900ms cubic-bezier(0.2, 0.8, 0.3, 1.2) 200ms both; }
.sc-anim-tick     { animation: sc-tick 4s steps(8, end) infinite; transform-box: fill-box; transform-origin: 50% 100%; }
.sc-anim-arc      { animation: sc-arc-away 2400ms cubic-bezier(0.3, 0, 0.6, 1) 500ms both; }
.sc-anim-jitter   { animation: sc-jitter 260ms linear infinite; }
/* KEYFRAMES USED BY THE CHOREOGRAPHY BELOW.
   These once had matching `.ch-startle` and `.ch-hop-in` classes. Those classes
   were never applied to anything — the scenes use `.sc-anim-startle` and
   `.sc-anim-hop` — so the classes went, and taking the @keyframes with them
   silently killed two live animations. The names live here, next to what uses
   them, so that cannot happen again. */
@keyframes ch-startle {
  0%   { transform: scale(1, 1) translateY(0); }
  30%  { transform: scale(1.1, 0.9) translateY(3px); }
  60%  { transform: scale(0.96, 1.08) translateY(-5px); }
  100% { transform: scale(1, 1) translateY(0); }
}
@keyframes ch-hop-in {
  0%   { opacity: 0; transform: translateY(26px) scale(0.9, 1.1); }
  55%  { opacity: 1; transform: translateY(-7px) scale(1.05, 0.95); }
  100% { opacity: 1; transform: translateY(0) scale(1, 1); }
}

.sc-anim-hop      { animation: ch-hop-in 640ms cubic-bezier(0.2, 0.8, 0.3, 1.1) 260ms both; }
.sc-anim-startle  { animation: ch-startle 520ms cubic-bezier(0.2, 0.9, 0.3, 1.4) 200ms both; }

/* Staggering, so a group of things does not move as one slab. */
.sc-d1 { animation-delay: 140ms; }
.sc-d2 { animation-delay: 300ms; }
.sc-d3 { animation-delay: 460ms; }

.sc-anim-shake  { animation: sc-shake 900ms ease-in-out infinite; }
.sc-anim-pulse  { animation: sc-pulse 1200ms ease-in-out infinite; }
.sc-anim-rise   { animation: sc-rise 700ms ease-out both; }
.sc-anim-rise-2 { animation: sc-rise 700ms ease-out 320ms both; }
.sc-anim-away   { animation: sc-drift-away 2200ms ease-in 500ms both; }
.sc-anim-slide  { animation: sc-slide-out 1600ms ease-in-out 400ms both; }
.sc-anim-glow   { animation: sc-glow-grow 1800ms ease-out both; }
.sc-anim-nod    { animation: sc-nod 2600ms ease-in-out infinite; }

/* The drawing vocabulary, shared by every scene so seven cartoons look like
   one hand drew them. */
.sc-ink    { stroke: var(--ink); stroke-width: 3; fill: none; stroke-linecap: round; stroke-linejoin: round; }
.sc-blue   { stroke: var(--accent); stroke-width: 3; fill: none; stroke-linecap: round; stroke-linejoin: round; }
.sc-blue-f { fill: var(--accent); }
.sc-warm   { stroke: var(--accent-warm); stroke-width: 3; fill: none; stroke-linecap: round; stroke-linejoin: round; }
.sc-warm-f { fill: var(--accent-warm); }
.sc-red    { stroke: var(--state-danger); stroke-width: 3; fill: none; stroke-linecap: round; stroke-linejoin: round; }
.sc-red-f  { fill: var(--state-danger); }
.sc-paper  { fill: var(--surface); stroke: var(--ink); stroke-width: 2.5; }
.sc-soft   { fill: var(--surface-soft); }
.sc-dim    { fill: var(--ink-soft); }

/* --- the cast --------------------------------------------------------------
   THE LESSONS ARE ACTED BY ANIMALS, not by people. A drawing of an older
   person being talked out of their savings is uncomfortable to look at when
   you ARE the older person — the reader ends up cast as the victim. A penguin
   answering the phone is warm, it is funny, and it is far easier to remember
   a week later, which is the entire point of a lesson.

   Their colours are their own rather than the theme's: a character whose
   belly turns dark in dark mode is not a character. Every one of them carries
   an outline so the shapes hold on either ground. */
.an-outline { stroke: #2B3A47; stroke-width: 2.5; stroke-linejoin: round; }
.an-slate   { fill: #3E5265; }
.an-slate-d { fill: #2B3A47; }
.an-cream   { fill: #F7F1E4; }
.an-beak    { fill: #E8963C; }
.an-gray    { fill: #A3B0BC; }
.an-gray-d  { fill: #7D8C9B; }
.an-nose    { fill: #2B3A47; }
.an-tan     { fill: #D9B98A; }
.an-tan-d   { fill: #B8945F; }
.an-pink    { fill: #E39898; }
.an-white   { fill: #FBF8F2; }
.an-eye     { fill: #22303C; }
.an-shine   { fill: #FFFFFF; }
.an-scope   { fill: #FFFFFF; }

/* --- BRINGING THEM TO LIFE -------------------------------------------------
   Two facts about `<use>`, both established by rendering a probe rather than
   by hoping, and together they are what makes a rigged character possible
   without duplicating the artwork nine times:

     1. CLASS RULES IN THIS STYLESHEET DO REACH INSIDE A `<use>`. A rule for
        `.an-eye` styles the eyes of every instance of every character.
     2. CUSTOM PROPERTIES INHERIT INTO A `<use>`, so `style="--lean:-6deg"` on
        one instance moves that one and no other.

   So: the shared rules below give every character a heartbeat — breathing and
   blinking — and the per-instance variables let a scene pose one of them
   without touching the sprite.

   Everything here is inside the reduced-motion switch-off at the bottom of the
   file. A page of animals breathing at somebody who asked for stillness would
   be worse than a page of animals not breathing. */

/* THE BLINK.
   The eye squashes to a sliver rather than a lid coming down over it. A lid
   would have to be painted the same colour as the face behind it, and there
   are five different face colours in this cast; squashing the eye itself
   needs no colour at all and reads exactly the same at this size.
   `transform-box: fill-box` is essential — without it the origin is the whole
   SVG's viewBox and the eye flies off sideways. */
.an-eye,
.an-shine {
  transform-box: fill-box;
  transform-origin: center;
  animation: ch-blink 7s ease-in-out infinite;
  animation-delay: var(--blink-delay, 0s);
}

@keyframes ch-blink {
  0%, 92%, 100% { transform: scaleY(1); }
  95%           { transform: scaleY(0.08); }
  98%           { transform: scaleY(1); }
}

/* THE BREATH. Slow, and only a couple of percent — it should be felt rather
   than seen. Anchored at the feet so they do not bob off the ground. */
.ch-alive {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: ch-breathe 4s ease-in-out infinite;
  animation-delay: var(--idle-delay, 0s);
}

@keyframes ch-breathe {
  0%, 100% { transform: scale(1, 1); }
  50%      { transform: scale(1.015, 1.03); }
}

/* POSES, set per instance from a scene: a lean, a droop of the ears, a turn.
   `--lean` tips the whole character; a scene uses a couple of degrees for
   interest and eight or ten for alarm. */
.ch-lean {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  transform: rotate(var(--lean, 0deg));
}

/* Ears. Dogs' ears carry most of their mood, and Tilly's going back is
   written into who he is. */
.an-ear-l {
  transform-box: fill-box;
  transform-origin: 80% 10%;
  transform: rotate(calc(var(--ears, 0) * -18deg));
}
.an-ear-r {
  transform-box: fill-box;
  transform-origin: 20% 10%;
  transform: rotate(calc(var(--ears, 0) * 18deg));
}

/* WHAT WAS HERE, AND WHY ITS ABSENCE MATTERED.
   Two orphaned @keyframes BODIES sat here — the percentage steps of ch-startle
   and ch-hop-in with their `@keyframes name {` opening lines missing, left
   behind by a botched edit. Both animations are defined properly further up
   (line ~1380) and were working; these were dead duplicates.

   Dead, but not harmless. `55% { ... }` is not a valid selector, so the CSS
   parser error-recovers by skipping to the next closing brace — and the stray
   brace after it consumed THE NEXT RULE. That rule was `.an-fawn`.

   The visible result, live on the site: **Tilly rendered BLACK instead of
   fawn**, because his body fill silently fell back to the SVG default of
   black. A missing rule does not throw and does not log; it just quietly
   makes a character the wrong colour, and it survived every test we have,
   because nothing measures the fill of a cartoon.

   Found 3 Sep while adding the ten new cast members — Doonie is also fawn,
   came out black, and chasing HER bug turned up Tilly's. */

.an-fawn    { fill: #D9A87A; }
.an-fawn-d  { fill: #B8834F; }
.an-butter  { fill: #F2C94C; }
.an-blanket { fill: #AFC3D8; }
.an-purple  { fill: #8E7BB5; }
.an-purple-d{ fill: #6E5C93; }
.an-lilac   { fill: #EDE6F7; }
.an-gold    { fill: #E8B96B; }
.an-gold-d  { fill: #C9974A; }

/* TWO NEW COLOUR FAMILIES, BOTH CALLED FOR BY THE CAST DOCUMENT.
   Rust went to Cilly the coyote — "instantly distinct from every dog in a
   line-up", which is the whole reason that slot was kept open. Blue-grey is
   Lily's: the coat pitbull people call blue, and it has to read as COOL next
   to Tilly's fawn and cooler than Biggle and Wally's warm grey, or the second
   pitbull just looks like the first one in bad light. */
.an-rust    { fill: #B5643C; }
.an-rust-d  { fill: #8E4A2A; }
.an-blue    { fill: #7E90A6; }
.an-blue-d  { fill: #5F7186; }

/* The stage is taller than a strip of icons because there are people in it. */
.scene-stage { min-height: 0; }

/* Lesson progress, shown on the dashboard's Lessons card. Hidden until the
   script confirms there is something to report. */
.card-progress {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 14px;
}

.card-progress[hidden] { display: none; }

.card-progress-track {
  flex: 1;
  height: 7px;
  border-radius: 999px;
  background: var(--surface-soft);
  overflow: hidden;
}

.card-progress-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--accent-warm);
  transition: width 400ms ease;
}

.card-progress-label {
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--ink-soft);
  white-space: nowrap;
}

.card-progress.complete .card-progress-fill { background: var(--state-safe); }
.card-progress.complete .card-progress-label { color: var(--state-safe); }

@media (prefers-reduced-motion: reduce) {
  .card-progress-fill { transition: none; }
}

.nav-card .icon {
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-soft);
  margin-bottom: 12px;
}

.nav-card .icon svg { width: 1.5rem; height: 1.5rem; color: var(--accent); }

.nav-card h2 {
  margin: 0 0 6px;
  font-size: 1.3rem;
  color: var(--ink);
}

.nav-card p {
  margin: 0;
  color: var(--ink-soft);
  font-size: 0.95rem;
}

/* Lessons page */
.progress-summary {
  margin: 0 0 28px;
  padding: 16px 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  font-size: 1rem;
  font-weight: bold;
}

.lesson-list {
  display: grid;
  /* minmax(0, 1fr), not 1fr: a grid item will not shrink below the widest
     unbreakable thing inside it, so at Extra Large text one long word in a
     heading made the CARD 416px wide inside a 390px phone and pushed the
     whole page sideways. */
  grid-template-columns: minmax(0, 1fr);
  gap: 18px;
  list-style: none;
  margin: 0;
  padding: 0;
}

.lesson-card {
  min-width: 0;
  padding: 22px 24px;
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

/* And the words themselves have to be allowed to break. At Extra Large text
   "Impersonating" alone is 268px, wider than the column it sits in.
   `anywhere`, not `break-word`: only `anywhere` also reduces the element's
   min-content width, which is what a flex or grid parent measures. With
   `break-word` the word simply painted past its own box — no element was ever
   wider than the card, and 25px of the page hung off the side of the phone
   with nothing to blame for it. */
.lesson-card h2,
.nav-card h2 { overflow-wrap: anywhere; }

.lesson-card.done { border-color: var(--state-safe); background-color: var(--state-safe-bg); }

.lesson-card h2 {
  margin: 0 0 10px;
  font-size: 1.2rem;
  color: var(--ink);
  display: flex;
  align-items: center;
  gap: 10px;
}

.lesson-card p {
  margin: 0 0 14px;
  color: var(--ink);
}

.lesson-check {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 1rem;
  font-weight: bold;
  cursor: pointer;
}

.lesson-check input {
  width: 22px;
  height: 22px;
  cursor: pointer;
}

.retention-hook {
  margin: 0 0 16px;
  padding: 14px 18px;
  background: var(--accent-warm-soft);
  border-left: 4px solid var(--accent-warm);
  border-radius: 6px;
}

.hook-line {
  margin: 0 0 6px;
  font-weight: bold;
  color: var(--accent-warm);
}

.hook-question {
  margin: 0;
  font-style: italic;
  color: var(--ink);
}

.reveal-hook-btn {
  display: inline-block;
  width: auto;
  margin: 0 0 16px;
  padding: 12px 18px;
  font-family: var(--font-body);
  font-size: 0.95rem;
  font-weight: bold;
  /* --ink on the soft warm panel rather than the warm accent on it: the warm
     pair is 2.99:1, which is under the floor for text this size even before
     anyone enlarges it. The dashed warm border still carries the colour. */
  color: var(--ink);
  background: var(--accent-warm-soft);
  border: 2px dashed var(--accent-warm);
  border-radius: 8px;
  cursor: pointer;
}

.reveal-hook-btn:hover { filter: brightness(0.97); }

.lesson-check input:disabled {
  cursor: not-allowed;
}

.lesson-check:has(input:disabled) {
  color: var(--ink-soft);
  cursor: not-allowed;
}

.lesson-check-hint {
  margin: 4px 0 0;
  font-size: 0.85rem;
  color: var(--ink-soft);
  font-style: italic;
}

/* Glossary page */

/* The search box's own name, visible and staying visible. See the comment on
   the markup in glossary.html for why a placeholder was not enough. */
.glossary-label {
  display: block;
  margin-bottom: 6px;
  font-weight: 700;
  color: var(--ink);
}

.glossary-search {
  width: 100%;
  /* Safari draws type="search" as its own rounded capsule with its own font and
     padding, and ignores most of what follows unless this is turned off. */
  -webkit-appearance: none;
  appearance: none;
  font-family: inherit;
  font-size: 1.05rem;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--ground);
  color: var(--ink);
  margin-bottom: 8px;
}

.glossary-search:focus {
  outline: 3px solid var(--accent);
  outline-offset: 1px;
}

.glossary-empty {
  display: none;
  margin: 16px 0;
  color: var(--ink-soft);
  font-style: italic;
}

.glossary-list {
  margin-top: 20px;
}

.glossary-term {
  padding: 16px 0;
  border-bottom: 1px solid var(--border);
}

.glossary-term:last-child { border-bottom: none; }

.glossary-term h3 {
  margin: 0 0 6px;
  font-size: 1.05rem;
  color: var(--ink);
}

.glossary-term p {
  margin: 0;
  color: var(--ink-soft);
}

/* ARRIVING FROM A LINK, YOU HAVE TO BE ABLE TO SEE WHICH ONE YOU CAME FOR.
   A lesson can now link straight to a definition. The browser scrolls the
   right term to the top and then gives no sign whatever which of the
   seventy-seven it stopped at — and on a page of near-identical rows, at the
   text size a lot of these readers use, "somewhere near the top" is not an
   answer. The highlight fades after a few seconds so the page does not stay
   permanently marked up. */
.glossary-term:target {
  background: var(--lamp-halo);
  border-radius: 10px;
  padding: 16px 14px;
  margin: 0 -14px;
  animation: glossary-found 4s ease-out 1 both;
}

@keyframes glossary-found {
  0%, 55% { background: var(--lamp-halo); }
  100% { background: transparent; }
}

/* Somebody who asked for stillness gets the highlight without the fade —
   the point is knowing where you landed, and that survives losing the
   animation. */
@media (prefers-reduced-motion: reduce) {
  .glossary-term:target { animation: none; }
}

/* ===========================================================================
   SAVE MY PROGRESS  (save-progress.html)
   ===========================================================================

   READ THIS BEFORE ADDING A RULE HERE.

   Near the top of the buttons section above there are two generic rules:
       button                          { background: var(--accent) }
       button:hover:not(:disabled)     { background: var(--accent-strong) }
   They are specific enough (0,1,1 and 0,2,1) to beat most single-class rules
   written below them, and they set BACKGROUND BUT NOT COLOUR.

   So a rule like `.button-quiet { background: transparent; color: blue }`
   keeps its blue text and loses its background to the generic hover the
   moment the pointer touches it — and on a touch screen :hover latches onto
   the last-tapped element and stays. This has now bitten three times: the
   Menu button, the selected text-size button, and the quiet buttons below.

   The rule that prevents it: any state that sets `color` must also set
   `background`, and must be paired with its own `:hover` variant so it
   out-ranks the generic one. Both are done below. Do the same for anything
   new. And never `color: #fff` on `var(--accent)` — in dark mode --accent is
   a pale blue and white on it measures 2.3:1. Use --selected-bg/--selected-ink.
   ------------------------------------------------------------------------- */

/* A hidden step must stay hidden. `.card-progress[hidden]` above exists
   because something in this file already beat the browser default once. */
main section[hidden] { display: none !important; }

input[type="email"],
input[type="text"] {
  width: 100%;
  font-family: inherit;
  font-size: 1.05rem;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--ground);
  color: var(--ink);
  line-height: 1.4;
}

input[type="email"]:focus,
input[type="text"]:focus {
  outline: 3px solid var(--accent);
  outline-offset: 1px;
}

/* The code box. Wide-spaced monospace at nearly twice body size, because the
   person reading it is copying six digits off a phone screen held at arm's
   length. `ch` units rather than a fixed width so it grows with the text-size
   setting instead of clipping at Extra Large. */
.code-input {
  font-family: 'Courier New', Courier, monospace;
  font-size: 1.9rem;
  font-weight: bold;
  letter-spacing: 0.35em;
  text-align: center;
  /* The trailing letter-spacing on the last character pushes the visual
     centre left; the padding-left compensates. */
  padding-left: calc(16px + 0.35em);
  /* 11ch was measured as too narrow at every text size — six digits clipped.
     letter-spacing adds 0.35em PER CHARACTER on top of each glyph's own
     width, which `ch` does not account for, so the box needs roughly
     6 x (1ch + 0.35em) plus the padding. 16ch measures comfortably at all
     three text sizes on a 360px phone. */
  max-width: 16ch;
  margin: 0 auto;
  display: block;
}

/* The two reassurance panels. Deliberately not styled as warnings — this is
   the calm bit of the page, and a red box would undo it. */
.reassure {
  margin: 0 0 26px;
  padding: 18px 20px;
  background: var(--surface-soft);
  border: 1px solid var(--border);
  border-radius: 12px;
}

.reassure p { margin: 0 0 10px; }
.reassure p:last-child { margin-bottom: 0; }

/* The one on the code screen IS a warning — it is the sentence that protects
   somebody months later, when a stranger phones and asks them to read out a
   number. It gets the caution colour so it is the thing the eye lands on. */
.reassure--warn {
  margin-top: 28px;
  background: var(--state-caution-bg);
  border-color: var(--state-caution);
}

.signed-in-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0 0 22px;
  padding: 18px 20px;
  background: var(--state-safe-bg);
  border: 2px solid var(--state-safe);
  border-radius: 12px;
  color: var(--state-safe);
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
}

.signed-in-banner svg {
  width: 1.6rem;
  height: 1.6rem;
  flex: none;
}

/* Secondary actions — "send another code", "sign out". Quieter than the main
   button, but still full-size tap targets: "sign out" being small and grey is
   how somebody ends up tapping the wrong thing.

   Both a background and a colour, and a :hover twin, for the reason at the
   top of this section. */
.quiet-actions {
  margin-top: 26px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* --accent, not --accent-strong, measured 4.39:1 on --surface in the light
   theme: under the 4.5 minimum, on a button that says "sign out". Caught by
   measuring it, not by looking at it — it looks perfectly fine. */
.button-quiet,
.button-quiet:hover:not(:disabled) {
  background: var(--surface);
  color: var(--accent-strong);
}

.button-quiet {
  width: 100%;
  margin-top: 0;
  padding: 15px 16px;
  font-family: var(--font-body);
  font-size: 1rem;
  font-weight: 700;
  border: 2px solid var(--border);
  border-radius: 12px;
  cursor: pointer;
  text-align: center;
}

.button-quiet:hover:not(:disabled) {
  border-color: var(--accent);
  box-shadow: var(--glow);
}

.button-quiet:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .button-quiet:hover:not(:disabled) { box-shadow: none; }
}

/* ===========================================================================
   WHAT YOU CAN SPOT  (badges.html)
   ---------------------------------------------------------------------------
   The visual job here is the same as the wording's: quietly proud, never a
   reward chart. So the ones she has are warm and solid, and the ones she does
   not have are simply calmer — no crosses, no grey "locked" padlocks, no
   empty slots. An empty slot is a list of failures, and she has not failed at
   anything.

   Reminder before adding a rule: the generic `button` and
   `button:hover:not(:disabled)` rules further up this file set BACKGROUND but
   not COLOUR, and beat most single-class selectors below them. Anything here
   that sets `color` on a state must set `background` too and carry its own
   `:hover` twin. See the note in the SAVE MY PROGRESS section.
   ------------------------------------------------------------------------- */

/* THE SHELF — what she has, gathered.
   Deliberately not a trophy cabinet: no ribbons, no shine, no counter shouting
   a score. Just the icons of the things she can now spot, sitting together, and
   a quiet line saying how many. It has to look right at ONE, which is why the
   items are generously spaced and the row is left-aligned rather than
   stretched — a single icon in a wide centred bar looks like a mistake. */
.badge-shelf {
  margin: 0 0 28px;
  padding: 18px 20px;
  border: 1px solid var(--hairline-warm, var(--border));
  border-radius: var(--radius);
  background: var(--accent-warm-soft, var(--surface));
}

.badge-shelf-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 14px 18px;
}

.badge-shelf-item {
  font-size: 1.7rem;
  line-height: 1;
  /* Scaled by the text-size control along with everything else, because
     somebody who needs Extra Large needs it here too. */
  width: 1.6em;
  height: 1.6em;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* A slot with nothing in it yet. Quiet enough that the row reads as the ones
   she HAS, with room around them — not as eight things she is missing. No
   cross, no lock, no number: this page does not tell anybody they have
   failed at something. */
.badge-shelf-item--empty {
  border: 2px dashed var(--hairline-warm, var(--border));
  border-radius: 50%;
  opacity: 0.4;
}

.badge-shelf-count {
  margin: 14px 0 0;
  font-size: 0.95rem;
  color: var(--ink-soft);
}

/* "What does this mean?" — a real button, because a phone has no hover and a
   hand that shakes cannot hold one steady. Quiet enough not to compete with the
   sentence above it, big enough to hit with a thumb. */
.badge-explain {
  /* The generic `button` rule above sets display:block, width:100% and a filled
     background, which turned this into a full-width blue bar dominating the row
     — and its :hover repainted the fill even after `background: none`. Both have
     to be undone explicitly, hover included. */
  display: inline-block;
  width: auto;
  text-align: left;
  margin: 10px 0 0;
  padding: 10px 0;
  min-height: 44px;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--accent-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.badge-explain:hover,
.badge-explain:hover:not(:disabled) {
  background: none;
  color: var(--ink);
}

.badge-explain:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

.badge-detail {
  margin: 4px 0 4px;
  padding: 14px 16px;
  border-left: 4px solid var(--accent-warm, var(--accent));
  background: var(--ground);
  border-radius: 0 8px 8px 0;
}

.badge-detail-line {
  margin: 0 0 10px;
  font-size: 1rem;
  line-height: 1.5;
}

.badge-detail-link {
  font-weight: 700;
  color: var(--accent-strong);
}

/* THE TWO GROUPS.
   The page used to be one list of nine, eight of which were things you could
   NOT yet do — under a heading announcing what you could. The owner of the app
   was confused by her own page, which is the loudest signal a design gets.
   A heading over each group makes the claim true, and makes progress legible
   at a glance instead of by counting green tints. */
.badge-group + .badge-group { margin-top: 32px; }

.badge-group-title {
  margin: 0 0 12px;
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--ink-soft);
  /* Quieter than an <h2> would normally be. These headings organise the page;
     they are not the point of it, and the rows underneath are. */
  letter-spacing: 0.01em;
}

.badge-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.badge {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

/* The ones she has. A warm left edge rather than a colour wash, so a long
   list of them still reads as a list and not as a wall. */
.badge--has {
  border-color: var(--state-safe);
  border-left-width: 6px;
  background: var(--state-safe-bg);
}

/* The ones she hasn't, quietened rather than crossed out. Opacity alone would
   drag the text under the contrast minimum, so the ink is left as it is and
   only the icon is softened. */
.badge:not(.badge--has) .badge-icon { opacity: 0.55; }
.badge:not(.badge--has) .badge-title { color: var(--ink-soft); }

.badge-icon {
  font-size: 1.7rem;
  line-height: 1.2;
  flex: none;
}

.badge-body { flex: 1 1 auto; min-width: 0; }

.badge-title {
  margin: 0 0 4px;
  font-family: var(--font-display);
  font-size: 1.1rem;
  font-weight: 600;
}

.badge-line {
  margin: 0;
  font-size: 0.95rem;
  color: var(--ink-soft);
  line-height: 1.5;
}

/* --ink-soft on the earned row's green tint measured 4.45:1, just under the
   4.5 minimum — invisible by eye, caught by measuring it. On that background
   the full ink is used instead; the row is already distinguished by its
   colour and its left edge, so the softer grey was not earning its keep. */
.badge--has .badge-line { color: var(--ink); }

.badge-tick {
  flex: none;
  font-size: 1.3rem;
  font-weight: bold;
  color: var(--state-safe);
  line-height: 1.4;
}

/* Space on BOTH sides. This had a top margin only, which was fine while every
   page ended with it. The thank-you page puts one in the middle, and the
   note under the button ran straight into the next heading — the sentence
   about signing in and the heading "What happens now" read as one block. */
.badge-cta { margin: 28px 0 30px; }

.badge-cta-note,
.badge-save-note {
  margin: 12px 0 0;
  font-size: 0.95rem;
  color: var(--ink-soft);
  text-align: center;
}

.badge-save-note a { color: var(--accent-strong); font-weight: 700; }

/* Said once to a screen reader, rather than repeated visually on every row. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ===========================================================================
   SOMEONE YOU TRUST  (trusted.html)
   ---------------------------------------------------------------------------
   The tone problem here is the opposite of the badges page. There, the risk
   was condescension; here it is surveillance. A list of somebody's activity,
   shown to a relative, can very easily look like a monitoring dashboard — and
   the person being summarised is an adult who agreed to share a count, not a
   patient on a ward.

   So: no red, no alarm icons, no permanent colour. A row is amber ONLY when
   something was actually flagged, because most weeks are quiet and a
   permanently coloured row is a permanent worry.

   Same standing warning as the other two sections: the generic `button` rules
   further up set BACKGROUND but not COLOUR and beat most single-class rules
   below them. Anything setting `color` on a state must set `background` too,
   and carry its own `:hover` twin.
   ------------------------------------------------------------------------- */

.section-heading {
  margin: 32px 0 12px;
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--ink);
}

.trust-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.trust-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 12px;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

/* Only when something was really flagged. */
.trust-row--flagged {
  border-color: var(--state-caution);
  border-left-width: 6px;
  background: var(--state-caution-bg);
}

.trust-body { flex: 1 1 14rem; min-width: 0; }

.trust-sentence {
  margin: 0 0 4px;
  font-size: 1.05rem;
  font-weight: 700;
}

.trust-since {
  margin: 0;
  font-size: 0.95rem;
  color: var(--ink-soft);
}

/* Ending a connection must never be the hardest thing on the page. It is
   quieter than the main action but still a full-width tap target on a phone,
   because "stop sharing with them" being small and grey is how somebody ends
   up unable to stop sharing with them. */
.button-quiet--small {
  flex: 1 1 100%;
  font-size: 0.95rem;
  padding: 13px 14px;
}

@media (min-width: 30rem) {
  .button-quiet--small { flex: 0 0 auto; width: auto; }
}

/* The connection code, big enough to read down a phone. */
.code-display {
  margin: 20px auto 0;
  padding: 20px 16px;
  max-width: 16ch;
  text-align: center;
  font-family: 'Courier New', Courier, monospace;
  font-size: 2rem;
  font-weight: bold;
  letter-spacing: 0.12em;
  color: var(--ink);
  background: var(--accent-warm-soft);
  border: 3px solid var(--accent-warm);
  border-radius: 12px;
}

/* Wider than the six-digit sign-in box, and no letter-spacing games — the
   code has a dash in the middle and eight characters to fit. */
.code-input--long {
  font-size: 1.5rem;
  letter-spacing: 0.12em;
  padding-left: calc(16px + 0.12em);
  max-width: 14ch;
}

.label-optional {
  font-weight: normal;
  color: var(--ink-soft);
}

/* ===========================================================================
   GLOSSARY POP-UPS IN LESSON TEXT  (glossary-pop.js)
   ===========================================================================

   A marked term has to read as "there is more here" without turning the
   paragraph into a field of blue. An underline the reader can see, in the
   body colour rather than link blue, is enough — these sit inside sentences
   that people are reading for meaning, and a paragraph with six bright links
   in it stops being a paragraph.
   =========================================================================== */

.gloss {
  color: inherit;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
  text-decoration-color: var(--accent-strong);
  /* `help` — the question-mark cursor — is the desktop signal for "this is
     not clickable". These are real links to a real page. */
  cursor: pointer;
}

.gloss:hover,
.gloss:focus-visible {
  text-decoration-style: solid;
  background: var(--lamp-halo);
  border-radius: 4px;
}

/* The focus ring is not optional here. These are inline, so the only thing
   telling a keyboard user which word they are on is this. */
.gloss:focus-visible {
  outline: 3px solid var(--accent-strong);
  outline-offset: 2px;
}

.gloss-pop {
  position: absolute;
  /* BELOW THE FIRST-VISIT TEXT-SIZE PROMPT, WHICH IS AT 50 AND IS SACRED.
     This was 60. On a cold first visit, four Shift+Tabs from the prompt reach
     a marked term in the lesson underneath — the prompt traps nothing — and
     Enter dropped this panel squarely on top of "How large would you like the
     text?", covering all three buttons. Somebody who cannot read the page
     cannot dismiss the thing offering to fix that. 45 keeps it above the page
     and under the prompt, and glossary-pop.js additionally refuses to open at
     all while the prompt is up, the same guard menu.js already carries. */
  z-index: 45;
  max-width: min(30rem, calc(100vw - 24px));
  /* AND IT CAN NEVER BE TALLER THAN THE SCREEN.
     Without this, the gift-card definition measured 1000px tall on a 320x568
     phone at Extra Large — 670px of it below the fold, including the Close
     button — and the only way to read the rest was to scroll, which destroys
     the panel. That is the app's core user on the app's core device, unable
     to read a definition at all. It scrolls inside itself now. */
  max-height: calc(100vh - 24px);
  /* THE PANEL ITSELF DOES NOT SCROLL — THE DEFINITION INSIDE IT DOES.
     When the whole panel scrolled, a clamped one put "Close" below its own
     fold: the reader had to know to scroll inside a box to find the way out of
     it. Now the term and the buttons are pinned and only the middle moves, so
     the way out is always on the screen. */
  display: flex;
  flex-direction: column;
  padding: 16px 18px;
  background: var(--surface);
  color: var(--ink);
  /* A DELIBERATE EDGE, NOT THE ORDINARY ONE.
     This was `var(--border)`, which is the right colour for a divider between
     two rows of the same page and the wrong one for a box floating on top of
     it: at 16% opacity it measured 1.34:1 against the page behind it, so the
     panel had, in practice, no boundary at all. Somebody reading at 200% with
     low vision would see a block of words landing among other words with
     nothing marking where one stopped. The shadow alone is not enough either —
     it disappears against a dark background. */
  border: 2px solid var(--accent-strong);
  border-radius: 12px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18);
}

.gloss-pop-term {
  flex: 0 0 auto;
  margin: 0 0 6px;
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--ink);
}

.gloss-pop-def {
  margin: 0 0 14px;
  color: var(--ink-soft);
  overflow-y: auto;
  overscroll-behavior: contain;
  min-height: 0; /* or a flex child refuses to shrink below its content */
}

.gloss-pop-actions {
  flex: 0 0 auto; /* pinned: never scrolls out of reach */
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
}

/* 44px minimum on both, because this opens on a phone more often than not.
   WIDTH AND MARGIN HAVE TO BE RESET EXPLICITLY. The site-wide `button, a.button`
   rule further up this file sets `width: 100%; margin-top: 18px`, and this rule
   overrode `display` and `padding` but not those two — so Close computed at
   560px wide on its own full-width row, the flex layout and the `margin-left:
   auto` beside it were dead intent, and every panel carried 72px of height it
   did not need. On a small phone that 72px is the difference between the Close
   button being on the screen and not. */
.gloss-pop-link,
.gloss-pop-close {
  min-height: 44px;
  width: auto;
  margin-top: 0;
  display: inline-flex;
  align-items: center;
  font: inherit;
  border-radius: 8px;
  padding: 0 14px;
}

.gloss-pop-link {
  color: var(--accent-strong);
  font-weight: 700;
  padding-left: 0;
}

.gloss-pop-close {
  margin-left: auto;
  background: var(--button-bg);
  color: var(--button-ink);
  border: none;
  font-weight: 700;
  cursor: pointer;
}

.gloss-pop-close:focus-visible,
.gloss-pop-link:focus-visible {
  outline: 3px solid var(--accent-strong);
  outline-offset: 2px;
}


/* ===========================================================================
   ABOUT AND CONTACT  (about.html, contact.html)
   ===========================================================================

   Plain prose pages. They get their own small block of rules rather than
   borrowing another page's, because borrowing is how a change to the badges
   page silently reformats the page explaining who you are.
   =========================================================================== */

.page-block {
  margin: 0 0 30px;
}

.page-block h2 {
  margin: 0 0 10px;
  font-size: 1.2rem;
  color: var(--ink);
}

.page-block p {
  margin: 0 0 14px;
  color: var(--ink-soft);
}

.page-block p:last-child { margin-bottom: 0; }

/* THE WARNING ON THE CONTACT PAGE IS THE POINT OF THE CONTACT PAGE.
   A page inviting older people to send an email is a page that can teach a
   bad habit, so the line that keeps the habit safe is marked out rather than
   left as the first of several paragraphs. Not red: this is not an error, and
   the red on this site means "we think this is a scam". */
.page-warning {
  padding: 16px 18px;
  border-left: 5px solid var(--accent-strong);
  background: var(--lamp-halo);
  border-radius: 0 10px 10px 0;
  color: var(--ink);
}

/* A PLACEHOLDER HAS TO BE IMPOSSIBLE TO SHIP BY ACCIDENT.
   These blocks are the parts only Kiffy and Dibbs can write. Left looking
   like ordinary text they would go live and sit there for months; drawn as a
   dashed, obviously-unfinished box, the page tells on itself the moment
   anybody looks at it. Delete the rule when the last one is gone. */
.page-placeholder {
  padding: 14px 16px;
  border: 2px dashed var(--accent-strong);
  border-radius: 10px;
  background: transparent;
  color: var(--ink);
}

/* A quieter aside inside a page block: true, worth saying, not the point.
   Used for the "300 a month" ceiling on the price page, which has to be
   stated but must not be the loudest thing in the section. */
.page-note {
  font-size: 0.94rem;
  padding-left: 14px;
  border-left: 3px solid var(--border);
}

/* ===========================================================================
   THE TWO PRICES
   ===========================================================================
   Two plain cards, side by side where there is room and stacked where there
   is not.

   THE STACK IS NOT A FALLBACK, IT IS THE COMMON CASE. This audience reads at
   Extra Large on a phone, and two 150px cards side by side at that size gives
   each price four characters of room. So they stack at anything under 34rem —
   a width chosen in rem rather than px on purpose, so it responds to the text
   size the reader chose rather than only to the width of their phone.

   No "most popular" flag, no crossed-out price, no colour on one and not the
   other. Every one of those is a nudge, and nudging an 80-year-old towards
   the bigger charge is not something this site is going to do. The yearly
   card says what it is — two months cheaper — and the reader decides. */
.plan-row {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin: 0 0 18px;
}

/* A column, so the two buttons line up along the bottom however many lines of
   text sit above them. Without this the yearly card's extra line pushes its
   button lower than the monthly one's, and two buttons at different heights
   read as two different KINDS of button — which is the last impression a
   price page for nervous people should give. */
.plan {
  flex: 1 1 14rem;
  min-width: 0;
  padding: 20px 18px;
  background: var(--surface-soft);
  border: 1px solid var(--border);
  border-radius: 14px;
  text-align: center;
  display: flex;
  flex-direction: column;
}

/* Specificity again, and this one passed the sweep while still being wrong:
   at 2.4rem bold the price counts as large text, so the inherited --ink-soft
   cleared the 3:1 floor that applies to it and nothing complained. The price
   is the loudest thing on the page and should be the full ink, so the
   selector has to actually win. */
.plan .plan-price {
  margin: 0;
  font-family: "Fredoka", system-ui, sans-serif;
  font-size: 2.4rem;
  font-weight: 700;
  line-height: 1.1;
  color: var(--ink);
}

/* Overrides .page-block p, which would otherwise put 14px between the number
   and the word that says what the number means.

   BOTH COLOURS HERE WERE PICKED BY MEASUREMENT, AND THE TWO OBVIOUS CHOICES
   BOTH FAILED. On the card's --surface-soft background, --ink-soft measures
   4.40:1 and --accent-warm 3.11:1, against the 4.5:1 floor this site holds
   text to — so the first draft of the price page had the word "a month" and
   the whole savings line below standard, on the one page a person reads
   before spending money. The site-wide sweep is what caught it. --ink is
   11.1:1 and --accent-strong 6.0:1, in both themes. */
/* `.plan .plan-period`, NOT `.plan-period`. `.page-block p` is a class plus an
   element and so outranks a bare class — the same specificity trap as
   `a.button` above, twice on one page. The first draft used `!important` on
   the margin, which fixed the spacing and left the COLOUR still inherited
   from .page-block p, which is exactly the --ink-soft that measures 4.40:1.
   Winning on specificity fixes both at once and needs no !important. */
.plan .plan-period {
  margin: 0 0 4px;
  font-size: 1.05rem;
  color: var(--ink);
}

.plan .plan-note {
  margin: 0 0 4px;
  font-size: 0.92rem;
  color: var(--accent-strong);
  font-weight: 700;
}

/* The text half of the card. Separated from the button so the button can be
   pushed to the bottom without the gap above it depending on how many lines
   of text this particular plan happens to have. */
.plan-body { margin-bottom: 14px; }

/* `a.plan-button`, NOT `.plan-button`, AND THE EXTRA LETTER IS THE WHOLE RULE.
   The site-wide `button, a.button` rule near the top of this file sets
   `margin-top: 18px`, and `a.button` (element + class) outranks a bare class
   selector — so a plain `.plan-button { margin-top: auto }` loses silently.
   Measured before the fix: the monthly button sat 33px above the yearly one,
   because `auto` never applied and each button simply followed however many
   lines of text its own card happened to have. Same trap, same file, as the
   note above .gloss-pop-close. */
/* `hidden` HAS TO BE PUT BACK, BECAUSE THIS FILE TOOK IT AWAY.
   The browser hides a `hidden` element with `display: none` from its own
   default stylesheet, and ANY author rule that sets `display` beats it. The
   site-wide `button, a.button { display: block }` is such a rule — so
   `<a class="button" hidden>` was not hidden at all, and both Subscribe
   buttons sat on the price page in the state where no payment links exist,
   pointing at "#". A button that looks like it will take somebody's $6 and
   does nothing is the worst thing this page could do, and it was live until a
   browser test looked. */
a.plan-button[hidden] { display: none; }

a.plan-button {
  /* `auto`, not a fixed gap: this is what pins the button to the bottom of
     its card so the two line up however tall the text above them is. */
  margin-top: auto;
  width: 100%;
  /* A price is short; a button label is not. Without this the label breaks out
     of a narrow card at Extra Large. */
  white-space: normal;
}

/* While the definition is being fetched. Deliberately quiet — a spinner on a
   word inside a sentence is more alarming than the half-second it covers. */
.gloss--loading {
  opacity: 0.65;
  text-decoration-style: wavy;
}

/* ===========================================================================
   THE WORDMARK LOCKUP  —  "My Porch" / "Light's On"
   ===========================================================================

   The name became a sentence, and a sentence does not fit a phone header on
   one line. The first attempt left it to wrap on its own and produced two
   separate flex items: the long half wrapped onto three lines while "On"
   stayed on the first line beside it, orphaned — and at 320px it ended up
   sitting UNDERNEATH the Menu button.

   The automated header check called that fine, because the brand's bounding
   box never overlapped the button's. Only rendering it and looking showed the
   name in pieces. Same lesson as everywhere else in this project: run it, then
   look at it.

   So the wordmark is now ONE element that breaks in a chosen place. Measured:
   80px of header at 320px/Extra Large, versus 105px for the same name left to
   wrap by itself — and 22px shorter than the old single-line wordmark at that
   text size.
   =========================================================================== */

.brand-name {
  display: inline;
  min-width: 0;
}

.brand-stack {
  display: flex;
  flex-direction: column;
  line-height: 1.05;
}

.brand-stack .brand-line2 { display: block; }

/* EACH LINE HOLDS TOGETHER. Without this the first line wrapped again — "My"
   over "Porch" — and the two-line lockup became three. The font-size clamp on
   a.brand already shrinks the wordmark with the viewport, so nowrap makes it
   get smaller rather than taller, which is the right trade in a header that
   also has to hold the Menu button. */
.brand-stack > * { white-space: nowrap; }

/* ===========================================================================
   FORWARD IT INSTEAD  (check.html)
   ===========================================================================

   Quieter than the paste box on purpose. Pasting is the fast path and answers
   in seconds; this is the fallback for somebody who cannot paste — shaky
   hands, a phone they are unsure of, an email they are frightened to touch.
   Given equal weight it would turn one obvious action into a decision.
   =========================================================================== */

.forward-panel {
  margin: 30px 0 0;
  padding: 20px 22px;
  background: var(--surface);
  border: 2px solid var(--border);
  border-radius: 14px;
}

.forward-panel h2 {
  margin: 0 0 10px;
  font-size: 1.15rem;
  color: var(--ink);
}

.forward-panel p { margin: 0 0 14px; color: var(--ink-soft); }

/* THE ADDRESS GETS ITS OWN LINE, AND IS NEVER ALLOWED TO BE CUT OFF.
   The first version set `white-space: nowrap` on it, inline in a sentence,
   reasoning that an email address should not break across lines. On a 320px
   phone at Extra Large that rendered as "check@myporchlig" and stopped — the
   single most important string on the page, unreadable, on the exact screen
   this app exists for. The overflow check missed it because it ran at 390px
   and normal text.

   So: its own line, sized to shrink on narrow screens rather than overflow,
   and allowed to break as an absolute last resort. A wrapped address is
   awkward; a truncated one is useless. */
.forward-address {
  display: block;
  margin: 0 0 14px;
  font-weight: 700;
  font-size: clamp(0.95rem, 4.6vw, 1.15rem);
  color: var(--accent-strong);
  overflow-wrap: anywhere;
  line-height: 1.35;
}

/* THE WARNING, NOT THE ADDRESS, IS THE POINT OF THIS PANEL.
   Inviting older people to forward email is inviting them to send things they
   should not send, so the line that keeps the habit safe is marked out rather
   than left as one paragraph among several. Not red: red on this site means
   "we think this is a scam". */
.forward-warning {
  padding: 14px 16px;
  border-left: 5px solid var(--accent-strong);
  background: var(--lamp-halo);
  border-radius: 0 10px 10px 0;
  color: var(--ink);
}

.forward-notes {
  margin: 0;
  padding-left: 22px;
  color: var(--ink-soft);
}

.forward-notes li { margin-bottom: 8px; }
.forward-notes li:last-child { margin-bottom: 0; }
