  :root {
    /* WilderCode — night sky + meadow green */
    --bg: #0d1117; --surface: #161b22; --surface-2: #1f2937; --border: #2d3748;
    --rung-bg: #1a212b;
    --text: #f0f4f8; --muted: #8b9ab3; --graphite: #ffd24a;
    --green: #a9d15c; --green-dark: #6f8f38; --green-glow: rgba(169,209,92,.15);
    /* The logo's two leaf greens, fixed at these exact values regardless of
       theme -- unlike --green/--green-dark, which themselves shift under
       the light theme for contrast. Used where the brand color itself is
       the point (e.g. the Help subheading icons), not just "a green that
       reads on this background". */
    --leaf-light: #a9d15c;
    --leaf-dark: #6f8f38;
    --glow: rgba(40,200,64,.35); --field: #0d1117;
    /* Search box at rest: darker than both the card and the page behind it, so
       it reads as a fine recessed rim rather than a drawn line. Separate from
       --border so the resting edge can be quiet without lightening every other
       divider on the page. */
    --field-border: #070a0e;
    /* Dark mode's --surface already reads as a distinct input well against
       --bg, so the field just reuses it here; only light mode needs its own
       tint (see below). */
    --field-bg: var(--surface);
    /* Wilder orange (the "ER" in the wordmark) — call-to-action accent */
    --orange: #f0891f; --orange-dark: #d1741a; --orange-glow: rgba(240,137,31,.32);
    --danger: #ff6b5b;
    /* Shared "filled button" look: every solid CTA (Rhyme, Scan, active
       toggles) pulls from these two instead of a flat color-fill, so they
       read as one soft family rather than each button being its own flat
       block of pure accent color. A narrower range than before -- both
       stops stay close to the true accent hue (only a light touch of white
       up top, a light touch of --muted down low) so the color doesn't wash
       out grey, while the gradient itself stays gentle instead of a
       high-contrast, eye-grabbing sweep. */
    --btn-orange: linear-gradient(180deg,
      color-mix(in srgb, var(--orange) 78%, white) 0%,
      color-mix(in srgb, var(--orange) 90%, var(--muted) 10%) 100%);
    --btn-orange-hover: linear-gradient(180deg,
      color-mix(in srgb, var(--orange-dark) 78%, white) 0%,
      color-mix(in srgb, var(--orange-dark) 90%, var(--muted) 10%) 100%);
    --btn-green: linear-gradient(180deg,
      color-mix(in srgb, var(--green) 78%, white) 0%,
      color-mix(in srgb, var(--green) 90%, var(--muted) 10%) 100%);
    /* Softened text for all filled buttons -- a slightly transparent white
       reads gentler than flat near-black, less of a hard-edged stamp. */
    --btn-text: rgba(255, 255, 255, .88);
    /* closure ramp: meadow green (resolved) -> faint slate (open) */
    --c-identity: #9b93b0;  /* identity: a muted tone, outside the green rhyme spectrum */
    --c-perfect: #a9d15c; --c-partner: #9cc856; --c-family: #8fbf4f; --c-additive: #6f8f38;
    --c-subtractive: #8a936a; --c-assonance: #8b9ab3; --c-consonance: #5f6b7a;
    /* word-underline ramp: shades of orange fading to grey, intensity tracking
       closure. Uses var()+color-mix so it re-resolves under the light theme. */
    --u-perfect:     var(--orange);
    --u-partner:     color-mix(in srgb, var(--orange) 92%, var(--muted));
    --u-family:      color-mix(in srgb, var(--orange) 84%, var(--muted));
    --u-additive:    color-mix(in srgb, var(--orange) 70%, var(--muted));
    --u-subtractive: color-mix(in srgb, var(--orange) 58%, var(--muted));
    --u-assonance:   color-mix(in srgb, var(--orange) 44%, var(--muted));
    --u-consonance:  color-mix(in srgb, var(--orange) 30%, var(--muted));
    --u-identity:    var(--muted);
    /* Row-background gradient: the app's existing closure ramp (--c-* above,
       meadow green fading to faint slate) reused as the one shared gradient
       for both the Rhyme type filter rows and the Rhyme categories table in
       Help -- one gradient, two places, instead of each inventing its own. */
    --rt-perfect:     var(--c-perfect);
    --rt-partner:     var(--c-partner);
    --rt-family:      var(--c-family);
    --rt-additive:    var(--c-additive);
    --rt-subtractive: var(--c-subtractive);
    --rt-assonance:   var(--c-assonance);
    --rt-consonance:  var(--c-consonance);
    --rt-identity:    var(--muted);
    --shadow: 0 1px 2px rgba(0,0,0,.3), 0 8px 24px rgba(0,0,0,.09);
    --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;
    --mono: ui-monospace, "SF Mono", "Cascadia Mono", Consolas, monospace;
    --serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
  }
  :root[data-theme="light"] {
    --bg: #fbfbf8; --surface: #ffffff; --surface-2: #f1f5f0; --border: #d7ddd0;
    --rung-bg: #f4f6ef;
    --text: #0f172a; --muted: #5a6a82; --graphite: #000000;
    --green: #6f8f38; --green-dark: #4a6025; --green-glow: rgba(111,143,56,.12);
    --glow: rgba(111,143,56,.28); --field: #ffffff;
    --field-border: #eaeee5;
    /* Light mode's --surface (white) is indistinguishable from the page card
       it sits on, so the search box reads as a flat panel rather than a
       field to type into. A neutral (untinted) light grey -- dark mode's
       near-black --surface already carries this contrast on its own -- gives
       it the look of a shallow recessed well instead. Tinted toward the
       page's own olive hue (#e6eadf, not neutral #e5e5e5) so the well
       belongs to the cards around it instead of reading as a grey,
       disabled-looking box. */
    --field-bg: #e6eadf;
    --orange: #e07d16; --orange-dark: #c26a12; --orange-glow: rgba(224,125,22,.22);
    --danger: #c0392b;
    --c-identity: #7a6f96;
    --c-perfect: #6f8f38; --c-partner: #678634; --c-family: #5f7d30; --c-additive: #4a6025;
    --c-subtractive: #6f7658; --c-assonance: #64748b; --c-consonance: #98a3b2;
    /* Underline ramp on the light page starts from --orange-dark. Bare
       --orange is 2.71:1 against this background, under the 3:1 floor for
       a graphical mark; --orange-dark clears it and still reads as the
       brand orange, richer instead of washed out. */
    --u-perfect:     var(--orange-dark);
    --u-partner:     color-mix(in srgb, var(--orange-dark) 92%, var(--muted));
    --u-family:      color-mix(in srgb, var(--orange-dark) 84%, var(--muted));
    --u-additive:    color-mix(in srgb, var(--orange-dark) 70%, var(--muted));
    --u-subtractive: color-mix(in srgb, var(--orange-dark) 58%, var(--muted));
    --u-assonance:   color-mix(in srgb, var(--orange-dark) 44%, var(--muted));
    --u-consonance:  color-mix(in srgb, var(--orange-dark) 30%, var(--muted));
    --shadow: 0 1px 2px rgba(15,23,42,.06), 0 8px 24px rgba(15,23,42,.09);
  }
  /* The shared --btn-orange/--btn-green (Scan buttons, Copy-hover, the
     pronunciation toggle -- NOT #go, which has its own full-saturation
     gradient) read fine against light theme's bright page, but the same
     --muted-blended fill reads murky against dark theme's near-black
     backdrop. Dark theme gets an almost-white (~90%) tint instead -- just
     enough accent color to hint at orange/green -- with matching dark text
     (--btn-text stays translucent white; that's #go's alone, and swapping it
     here too would vanish against this near-white fill). Light theme's own
     look is untouched. */
  :root[data-theme="dark"] {
    --btn-orange: linear-gradient(180deg,
      color-mix(in srgb, var(--orange) 12%, white) 0%,
      color-mix(in srgb, var(--orange) 22%, white) 100%);
    --btn-orange-hover: linear-gradient(180deg,
      color-mix(in srgb, var(--orange-dark) 12%, white) 0%,
      color-mix(in srgb, var(--orange-dark) 22%, white) 100%);
    --btn-green: linear-gradient(180deg,
      color-mix(in srgb, var(--green) 12%, white) 0%,
      color-mix(in srgb, var(--green) 22%, white) 100%);
    --btn-text-onlight: #16181c;
  }
  /* High contrast: a deliberate accessibility choice, not a fourth point on
     the light/dark spectrum -- true black/white, thick always-visible
     borders (every card already has `border: 1px solid var(--border)`
     underneath its accent styling, so this alone outlines everything), and
     the closure/rhyme-gender ramps replaced outright rather than derived
     from color-mix() blends, since several of the original blend endpoints
     (the faint end of each ramp, meant to read as "barely there" against a
     soft dark backdrop) drop below AA text contrast against true black.
     Every color below was chosen bright enough to clear ~7:1 against #000
     on its own, not just tuned by feel. */
  :root[data-theme="hc"] {
    --bg: #000000; --surface: #000000; --surface-2: #141414; --border: #ffffff;
    --rung-bg: #000000;
    --text: #ffffff; --muted: #dcdcdc; --graphite: #ffffff;
    --green: #6bff4d; --green-dark: #3ecc23; --green-glow: rgba(107,255,77,.3);
    --glow: rgba(107,255,77,.55); --field: #000000;
    --field-border: #ffffff; --field-bg: #000000;
    --orange: #ffb347; --orange-dark: #ff9500; --orange-glow: rgba(255,179,71,.45);
    --danger: #ff8a80;
    --btn-orange: linear-gradient(180deg, #ffc670 0%, #ffb347 100%);
    --btn-orange-hover: linear-gradient(180deg, #ffd794 0%, #ffc670 100%);
    --btn-green: linear-gradient(180deg, #8dff70 0%, #6bff4d 100%);
    --btn-text: #000000; --btn-text-onlight: #000000;
    /* closure ramp: kept the same "green, closest -> faded, most distant"
       direction as the default theme, but each stop is its own bright hue
       (not one hue fading to grey) so adjacent rungs stay distinguishable
       even for a viewer who can't rely on subtle saturation differences. */
    --c-identity: #eab8f7;
    --c-perfect: #7cfc6b; --c-partner: #79e8a0; --c-family: #7bd6c4;
    --c-additive: #7cc3e8; --c-subtractive: #9ab4e0; --c-assonance: #b7a6e0;
    --c-consonance: #d6d6e6;
    --u-identity: var(--muted);
    --shadow: 0 0 0 1px #ffffff;
  }
  /* Focus is otherwise mostly the browser default outline, which is too
     thin/low-contrast against a stark black-and-white page to trust here. */
  :root[data-theme="hc"] *:focus-visible {
    outline: 3px solid #ffe600; outline-offset: 2px;
  }
  /* The dark-theme photo backdrop (see the body rule below) reintroduces
     exactly the low-contrast, hard-to-predict-luminance backdrop this theme
     exists to remove. */
  :root[data-theme="hc"] body {
    background-image: none !important; background-attachment: scroll;
  }
  /* Same intent as [data-theme="hc"] above (true, deliberate contrast rather
     than a themed palette), just inverted -- black text/borders on white
     for whoever finds a bright field easier to read than a dark one. Same
     ramp *direction* per rung (green -> teal -> blue -> lavender -> grey,
     identity its own magenta-ish outlier) as the dark HC theme, just each
     stop darkened instead of brightened so it clears ~7:1 against #fff
     instead of #000. */
  :root[data-theme="hc-light"] {
    --bg: #ffffff; --surface: #ffffff; --surface-2: #efefef; --border: #000000;
    --rung-bg: #ffffff;
    --text: #000000; --muted: #3a3a3a; --graphite: #000000;
    --green: #146608; --green-dark: #0e4a06; --green-glow: rgba(20,102,8,.12);
    --glow: rgba(20,102,8,.3); --field: #ffffff;
    --field-border: #000000; --field-bg: #ffffff;
    --orange: #a04e00; --orange-dark: #7a3c00; --orange-glow: rgba(160,78,0,.22);
    --danger: #a3170e;
    --btn-orange: linear-gradient(180deg, #a04e00 0%, #7a3c00 100%);
    --btn-orange-hover: linear-gradient(180deg, #7a3c00 0%, #5c2d00 100%);
    --btn-green: linear-gradient(180deg, #146608 0%, #0e4a06 100%);
    --btn-text: #ffffff; --btn-text-onlight: #ffffff;
    --c-identity: #7a1069;
    --c-perfect: #146608; --c-partner: #12603f; --c-family: #10594f;
    --c-additive: #0e4a68; --c-subtractive: #1b3a70; --c-assonance: #42267a;
    --c-consonance: #4a4560;
    --u-identity: var(--muted);
    --shadow: 0 0 0 1px #000000;
  }
  :root[data-theme="hc-light"] *:focus-visible {
    outline: 3px solid #0033cc; outline-offset: 2px;
  }
  :root[data-theme="hc-light"] body {
    background-image: none !important; background-attachment: scroll;
  }
  /* Settings > Colorblind-friendly colors. Deliberately NOT a theme: colour
     vision is independent of whether you want a light, dark or high-contrast
     page, so folding it into the Theme select would have forced a choice
     between the two. It overrides only the eight rung colours -- the one
     place in the app where meaning is carried by hue.

     The default ramp walks green -> olive -> slate, which is close to the
     axis red-green colour blindness collapses: Perfect, Partner and Family
     sit within a few percent of each other in hue AND lightness, so for a
     deuteranope or protanope those three rungs are effectively one colour.

     These eight are anchored on the Okabe-Ito palette (the literature-
     standard categorical set verified distinguishable under protanopia,
     deuteranopia and tritanopia), then lightness/saturation-tuned per theme
     -- hue held fixed -- against three measured constraints, not eyeballed:
     minimum pairwise CIE76 ΔE >=10 after simulating all three CVD types
     (Brettel/Viénot transform), WCAG contrast >=4.5:1 (>=7:1 for the two
     high-contrast themes, matching their own stricter promise) against that
     theme's rung background, and >=10 ΔE from the theme's own body text
     colour so a rung tint never reads as plain text. One set per theme,
     not one set reused everywhere: a colour clearing contrast on a
     near-black page is nowhere near 4.5:1 on a near-white one, so hue
     identity carries across themes but the actual values don't. */
  :root[data-cb="on"] {
    --c-identity: #aa7a94;
    --c-perfect: #00ffb9;
    --c-partner: #1d95d9;
    --c-family: #68c3f5;
    --c-additive: #ffc94f;
    --c-subtractive: #ec791e;
    --c-assonance: #c5b1b1;
    --c-consonance: #fff89b;
  }
  :root[data-cb="on"][data-theme="light"] {
    --c-identity: #48313e;
    --c-perfect: #1c7c62;
    --c-partner: #006fad;
    --c-family: #02466c;
    --c-additive: #966800;
    --c-subtractive: #783705;
    --c-assonance: #110808;
    --c-consonance: #252303;
  }
  :root[data-cb="on"][data-theme="hc"] {
    --c-identity: #c1acb8;
    --c-perfect: #a3f4de;
    --c-partner: #93c9e7;
    --c-family: #4bbcfb;
    --c-additive: #ffebbd;
    --c-subtractive: #ecb68c;
    --c-assonance: #d28888;
    --c-consonance: #9e9954;
  }
  :root[data-cb="on"][data-theme="hc-light"] {
    --c-identity: #390b24;
    --c-perfect: #33564c;
    --c-partner: #07547f;
    --c-family: #2a3a43;
    --c-additive: #402e06;
    --c-subtractive: #231204;
    --c-assonance: #5c3030;
    --c-consonance: #615a05;
  }
  * { box-sizing: border-box; }
  html, body { margin: 0; }
  body {
    background: var(--bg); color: var(--text); font: 15px/1.5 var(--sans);
    padding: 30px 20px 80px; display: flex; justify-content: center;
    background-image: radial-gradient(60% 45% at 50% -5%, var(--green-glow), transparent 70%);
  }
  /* Dark mode: night-meadow photo behind everything, under a heavy dark scrim
     so the cards and text keep their contrast. (Experiment — easy to remove.) */
  :root[data-theme="dark"] body {
    background-image:
      radial-gradient(60% 45% at 50% -5%, var(--green-glow), transparent 70%),
      linear-gradient(rgba(13,17,23,.78), rgba(13,17,23,.9)),
      url("/assets/pexels-shijin-cm-177081536-11128041.jpg");
    background-size: auto, auto, cover;
    background-position: top, top, center;
    background-repeat: no-repeat;
    background-attachment: fixed;
  }
  /* iOS repaints a fixed background-attachment every frame instead of
     compositing it, visibly janky on scroll -- small screens are almost
     always a phone, so fall back to scroll there. */
  @media (max-width: 768px) {
    :root[data-theme="dark"] body { background-attachment: scroll; }
  }
  /* Light mode: no full-cover photo -- just soft glows behind the content. */
  :root[data-theme="light"] body {
    background-image:
      radial-gradient(65% 50% at 50% -5%, var(--green-glow), transparent 70%),
      radial-gradient(55% 45% at 88% 12%, rgba(224,125,22,.09), transparent 70%),
      radial-gradient(70% 55% at 10% 105%, rgba(111,143,56,.10), transparent 70%);
    background-attachment: fixed;
  }
  /* Settings > Display background image, off. Same mechanism the two
     high-contrast themes already use above, for the same reason stated
     differently: some readers just want a flat field behind the text. It
     also stops the dark theme's 442 KB photo being downloaded at all --
     `background-image: none` means the browser never requests the URL. */
  :root[data-bgimg="off"] body {
    background-image: none !important; background-attachment: scroll;
  }
  .wrap { width: 100%; max-width: 760px; }

  header { text-align: center; margin-bottom: 18px; }
  /* Header lockup: the leaf mark and the wordmark are one unit, not two
     elements that happen to sit near each other. Everything inside is sized in
     em off the single font-size below -- change that one number and the mark,
     the gap and the optical nudge all scale with it in step, which is what
     keeps the relationship between them fixed at any size. */
  /* A COLUMN. The h1 and the .subtitle under it are both children here, so
     the default row direction laid the scope line out BESIDE the wordmark,
     vertically centered against it, instead of under it. Harmless while the
     subtitle was short enough to read as a tagline to the right; obvious
     once the copy widened to "for the page and the stage". The stacking is
     the container's job -- .subtitle's own margin-top only spaces it. */
  .brand {
    display: inline-flex; flex-direction: column; align-items: center;
    justify-content: center;
    font-size: 22px;
  }
  /* Sized off .brand-top's own em (the small "Wilder" line), not the
     bigger "Rhymes" line below -- the mark is a small eyebrow over the
     product name, not scaled to match it. */
  .logo {
    width: .9em; height: .9em; flex: none;
  }
  /* Stacked lockup: logo + "Wilder" share a baseline on a small top line;
     "Rhymes" -- the actual product name -- sits centered underneath, much
     larger. One considered mark instead of a single-line wordmark next to
     a leaf icon. h1 stays the single accessible heading (both lines are
     inside it); the two lines are plain spans so the column layout and
     independent sizing live on them, not on separate headings. */
  h1 {
    display: flex; flex-direction: column; align-items: center;
    font: 400 1em/1 var(--serif); letter-spacing: -.2px; margin: 0;
    color: var(--graphite);
  }
  .brand-top { display: flex; align-items: center; gap: .25em; font-size: .95em; color: inherit; text-decoration: none; }
  /* The small "Wilder" line tracks --text rather than --graphite: white on
     the dark themes instead of the wordmark's marigold, so the name reads as
     the company signature above the title rather than a second gold element
     competing with "Rhyme Dictionary" underneath it. Light themes are
     unaffected -- --text and --graphite are both near-black there. */
  .wilder { font-size: 1em; color: var(--text); }
  /* --graphite: near-black in light theme, warm gold in dark -- the
     wordmark's own ink color rather than the leaf-green accent. Underline
     matches it exactly, so it's one solid color, not two. */
  .rhymes {
    font-size: 1.8em; margin-top: .06em;
    text-decoration: underline; text-underline-offset: .18em; text-decoration-thickness: 1.5px;
    text-decoration-color: var(--graphite);
  }
  /* The scope line, directly under the lockup. Serif like the title and the
     tagline so the three read as one unit, but upright rather than italic:
     the tagline below is the aside, this is a claim, and setting both in
     italic would flatten them into one voice. Sized between the two --
     bigger than the tagline because it carries "complete", smaller than the
     title because it is not the name. */
  .subtitle {
    color: var(--muted); font: 400 15px/1.4 var(--serif);
    margin: 6px 0 0; letter-spacing: .1px;
  }
  /* Serif + italic, matching h1's family rather than the page's system
     sans -- reads as one considered lockup (mark + name + credit) instead
     of a headline in one type system with a caption bolted on in another.
     Names only now (no "for poets, playwrights and lyricists" slogan) --
     Wood and Pattison are the names this audience actually recognizes. */
  .tag { color: var(--muted); font: italic 13px/1.5 var(--serif); margin-top: 4px; }
  /* Keep a proper noun whole across a line break. Without this the credit
     line split the institution itself on a phone -- "...of Berklee" /
     "College of Music" -- which reads as two separate places rather than
     one name. Breaking BEFORE "Berklee" is always the better seam. */
  .nb { white-space: nowrap; }
  /* Early-preview badge: calibrates a first-time visitor's expectations
     (this is being actively built, not a finished product) and points
     straight at the feedback channel instead of leaving that to the footer.
     Sits on its own centered line below the lockup. .badge-row is a
     block-level flex wrapper rather than an inline sibling, so the badge is on
     its own line unconditionally instead of relying on it and .brand happening
     to run out of room at a given width. */
  .badge-row { display: flex; justify-content: center; margin-top: 6px; }
  /* An outline box, no fill -- same rounded-rectangle treatment and neutral
     --border line color as the app's other small popovers (e.g. .layerdd
     .layermenu), rather than a fully-rounded pill in its own one-off green
     outline, so the badge doesn't look like a unique control. Text/bullets
     use --muted (slightly darker than --border itself, which is too low-
     contrast to read as text) instead of brand green, so the whole badge
     reads as one quiet, neutral marker rather than a colored callout. */
  .preview-badge {
    display: flex; align-items: center; justify-content: center; gap: 6px;
    /* A narrow, shallow box that hugs its text -- not a wide bar competing
       with the wordmark above it. */
    width: 20%; min-width: fit-content; max-width: 200px;
    margin: 0; padding: 3px 14px; border-radius: 10px;
    border: 1px solid var(--border); background: transparent;
    color: var(--muted); font: 400 12px var(--sans); letter-spacing: .1px;
    text-transform: none; text-decoration: none;
  }
  .preview-badge::before, .preview-badge::after { content: "\2022"; opacity: .6; }

  /* Search field + filter summary as ONE card: the field is a recessed well
     inside it, the filter bar is its footer row. */
  .searchgroup {
    background: color-mix(in srgb, var(--surface) 80%, var(--border) 20%);
    border: 1px solid var(--border); border-radius: 12px;
    box-shadow: var(--shadow); padding: 8px 8px 0;
    transition: border-color .15s;
  }
  .searchgroup:hover, .searchgroup:focus-within { border-color: var(--green); }
  .searchgroup .search { box-shadow: none; border-radius: 9px; }
  .searchgroup .search:focus-within { box-shadow: 0 0 0 3px var(--green-glow); }
  .searchgroup .filterspanel,
  .searchgroup .filterspanel:hover,
  .searchgroup .filterspanel[data-open="true"] {
    margin: 0 -8px; border: 0; border-radius: 0 0 11px 11px;
    background: transparent; box-shadow: none;
  }
  .search {
    display: flex; gap: 10px; align-items: center; background: var(--field-bg);
    border: 1px solid var(--field-border); border-radius: 12px;
    padding: 8px 8px 8px 18px; box-shadow: var(--shadow);
    transition: border-color .15s, box-shadow .15s;
  }
  .search:focus-within {
    border-color: var(--green);
    box-shadow: var(--shadow), 0 0 0 3px var(--green-glow);
  }
  /* Safe for All Ages: lives inside the Filters drawer now, styled like an
     ordinary checkbox row (same size/weight as the .layermenu labels it
     sits above) rather than the loud standalone pill it started as -- it's
     one filter among several here, not a banner. The accent color still
     tracks checked state so on/off reads at a glance without relying on
     the checkbox alone. */
  .safetoggle-inline {
    display: flex; align-items: center; gap: 7px; cursor: pointer;
    font: 600 13px var(--sans); color: var(--text);
    /* .filterspanel-inner uses align-items: flex-start so wrapped rows of
       unequal-height items don't stretch -- fine for the dropdown "select
       boxes" (all the same height as each other), but it left this plain
       checkbox+label sitting top-aligned against them instead of centered
       in the row. align-self overrides just this one item back to center. */
    align-self: center;
  }
  .safetoggle-inline input { accent-color: var(--green); }
  .safetoggle-inline abbr { text-decoration: none; cursor: help; display: inline-flex; }
  .safetoggle-inline:hover .fhelp { color: var(--green); }
  #word {
    /* min-width:0 overrides the flex item default (min-width:auto), which
       Safari/iOS in particular respects for <input> elements: without it,
       the input refuses to shrink below its own intrinsic content width
       and pushes #go out past the edge of the screen on a narrow phone --
       Chromium is more forgiving here, so this didn't show up in earlier
       desktop-browser testing. */
    flex: 1; min-width: 0; border: 0; background: transparent; color: var(--text);
    font: 400 24px/1.2 var(--serif); outline: none; padding: 6px 0;
  }
  /* --muted alone (used everywhere else for real secondary text -- hints,
     labels) still reads as too solid at 24px serif for something that's
     pure placeholder, competing with an actually-typed word instead of
     obviously giving way to it. Dial it back further here specifically,
     rather than lightening --muted itself and under-weighting real UI text
     that uses it elsewhere. */
  #word::placeholder { color: var(--muted); opacity: .65; }
  /* 24px serif reads fine on desktop, but a narrow phone (or any touch
     device, which is never far from narrow) squeezes #word between the
     nav arrows and the Rhyme button -- the placeholder was landing right
     at the edge of legible. Small screens AND touch, not just one: a
     touch tablet in landscape has the width but not the precision for a
     tiny target, so this only shrinks the text, never the tap area. */
  @media (max-width: 480px), (hover: none) {
    #word { font-size: 19px; }
  }
  /* Below ~440px the nav arrows + Rhyme button alone can eat most of the
     bar's width, leaving #word only a few characters of room (it can shrink
     to min-width:0 but has nothing left to shrink INTO). Wrap into two rows
     instead: the input gets the full width on its own top row, nav arrows
     and Rhyme sit together on the row underneath -- still one bar visually
     (shared border/background/padding via .search), just tall instead of
     wide. */
  @media (max-width: 440px) {
    .search { flex-wrap: wrap; padding: 10px 10px 8px; }
    #word { flex-basis: 100%; order: 1; padding: 4px 0 8px; }
    .navbtns { order: 2; }
    /* Needs an explicit order like everything else here: unordered flex items
       default to 0, which put the clear button on its own row ABOVE the input
       it belongs to. Sits with the nav arrows instead -- the small round
       buttons together on the left, Rhyme still pushed right. */
    #wordClear { order: 3; }
    #go { order: 4; margin-left: auto; }
  }
  /* The shared muted-gradient button fill (see --btn-orange in :root) --
     reads as a soft, pressable surface rather than a stark block of pure
     orange. The main CTA gets its own full-saturation gradient rather than
     the shared muted --btn-orange token -- true brand orange at the bottom
     stop in both themes, just a light touch of white up top, no blend
     toward --muted. */
  #go {
    flex-shrink: 0; border: 1px solid var(--orange); color: var(--btn-text); cursor: pointer;
    background: linear-gradient(180deg, color-mix(in srgb, var(--orange) 70%, white) 0%, var(--orange) 100%);
    font: 600 15px var(--sans); padding: 11px 20px; border-radius: 10px;
  }
  #go:hover {
    background: linear-gradient(180deg, color-mix(in srgb, var(--orange-dark) 70%, white) 0%, var(--orange-dark) 100%);
    border-color: var(--orange-dark);
  }
  #go:active { transform: translateY(1px); }
  /* Busy state while a lookup is in flight -- blocks a double-submit and
     gives a visible cue beyond the "…" status text alone. */
  #go:disabled { opacity: .55; cursor: default; transform: none; }

  /* Back / forward through the words you've looked at. */
  .navbtns { display: inline-flex; flex-shrink: 0; gap: 4px; }
  .navbtn {
    border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
    border-radius: 8px; cursor: pointer; font: 500 16px var(--sans);
    width: 32px; height: 32px; line-height: 1; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    transition: border-color .12s, color .12s, opacity .12s;
  }
  .navbtn:hover:not(:disabled) { border-color: var(--green); color: var(--green); }
  /* Disabled keeps its shape (dashed outline, no fill) at full strength, so
     the arrows read as "nothing that way yet", not as missing. */
  .navbtn:disabled {
    opacity: 1; cursor: default; background: transparent;
    border-style: dashed; color: var(--muted);
  }
  /* Clears the text in #word only -- the last-run results stay on screen
     until a new word is actually searched, so this is a typing shortcut
     for backspace-holding, not a "start over" action. Hidden (not just
     dimmed) while the field is empty: an always-visible clear button next
     to an empty field has nothing to do. */
  /* Wears .navbtn's border, fill and hover so it belongs to the same family
     as the arrows beside it, but sized to its label instead of a 32px square.
     It started as a bare "×" and that failed: unlabelled, sat next to two
     other small square buttons, and cleared only the field -- so clicking it
     looked like nothing had happened. The word says what it does.

     Height deliberately left to .navbtn, which grows to 44px on touch --
     an id rule pinning it to 32px would quietly undo that touch target. */
  #wordClear {
    flex-shrink: 0; width: auto; padding: 0 12px;
    font: 600 12px var(--sans);
  }
  #wordClear[hidden] { display: none; }

  .viewbar {
    display: flex; flex-wrap: wrap; gap: 14px 22px;
    color: var(--muted); font-size: 13px; align-items: center;
  }
  /* Simplified view (permanent, no longer a toggle -- <body> always carries
     .simple): for someone who just wants to type a word and get rhymes, the
     Help row is overhead, not something they're choosing between -- hiding
     it puts the search box directly above the results/Your list for free,
     since that's already their document order. Filters stays put as a
     permanent drawer trigger under the search box either way (see
     .filterspanel below) -- nothing disabled, just out of the way. */
  body.simple .viewbar { display: none; }
  /* Filters: the status line doubles as the drawer's toggle -- always on
     screen (not swapped for a separate summary once opened), showing the
     live filter summary whether the drawer below it is open or closed. Not
     a native <details>, which snaps open/closed with no animation and its
     own inconsistent marker -- the chevron rotates in place instead of
     swapping glyphs, and the slide itself is a CSS grid-row transition.

     One box, not two: the card look (background/border/radius/shadow) lives
     ONLY on .filterspanel, the outermost element -- the trigger and the
     slide-open content beneath it are both plain, unstyled regions inside
     that single box, so opening it reads as the box itself growing taller,
     not a second card arriving underneath the first with a gap in between.
     A thin border-top on the content (see .filterspanel-inner/.helpinner
     below) is the only seam between the two regions. */
  .filterspanel {
    /* Side margins were 4px, which inset Filters and Help from the search box
       and "Your list" either side of them -- four stacked cards on three
       different left edges. They all share one edge now. */
    margin: 6px 0 0;
    /* A touch toward --border, not plain --surface -- --surface is pure white
       in light theme (identical to popovers/tooltips, which are meant to
       pop), so a box this size in that exact color read as a stark, separate
       card rather than a quiet home for settings. --bg was tried first and
       didn't move the needle (light theme's --bg is #fbfbf8, itself nearly
       white); --border has enough tonal distance from --surface in both
       themes to actually read as toned down at a small blend ratio. */
    background: color-mix(in srgb, var(--surface) 80%, var(--border) 20%);
    border: 1px solid var(--border); border-radius: 12px;
    box-shadow: var(--shadow);
    transition: border-color .15s;
  }
  .filterspanel:hover { border-color: var(--green); }
  /* Help gets a more muted background than Filters, on top of its own
     border/icon accent -- same ingredients, just leaned further toward
     --border, so the two panels read as different at rest, not just when
     open or hovered. */
  #helpPanel { background: color-mix(in srgb, var(--surface) 65%, var(--border) 35%); }
  #helpPanel:hover { border-color: var(--green-dark); }
  .filterstatus-row { display: flex; align-items: center; gap: 4px; }
  .filterstatus {
    display: flex; align-items: center; gap: 8px; width: 100%; min-width: 0; text-align: left;
    cursor: pointer; user-select: none;
    margin: 0; padding: 8px 12px;
    background: transparent; border: 0;
    color: var(--text); font: 600 13px var(--sans);
  }
  /* Sits just outside the status button (a button can't nest another
     button), opening downward -- unlike .fmt-pop's upward-opening popover,
     this bar sits near the top of the page, so there's room below it. */
  .filterstatus-qmark { flex: none; margin-right: 10px; }
  #filterstatus-pop {
    bottom: auto; top: 26px; right: 10px;
    font: 400 13px var(--sans);
  }
  #filterstatus-pop div + div { margin-top: 4px; }
  .filterstatus-text {
    overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
    color: var(--muted); font-weight: 400;
  }
  .filterstatus b { font-weight: 600; color: var(--muted); }
  /* Phone: let the summary wrap onto a second line so nothing is cut to
     "Groups:..."; the i popover still has the one-per-line version. */
  @media (max-width: 600px) {
    .filterstatus { height: auto; min-height: 34px; align-items: flex-start; }
    .filterstatus-text { white-space: normal; overflow: visible; text-overflow: clip; line-height: 1.45; }
  }
  .filterschevron { color: var(--muted); flex: none; transition: transform .2s ease; }
  .filtersicon { color: var(--muted); flex: none; margin-left: auto; }
  /* Help is the secondary bar next to Filters -- quieter at rest, full
     contrast on hover/open so it's never actually hard to find or read.
     The quieting rides on the chevron alone, not an opacity on the whole
     button: fading the button faded its "Help" label with it, which put the
     label at 2.3:1 against the panel. The panel's own tinted background is
     what makes this bar read as secondary now. */
  #helpToggle .filterschevron { opacity: .55; transition: opacity .15s ease; }
  #helpToggle:hover .filterschevron,
  #helpPanel[data-open="true"] #helpToggle .filterschevron { opacity: 1; }
  /* Help's own accent -- a darker, more subdued green than Filters' own
     --green border, in both themes -- otherwise the two bars are only told
     apart by their label text, and an open Help drawer looked identical to
     an open Filters drawer at a glance. Orange was tried first and read as
     too loud/alarm-colored for a passive reference panel. */
  #helpToggle .filtersicon { color: var(--green-dark); }
  .filterspanel[data-open="true"] { border-color: var(--green); }
  #helpPanel[data-open="true"] { border-color: var(--green-dark); }
  .filterspanel[data-open="true"] .filterschevron { transform: rotate(180deg); }
  /* The slide itself: a 0fr/1fr grid-row animates height without ever
     measuring the content's pixel height in JS. .filterspanel-body (the grid
     item) needs overflow:hidden + min-height:0 so it actually clips down to
     nothing while the row collapses -- padding can't live on that same
     element, since a box's own padding never shrinks below its set value no
     matter how far its content collapses, which left a permanent sliver at
     rest. Padding (and the divider border-top) instead lives on
     .filterspanel-inner, one level down, which has no min-height override
     and so is free to be clipped away entirely by its overflow:hidden
     parent. No margin-top here any more -- see the note above the outer
     .filterspanel rule for why the gap that used to sit here is gone. */
  .filterspanel-wrap {
    display: grid; grid-template-rows: 0fr;
    transition: grid-template-rows .25s ease;
  }
  .filterspanel[data-open="true"] .filterspanel-wrap { grid-template-rows: 1fr; }
  .filterspanel-body { overflow: hidden; min-height: 0; }
  /* ...but overflow:hidden also clips the Part of speech/Rhyme type/Layers
     popovers below, which pop out past the drawer's own edge via
     position:absolute (see .layerdd .layermenu). Once the drawer is fully
     open (not mid-transition), let it overflow again so those popovers
     aren't cut off -- closing snaps data-open back to false immediately, so
     the collapse animation is still clipped correctly the instant it
     starts. */
  .filterspanel[data-open="true"] .filterspanel-body {
    overflow: visible;
    /* Hold the clip until the slide has finished (same .25s as the grid-rows
       transition above). Switching to visible the instant data-open flips let
       the drawer's content spill out over Help and Your list while the row
       was still growing, then snap into place -- the page seemed to jump.
       allow-discrete is what lets `overflow` take a delay; a browser without
       it just falls back to the old immediate switch. */
    transition: overflow 0s linear .25s allow-discrete;
  }
  .filterspanel-inner {
    padding: 14px 16px; border-top: 1px solid var(--border);
  }
  /* General Audiences sits above this as a plain toggle; .ga-scope is the
     literal bracket around the three filters it applies to (Part of
     speech, Rhyme type, Groups) -- drawn with ::before rather than left to
     a tooltip, so "this wraps everything below it" reads as a shape, not
     just a sentence. A square-bracket line ([, not a curly brace -- easier
     to render crisply at this size and just as legible as "encloses this
     group") in three border segments (top/left/bottom, no right), colored
     by JS toggling .ga-active to match the toggle's own on/off state:
     green while General Audiences is actively filtering everything inside,
     muted the moment it's off (nothing extra being held back). */
  .ga-scope {
    position: relative; margin-top: 10px; padding: 4px 0 4px 18px;
    display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: flex-start;
  }
  /* A real element, not ::before -- pseudo-element computed style proved
     unreliable to query/verify (reported stale border-color after a class
     change even though classList and the underlying var were both
     confirmed correct), so this uses an actual empty, aria-hidden span
     instead. Same visual result, but its style is directly inspectable. */
  .ga-bracket {
    position: absolute; left: 2px; top: 1px; bottom: 1px; width: 9px;
    border-radius: 5px 0 0 5px;
    border-top: 2px solid var(--border); border-left: 2px solid var(--border);
    border-bottom: 2px solid var(--border);
    transition: border-color .15s; pointer-events: none;
  }
  .ga-scope.ga-active .ga-bracket { border-color: var(--green); }
  .fcount { color: var(--muted); font-weight: 500; }
  /* Each filter group's checklist stacks in a single column inside its
     dropdown -- consistent with the Layers dropdown this pattern was lifted
     from, and a popover is narrow enough that multiple columns would just
     wrap anyway. */
  .fdd .layermenu { display: flex; flex-direction: column; gap: 8px; color: var(--muted); font-size: 13px; min-width: 210px; }
  .fdd .layermenu label { display: inline-flex; align-items: center; gap: 6px; color: var(--text); }
  .fdd .layermenu label abbr { text-decoration: none; cursor: help; display: inline-flex; }
  /* WCAG 1.4.3: these examples sit on the Rhyme-categories rows, which are
     tinted with the closure ramp itself -- so this is green text on a green
     wash, and it measured 2.34-2.79:1 depending on how strong that row's tint
     is. Seated harder toward --text than the other accents (50/50, not 80/20)
     because the backdrop is doing the same thing the foreground is; 50/50
     clears 4.5:1 against the strongest tint in the table with room to spare
     and still reads green rather than black. The row colour, not this text,
     is what carries the rung identity here. */
  .rt-ex {
    background: none; border: 0; padding: 0; font: inherit; font-size: 12px;
    font-style: italic; white-space: nowrap; cursor: pointer;
    color: color-mix(in srgb, var(--green) 50%, var(--text));
    border-bottom: 1px dotted currentColor;
  }
  .rt-ex:hover { color: var(--orange); }
  #rungDD .layermenu { min-width: 266px; gap: 4px; }
  /* Same closure-strength color ramp the results underline already uses
     (orange fading to grey) -- reused here as each row's own background
     tint so the relative strength of each rhyme type reads at a glance,
     without having to parse "very strong closure" vs. "weakest closure"
     as text each time. Full-bleed via negative margin so the tint reads as
     a row, not just a pill hugging the text. */
  .rt-row { width: 100%; padding: 5px 8px; margin: 0 -8px; border-radius: 6px; box-sizing: border-box; }
  /* Marks where the rhymes that share only a vowel (assonance) or only the
     ending consonants (consonance) begin. A plain labelled rule: text, not
     color, carries the meaning, and it holds no input so the filter
     script (which collects inputs only) never sees it. */
  .rt-divider { margin: 6px 0 2px; padding-top: 6px; border-top: 1px solid var(--border);
    font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
  .fhelp { color: var(--muted); vertical-align: -1px; transition: color .12s; }
  .fdd .layermenu label:hover .fhelp { color: var(--green); }
  /* Floats above the (narrow, 175-260px) checklist instead of squeezing into
     it -- an inline banner inside .layermenu had to wrap its own text across
     several lines just to fit the dropdown's width. Positioned relative to
     .layermenu (nearest positioned ancestor) but sized by its own content,
     so it reads as a small modal card centered over the checklist, wide
     enough to hold its message on one or two lines regardless of which of
     the three (differently-sized) dropdowns it belongs to. Dismissible by
     click -- see the delegated listener near FILTER_SECTIONS. */
  .filtererr {
    position: absolute; top: calc(100% + 8px); left: 50%; transform: translateX(-50%);
    width: max-content; max-width: 240px; z-index: 50; cursor: pointer;
    text-align: center; color: var(--danger); font-size: 12px;
    padding: 9px 14px; border: 1px solid var(--danger); border-radius: 8px;
    background: var(--surface); box-shadow: var(--shadow);
  }
  /* Same anchored-under-the-checklist shape as .filtererr, but advisory
     (orange) rather than a hard stop (red/--danger) -- checking Vulgar
     while General Audiences is on is a valid, allowed combination, just
     one worth a heads-up: some of what Vulgar would otherwise show stays
     hidden until General Audiences is off too. Never blocks the checkbox
     itself, only informs. */
  /* [hidden] must win here: an author `display` rule always beats the
     browser's own `[hidden] { display: none }` UA rule regardless of
     selector specificity, so without this override toggling the `hidden`
     attribute in JS (see syncGaState()) has no visual effect at all --
     the note shows unconditionally no matter what state it's set to. */
  .filternote[hidden] { display: none; }
  .filternote {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    color: var(--orange-dark); font-size: 12px; margin-top: 2px;
    padding: 6px 8px; border: 1px solid var(--orange); border-radius: 6px;
    background: color-mix(in srgb, var(--orange) 12%, transparent);
  }
  .filternote button {
    font: 600 12px var(--sans); color: var(--orange-dark);
    background: none; border: 1px solid var(--orange); border-radius: 5px;
    padding: 2px 7px; cursor: pointer; white-space: nowrap;
  }
  .filternote button:hover { background: color-mix(in srgb, var(--orange) 18%, transparent); }
  /* .viewbar is permanently hidden now -- Theme lives in the gear dropdown
     and Help is its own drawer, so there's nothing left to show here. */
  .viewbar { margin: 12px 4px 0; }
  /* Help drawer: reuses .filterspanel/.filterstatus/.filterspanel-wrap
     wholesale for the outer toggle+slide (same component as Filters, just a
     static "Help" label instead of a live summary) -- only the inside is
     different. Each topic stacks as a plain accordion row instead of the
     wrapping pill layout .ga-scope uses for checklists. */
  /* Was 6px, deliberately tighter than .filterspanel's own top margin, to
     read Filters and Help as a related pair rather than two unrelated
     sections. Reversed on purpose: at four stacked cards the varied gaps
     (0 / 10 / 6 / 14) read as drift rather than grouping, and an even
     rhythm is what makes a reference tool look considered. If the pairing
     needs saying again, say it with something other than 4px. */
  /* Help is reference, not a control: closed, it is just a quiet text row
     (no card chrome) that gains its border on hover and while open. A bigger
     gap above separates it from the search card. */
  #helpPanel { margin-top: 14px; }
  #helpPanel[data-open="false"] { background: transparent; border-color: transparent; box-shadow: none; }
  #helpPanel[data-open="false"]:hover { border-color: var(--green-dark); }
  .helpinner {
    padding: 6px 16px; border-top: 1px solid var(--border);
    display: flex; flex-direction: column;
    /* Capped, so an open Help does not push the results a page away: it
       scrolls inside itself instead. */
    max-height: min(60vh, 520px); overflow-y: auto; overscroll-behavior: contain;
  }
  .helpacc { border-top: 1px solid var(--border); }
  .helpacc:first-child { border-top: 0; }
  .helpacc > summary {
    cursor: pointer; list-style: none;
    padding: 12px 4px; display: flex; align-items: center; justify-content: space-between; gap: 10px;
    font: 600 14px var(--sans); color: var(--text);
  }
  .helpacc > summary::-webkit-details-marker { display: none; }
  .helpacc > summary:hover { color: var(--green); }
  .helpacc-chevron { color: var(--muted); flex: none; transition: transform .2s ease; }
  .helpacc[open] > summary .helpacc-chevron { transform: rotate(180deg); }
  .helpacc-label { display: flex; align-items: center; gap: 9px; min-width: 0; }
  .helpacc-icon { flex: none; color: var(--leaf-dark); opacity: .9; }
  .helptext { padding: 0 4px 18px; }
  .helptext dl { margin: 14px 0; }
  .helptext dt { font-weight: 700; color: var(--green); margin-top: 12px; }
  /* A dt subordinate to the section's own green heading (e.g. author names
     under "How this fits the Berklee Method") -- plain text color keeps it
     from reading as a second, competing section title, especially now that
     links share the same green. */
  .helptext dt.dt-sub { color: var(--text); }
  .helptext dt .dt-example { display: block; color: var(--text); font-weight: 400; font-style: italic; margin-top: 2px; }
  /* Same shared gradient as .rt-row (the Rhyme type filter), applied to
     this table's own rows via inline style per entry -- one gradient, one
     source of truth (--rt-* in :root), two places it shows up. */
  .helptext dt.cat-row { padding: 6px 10px; margin: 12px -10px 0; border-radius: 8px; }
  .helptext dd { margin: 2px 0 0; color: var(--muted); }
  .helptext dd b { color: var(--text); font-weight: 600; }
  /* Plain <a> links here (Wiktionary, book links, etc.) had no color of
     their own, so they fell back to the browser's default blue -- unreadable
     against the dark theme's near-black surface. Same green/orange pairing
     as the footer's own links. */
  .helptext a { color: var(--green); }
  .helptext a:hover { color: var(--orange); }
  /* Book links get the brand orange instead of the green used for reference
     links (Wiktionary, WordNet, etc.) -- sets them apart as the one place
     in Help that leads off-site to a purchase. Hover swaps to green, same
     pairing as everywhere else, just inverted. */
  .book-row a { color: var(--orange); }
  .book-row a:hover { color: var(--green); }
  /* Book links get a cover thumbnail instead of a bare underlined link --
     the row wraps on narrow screens rather than clipping the title. Real
     cover art isn't a uniform 2:3 ratio across editions, so the image sits
     inside a fixed-size frame and is letterboxed (object-fit: contain)
     rather than stretched to fill it -- every row lines up the same
     regardless of each cover's actual shape. */
  .book-row { display: flex; align-items: flex-start; gap: 12px; margin: 12px 0; padding-bottom: 12px; border-bottom: 1px solid var(--border); }
  .book-row:last-child { border-bottom: 0; padding-bottom: 0; }
  .book-thumb-frame { width: 46px; height: 66px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
  .book-thumb { max-width: 100%; max-height: 100%; border-radius: 2px; box-shadow: 0 1px 4px rgba(0,0,0,.3); }
  .book-note { display: block; font-size: 12px; font-style: italic; color: var(--muted); margin-top: 2px; }
  .helptext dd table { border-collapse: collapse; margin: 8px 0 4px; font-size: 12px; }
  .helptext dd th, .helptext dd td { padding: 3px 10px 3px 0; text-align: left; vertical-align: top; }
  .helptext dd th { color: var(--text); font-weight: 600; border-bottom: 1px solid var(--border); }
  .helptext dd td { border-bottom: 1px solid var(--border); }
  .helptext dd tr:last-child td { border-bottom: 0; }
  select {
    border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
    border-radius: 8px; padding: 5px 8px; font-size: 13px;
  }
  /* Fixed to fit its longest option ("Entire dictionary") so picking a
     shorter one doesn't shrink the box and shift anything next to it.
     Shortened from 230px alongside the option labels themselves (dropped
     "words"/the parenthetical count) specifically to get Part of speech +
     Rhyme type + Groups to fit on one row instead of Groups wrapping to
     its own line -- see .ga-scope's flex-wrap. */
  .spacer { flex: 1; }

  /* Layers: a select-look dropdown that opens to CHECKBOXES (layers combine,
     so a single-choice select won't do). Plain <details>, closed on outside
     click by a small script. */
  .layerdd { position: relative; display: inline-block; }
  .layerdd summary {
    list-style: none; cursor: pointer; user-select: none;
    border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
    border-radius: 8px; padding: 5px 8px; font-size: 13px;
  }
  .layerdd summary::-webkit-details-marker { display: none; }
  /* A literal leading space in the ::after content rendered wider than the
     8px right padding it was meant to match, so the button looked lopsided
     even though the padding itself was symmetric. A fixed margin-left
     instead of a space character keeps it exact. */
  .layerdd summary::after { content: "▾"; color: var(--muted); font-size: 12px; margin-left: 5px; }
  .layerdd[open] summary { border-color: var(--green); }
  .layerdd .layermenu {
    position: absolute; top: calc(100% + 6px); left: 0; z-index: 20;
    display: flex; flex-direction: column; gap: 8px; min-width: 175px;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 10px; padding: 12px 14px; box-shadow: var(--shadow);
  }
  .layerdd .layermenu label { color: var(--text); }
  .layerdd .layermenu input { accent-color: var(--green); }
  .layerdd .layermenu input:disabled { cursor: not-allowed; }
  /* Opacity on the whole label (not just a recolored text) so the row reads
     as disabled at a glance -- a muted-grey text color alone was too close
     to the enabled rows' own text color to register as "off" here. */
  .layerdd .layermenu label:has(input:disabled) { opacity: .5; cursor: not-allowed; }
  /* Only the gear menu needs an internal heading -- Part of speech/Rhyme
     type/Layers are already named by the visible label text next to their
     trigger, but the gear button is icon-only, so its popover would open
     with no indication of what it is. */
  .menuhead {
    font: 700 12px var(--sans); letter-spacing: .4px; text-transform: uppercase;
    color: var(--muted); padding-bottom: 2px; margin-bottom: 2px;
    border-bottom: 1px solid var(--border);
  }

  /* Gear trigger: same dropdown mechanics as the filter menus above, but an
     icon-only button (Theme isn't a filter, so it doesn't belong in that
     row) rather than a "value ▾" summary. Deliberately quiet -- it used to
     carry a solid fill, border, and shadow (a bordered light-grey circle in
     dark theme especially), which read as the single most prominent control
     on the page for a settings menu nobody needs to open often. Transparent
     at rest in both themes now, same "fades in toward the icon it actually
     is" treatment as the per-word look/eye/thes icons, rather than looking
     like a card floating over the page. */
  .gearbtn summary {
    display: inline-flex; align-items: center; justify-content: center;
    width: 40px; height: 40px; padding: 0; color: var(--muted);
    background: transparent; border: 1px solid transparent; border-radius: 10px;
    opacity: .6; transition: opacity .15s ease, border-color .15s ease, color .15s ease;
  }
  .gearbtn summary::after { content: none; }
  .gearbtn summary:hover, .gearbtn[open] summary {
    opacity: 1; color: var(--text); border-color: var(--border);
  }
  /* Config lives at the top-right of the page, like a settings icon in an
     app's corner, instead of buried mid-page among the result filters.
     Tracks the right edge of .wrap (max-width: 760px), not the browser
     window -- on a wide monitor the content column sits far from the
     viewport edge, and a viewport-relative `right` stranded the button out
     in empty space past it. No fill/shadow of its own now -- the summary
     above carries what little chrome the button has.

     position: absolute, not fixed -- it used to stay pinned to the
     viewport through the whole scroll, floating over results long after
     the header it belongs with had scrolled away. Absolute anchors it to
     the page instead (no positioned ancestor between it and <html>, so
     top/right measure from the document, not the viewport), so it scrolls
     away with the header like any other part of the page. */
  .gearbtn-fixed {
    position: absolute; top: 16px; right: max(16px, calc((100vw - 760px) / 2));
    z-index: 10;
  }
  .gearbtn-fixed .layermenu { left: auto; right: 0; }
  @media (max-width: 480px) {
    .gearbtn-fixed { top: 10px; right: 10px; }
  }
  .gearbtn .layermenu { min-width: 150px; }
  /* Match the 13px used by the select and the other filter menus -- the
     labels otherwise fall back to the body's 15px and read as a size bump
     with no purpose next to the 13px control beside them. */
  /* Text always left-aligned, control always right-aligned -- justify-content:
     space-between alone doesn't do that here, since the Theme row has its
     label text BEFORE the <select> while the checkbox rows have their
     <input> BEFORE the text, so space-between was pushing each row's text to
     opposite sides. `order` moves the checkbox after its text (matching
     Theme's already-last select), then margin-left: auto pushes whichever
     control is last flush right in both cases. */
  .gearbtn .layermenu label {
    display: flex; align-items: center; gap: 10px; font-size: 13px;
  }
  .gearbtn .layermenu label select,
  .gearbtn .layermenu label input[type="checkbox"] { margin-left: auto; }
  .gearbtn .layermenu label input[type="checkbox"] { order: 1; }
  /* Quieted from a full --green: with the orange CTA button and the yellow
     wordmark both live above this row, a third bright accent read as three
     competing hues rather than one system. Blending green toward --muted
     keeps just enough hue to still register as "this is a link" without
     contending with orange as the page's lead color. */
  .link { color: color-mix(in srgb, var(--green) 55%, var(--muted)); cursor: pointer; text-decoration: none; }
  .link:hover { text-decoration: underline; }

  /* Plain underline for the title -- see index.html's own comment on why
     this one carries no leaf. .brand is an inline-flex column sized to its
     widest line ("Rhyme Dictionary"), so stretching this to that same
     width makes it read as the title's own underline instead of
     shrink-wrapping to nothing (align-items: center on .brand) or
     spanning the full page (unrelated to the title). */
  .title-rule {
    align-self: stretch; height: 1px; background: var(--border);
    margin: 10px 0 2px; opacity: .6;
  }
  /* The leafed divider between a search's word display and its rung
     groups (built per-group in render(), see RUNG_GROUPS in app.js) --
     the WilderCode leaf-W mark, same paths as branding/wilder-mark.svg
     recentered to this viewBox, echoing the header logo instead of a
     generic rule. */
  .flourish { display: flex; align-items: center; gap: 14px; margin: 0 0 10px; opacity: .6; }
  .flourish-line { flex: 1; height: 1px; background: var(--border); }
  .flourish-icon { flex: none; }
  #status { text-align: center; color: var(--muted); margin: 40px 0; min-height: 1em; }
  #status:empty { margin: 0; min-height: 0; }

  .query { text-align: center; margin: 18px 0 22px; }
  .query .w { font: 400 42px/1 var(--serif); letter-spacing: .5px; }
  .query .w .rime { border-bottom: 2px solid var(--green); }
  .query .w .sv { color: var(--orange); font-weight: 700; }
  .query .scan, .chiptip .scan {
    display: flex; gap: 9px; align-items: center; margin-top: 13px;
  }
  .query .scan { justify-content: center; }
  .chiptip .scan { justify-content: flex-start; }
  .query .dot, .chiptip .dot { display: inline-block; border-radius: 50%; }
  .query .dot.s1, .chiptip .dot.s1 { width: 11px; height: 11px; background: var(--orange); }
  .query .dot.s2, .chiptip .dot.s2 { width: 9px; height: 9px; background: var(--green); }
  .query .dot.s0, .chiptip .dot.s0 { width: 5px; height: 5px; background: var(--muted); }
  .query .dot.anchor, .chiptip .dot.anchor { box-shadow: 0 0 0 3px var(--green-glow); }
  .query .t {
    color: var(--muted); font: 12px/1 var(--mono); margin-top: 12px; letter-spacing: 1px;
  }
  .query .say {
    color: var(--muted); font: 13px/1.3 var(--mono); margin-top: 10px;
    letter-spacing: .3px;
  }
  .query .say b { color: var(--text); font-weight: 600; }
  .pronrow { display: flex; gap: 8px; justify-content: center; margin-top: 14px; }
  .pronchip {
    font: 600 12px/1 var(--mono); letter-spacing: .5px; color: var(--muted);
    background: var(--surface); border: 1px solid var(--border); border-radius: 20px;
    padding: 5px 12px; cursor: pointer; transition: background .12s, color .12s, border-color .12s;
  }
  .pronchip.say { letter-spacing: 0; text-transform: none; }
  .pronchip:hover { border-color: var(--green); color: var(--text); }
  .pronchip.active {
    background: var(--btn-green); border-color: var(--green); color: var(--btn-text-onlight, var(--btn-text));
  }

  .rung {
    background: var(--rung-bg); border: 1px solid var(--border);
    border-left: 4px solid var(--rc); border-radius: 12px;
    padding: 14px 18px; margin: 12px 0; box-shadow: var(--shadow);
  }
  /* WCAG 1.4.3: the closure ramp (--rc) is tuned to read as a *ramp* against
     the rung card, which means its faint end -- consonance especially -- lands
     well under 4.5:1 as 13px bold text (measured 2.35:1 on light, 2.99:1 on
     dark). The ramp itself is not the problem: as a 4px left border it is a
     non-text component and only owes 3:1, and it is CVD-verified as it stands.
     So the border keeps --rc untouched and only the *heading text* is pulled
     toward the theme's own text colour -- darker on light, lighter on dark,
     automatically, including under the colourblind-friendly palettes, which is
     why this is a color-mix rather than eight more hand-picked tokens to keep
     in step across six palettes. 60/40 is the ratio that clears 4.5:1 on every
     rung in every theme with margin (worst case: consonance, 4.94:1 on light).
     Adjacent rungs stay as distinguishable from each other as they were --
     every stop moves the same way, so the ramp is preserved, just seated. */
  /* The ladder's two halves. Quiet by design: this is a signpost between
     groups of cards, not a competing headline -- the rung names underneath
     are what you actually scan for. A hairline rule carries the division and
     the type stays small, so it reads as structure rather than as another
     result. */
  /* Help's version of the same two-group split the results ladder shows.
     Same words, same order, so the table and the ladder teach one thing. */
  .cat-group {
    font: 700 11px var(--sans); letter-spacing: .8px; text-transform: uppercase;
    color: var(--text); margin: 14px 0 6px;
  }
  .helptext > .cat-group:first-child { margin-top: 0; }
  .cat-groupnote {
    font: 400 12px/1.5 var(--sans); color: var(--muted);
    margin: 0 0 8px; max-width: 66ch;
  }

  .rung-group { margin: 26px 0 4px; }
  .rung-group:first-child { margin-top: 8px; }
  .rung-group-t {
    font: 700 12px var(--sans); letter-spacing: .8px; text-transform: uppercase;
    color: var(--text); margin: 0 0 3px;
  }
  .rung-group-s {
    font: 400 12px/1.45 var(--sans); color: var(--muted);
    margin: 0; max-width: 66ch;
  }

  .rung h3 {
    font: 700 13px var(--sans); letter-spacing: .3px; margin: 0 0 2px;
    color: color-mix(in srgb, var(--rc) 60%, var(--text));
    display: flex; align-items: baseline; gap: 10px;
  }
  .rung h3 .cl { color: var(--muted); font-weight: 500; }
  .rung .rung-quote { margin: 0 0 .5rem; color: var(--muted); font-size: .9em; }
  .rung .rung-quote b { color: var(--rc); }
  .rung h3 .ct {
    color: var(--muted); font: 500 12px var(--mono); margin-left: auto;
  }
  /* The rung's rule, restated on every search rather than once in a Help
     accordion -- "educational via use". Quiet enough not to compete with
     the header line above it, but present every time. */
  .rung .principle {
    color: var(--muted); font-size: 12px; margin: 0 0 8px;
  }
  /* The source word rides in every rung header, so "what am I rhyming?"
     stays on screen at any scroll depth. */
  .rung h3 .rsrc {
    font: 400 16px var(--serif); font-weight: 400; color: var(--text);
  }
  .rung h3 .rsrc .sv { color: var(--orange); font-weight: 700; }
  .rung h3 .rsrc .rime { border-bottom: 2px solid var(--u-perfect); padding-bottom: 1px; }
  /* Once the header may wrap, an auto margin on the count opens a gap that
     eats the remaining width and strands "· 78 words" on a line of its own
     at phone widths, so the count sits left there. */
  @media (max-width: 1180px) {
    .rung h3 { flex-wrap: wrap; }
    .rung h3 .ct { margin-left: 0; }
  }
  .words { margin-top: 6px; }
  .sylhead {
    font: 600 12px var(--sans); letter-spacing: .5px; text-transform: uppercase;
    color: var(--muted); margin: 12px 0 2px;
  }
  /* CSS multi-column, not flex/inline flow: words are already alphabetical
     within a syllable group, and a multi-column box reads top-to-bottom per
     column before moving to the next one -- alphabetical DOWN each column,
     not across the row, with the column count falling out of the available
     width for free (no JS layout math needed). */
  .sylline {
    font: 400 19px/1.9 var(--serif); word-spacing: 2px;
    /* column-count left unset (auto): the browser packs in as many
       column-width-ish columns as the container can fit, so a wide rung
       card fills itself with 2-3 columns instead of one long single-file
       list, and a narrow one falls back to 1 column on its own -- no
       breakpoint-specific column count to maintain. 210px is a measured
       safety margin: the longest real chip content found (a 13-letter
       word plus its lookup/thesaurus/insight icons, e.g. "guidelessness")
       renders at ~187px, so 210px never wraps it mid-word even though it's
       comfortably narrower than the old fixed 1-column layout, which threw
       away all the rest of a wide rung card's width on lists that could
       have used 2-3 columns. */
    column-width: 250px; column-gap: 18px;
    /* Re-measured 2026-10-04: the three icon boxes grew to 24px (WCAG 2.5.8)
       after the 210px/160px figures above were taken, and a chip's content
       now needs up to ~211px (2 syllables) / ~187px (1 syllable) in a
       sample, more for 13-letter words. Columns narrower than that let the
       icons spill out of the green picked box. */
  }
  .sylline[data-syl="1"] {
    /* Same auto-fill approach, tighter minimum: 1-syllable chips are
       shorter, so 200px (vs. 250px above) is the measured safety margin --
       the widest real chip in a sample ("freaked" + its three icons) needs
       ~187px. Letting column-count fall out of the width instead of a
       fixed cap means a wider rung card (or a future wider page) keeps
       adding columns on its own rather than plateauing once the container
       outgrows whatever number was hardcoded here. */
    column-width: 200px; column-gap: 18px;
  }
  /* Below ~600px a phone-width rung card is too narrow for even one 160px/
     210px desktop column plus its gap without squeezing -- a longer word
     plus its part-of-speech marker and two icons wants more room than that
     (e.g. "clear" wants 150px in a 138px column). Rather than drop every
     group to one column (twice the scrolling on a phone), let the chip
     wrap onto a second line instead of spilling into the next column, and
     force exactly two columns -- but only for the 1-syllable group within
     a rung. column-width must be pulled back down here too (not just
     column-count): the desktop rule's 160px minimum, combined with a
     column-count of 2, would otherwise let the browser collapse to a
     single column the moment the phone's real width falls under ~334px
     (2*160 + 14 gap) -- see the CSS spec note above on column-width being
     a true minimum, not just a desktop-only hint. Longer words (2+
     syllables) are wide enough on their own that two narrow columns means
     near-constant wrapping, so those groups drop to one column same as
     desktop would effectively be; short 1-syllable words comfortably hold
     two. */
  @media (max-width: 600px) {
    .sylline { column-width: auto; column-count: 1; }
    .sylline[data-syl="1"] { column-width: 130px; column-count: 2; column-gap: 14px; }
  }
  .chip {
    cursor: pointer; border-radius: 6px;
    /* Extra bottom padding, not a symmetric 2px/2px: the serif font's
       line-height leading splits unevenly above vs. below the glyphs
       (measured ~7px above, ~4px below the underline at 2px/2px), invisible
       normally but obvious once .chip.picked draws a box around it. This
       equalizes the picked box's padding without moving the underline
       itself, which sits at a fixed offset from the text everywhere else. */
    padding: 2px 7px 5px; transition: background .12s, box-shadow .12s;
    white-space: nowrap; display: inline-block; width: 100%; break-inside: avoid;
  }
  /* The word inside the chip is the real add-to-list button now (WCAG 4.1.2 --
     see chip() in app.js). It used to be the chip that took focus, so the
     focus ring needs to follow the role down here or keyboard users lose the
     "where am I" marker that the 2026-08-27 pass put in. Offset outward so the
     ring clears the syllable dots and the rime underline rather than sitting
     on top of them. The hc/hc-light themes' *:focus-visible rule overrides the
     colour; this is the default- and light-theme ring. */
  .chip .wtext:focus-visible {
    outline: 2px solid var(--green); outline-offset: 2px; border-radius: 4px;
  }
  /* This has to live AFTER the base .chip rule above, not inside the
     .sylline media query further up -- same selector, same specificity
     (0,0,1,0), so it's a pure source-order tie-break either way. Nesting
     it up there with the .sylline overrides put it BEFORE the base rule
     instead, which silently lost every time regardless of viewport width:
     chips never actually wrapped on phones, they just overflowed their
     narrow columns with white-space staying nowrap. Found while widening
     the rung column layout for desktop -- unrelated to that change, but
     sitting right next to it. */
  @media (max-width: 600px) {
    .chip {
      white-space: normal;
      /* Flex row, not plain inline flow: the word and the icon group are two
         units that wrap against each other, so a long word pushes the whole
         icon row down as a block instead of letting the icons wrap one at a
         time. margin-left:auto keeps the icons trailing a short word on the
         same line (right-aligned in the column) and only spends a second
         line when the word actually needs the width -- a 1000-word rung
         can't afford two lines per chip unconditionally. */
      display: flex; flex-wrap: wrap; align-items: center; row-gap: 2px;
    }
    /* min-width:0 is what makes the word yield: without it a flex item
       refuses to shrink below its content width and shoves the icons out
       of the chip rather than wrapping at its own <wbr> syllable breaks. */
    .chip .wtext { flex: 1 1 auto; min-width: 0; }
    /* Left-aligned under the word, not right-aligned and not centered.
       Right-aligning (the first attempt) floated the icons off in the
       column's empty right edge and read as detached from the word they
       belong to; centering breaks the column's left edge and leaves the row
       hovering between two words. Below rather than above, because a row
       placed above sits against the PREVIOUS word's baseline. */
    .chip .acts { margin-left: 0; }
  }
  /* The three per-word controls as one atomic unit -- inline-flex, so the
     row can move to a new line but can never split 1+2 across lines. */
  .chip .acts { display: inline-flex; align-items: center; flex: 0 0 auto; }
  /* Above the phone breakpoint the icon row used to sit at 0 gap from the
     word -- correct for the mobile stacked layout (its own indented line
     below the word, see the media query beneath), but on one nowrap line
     it read as icons crammed against the last letter. A small fixed gap
     here, not on .acts itself, so the phone rule's margin-left:auto/0 below
     still owns the mobile spacing untouched. */
  @media (min-width: 601px) {
    .chip .acts { margin-left: 6px; }
  }
  /* Below 600px the icon row ALWAYS takes its own line, rather than trailing
     a short word and dropping only when the word needs the width. That used
     to be a deliberate saving -- "a 1000-word rung can't afford two lines per
     chip" -- but measuring a real rung at 375px shows 239 of 278 chips
     already wrap, so the saving is 14% of rows bought with a layout whose
     row height changes word by word. Uniform costs those few extra lines and
     buys a rung you can read down: every word on its own line, every icon in
     the same three positions under it. (This has to sit AFTER the .acts base
     rule above -- identical specificity, so it is a pure source-order
     tie-break, the same trap documented on the .chip rule further up.)
     flex-basis:100% is what forces the break: an item that claims the whole
     line can never share one with the word before it. */
  @media (max-width: 600px) {
    .chip .acts {
      flex: 1 1 100%; min-width: 0;
      /* No mark at all -- just an indent. The icon row hangs under its
         word, stepped in from the word's own left edge so it reads as
         subordinate to that word rather than as a peer of the next one.

         ---- how this got here (skip unless changing it) ----

         Spacing alone was the original grouping, and 2px-to-its-own-word
         against ~36px-to-the-next-chip is a real signal -- but it's a
         relative one, and halfway down a long rung there's nothing on
         screen to compare against. So a 2px border-left was added as an
         absolute answer. It sat at the word's own left edge, though, so
         line and first letter shared one vertical, and down a 278-word
         rung that stacks into a broken, stuttering column competing with
         the words themselves.

         Eight variants were mocked side by side on 2026-09-02 (indented
         line, elbow, GA-style bracket, tray, nothing). A translucent tray
         shipped first and came back off the same day: once you notice that
         :hover and .picked already highlight the whole chip -- word and
         icons together, as one block -- the tray is a second answer to a
         question already answered, and a permanent one competing with the
         two that only appear when they're relevant. The indent alone
         carries it.

         The GA filter's square bracket (.ga-bracket) was the obvious
         precedent and lost on density: it appears once on screen, this
         mark would appear on every word, three strokes times 278 rows.

         What that leaves for touch, where :hover never fires: the indent,
         plus .picked on tap. Watch this one in real use -- if the grouping
         turns out not to read on a phone, the fix is to bring back the
         quietest of the eight, not to re-argue the whole set. */
      margin-left: calc(.8em + 14px);
    }
  }
  /* <wbr> sits between every syllable in .wtext (see the chip-building JS)
     specifically so the phone-width wrap above lands on a syllable
     boundary instead of splitting a syllable in half. But <wbr> is a
     forced break *opportunity* that browsers honor regardless of
     white-space -- it isn't scoped by the nowrap-vs-normal switch above,
     so a long/high-syllable word (circumnavigate) could still wrap mid-
     word above 600px even with white-space staying nowrap there, blowing
     the chip's height out to multiple lines. Hiding the <wbr> nodes above
     the phone breakpoint removes the break opportunity entirely, so nowrap
     actually means nowrap on desktop; below 600px they stay in the DOM and
     keep doing their job. */
  @media (min-width: 601px) {
    .chip wbr { display: none; }
  }
  /* Word-insight popover: sound, part of speech, then numbered senses.
     Shown on hover (mouse, decorative -- see .pinned below for the other
     case) and on tap of the "eye" icon (touch, or a deliberate click).
     pointer-events: none on the hover form so it never intercepts the
     click meant for whatever's under it, and so hovering can't chase the
     pointer into the popover itself. */
  .chiptip {
    position: fixed; z-index: 60; max-width: min(320px, calc(100vw - 16px));
    background: var(--surface); color: var(--text); border: 1px solid var(--border);
    border-radius: 8px; padding: 7px 11px; font: 500 12px var(--sans);
    line-height: 1.45; box-shadow: var(--shadow); pointer-events: none;
    /* Starts invisible; JS (fadeInPanel) adds .show a frame after insertion
       so this actually transitions instead of snapping straight to opaque --
       appearing instantly read as popovers ambushing the page as you swept
       the mouse across a rung. */
    opacity: 0; transition: opacity .12s ease;
  }
  /* Dark theme's --surface (#161b22) sits almost on top of --rung-bg
     (#1a212b), the rung cards this popover floats over -- close enough in
     luminance that the border alone wasn't enough separation and the
     popover read as blending into the card behind it. --surface-2 is
     dark theme's already-established "elevated panel" tone (used for the
     settings panel, inputs, etc.) -- reusing it here instead of a
     one-off color keeps the popover reading as the same kind of raised
     surface as everything else, just lighter than what it's floating
     over. Light theme's --surface (white) already contrasts fine against
     --rung-bg's off-white card, so it's untouched. */
  :root[data-theme="dark"] .chiptip { background: var(--surface-2); }
  /* A click/tap opens the SAME panel but pinned: it stays open until its
     own × button closes it (see dismissInsight) -- immune to an outside
     click, page scroll, or the pointer just wandering off, rather than
     closing the moment the pointer leaves like the hover form does.
     Standard tooltip-vs-popover split -- hover is a glance and shouldn't
     demand interaction, a deliberate click/tap is asking to read the whole
     thing, so THAT'S where clamped text (.ct-etym below) gets to un-clamp
     and the panel gets to scroll instead of just running off-screen.

     The scrolling itself happens on .ct-scroll, an inner wrapper (see
     showInsight) carrying the padding .chiptip normally has -- NOT on
     .chiptip directly. A close button that scrolled away with the content
     the moment you tried to read a long entry would defeat its own point;
     keeping it a sibling of .ct-scroll, outside the scrolling box, is what
     lets it stay fixed in the corner instead. */
  .chiptip.pinned { pointer-events: auto; padding: 0; }
  .chiptip.pinned .ct-scroll { max-height: 80vh; overflow-y: auto; padding: 7px 11px; }
  /* ONE dismiss button for every panel that can be closed -- the thesaurus
     and "show all" popups (.tclose), the help/scan panels (.helpclose), and
     the word-insight popover (.ct-close). These had drifted into three
     near-copies of the same rule (radius 8 vs 6, 20px vs 16px, different
     padding), so a "close" looked subtly different depending on which
     surface you were dismissing. Shape lives here; each surface below
     overrides only its own corner offset, and the popover its smaller
     scale. Muted until pointed at, then the same orange every other
     interactive control warms to. */
  .tclose, .helpclose, .ct-close {
    border: 0; background: transparent; color: var(--muted); cursor: pointer;
    font: 600 20px/1 var(--sans); padding: 2px 8px; border-radius: 8px;
    transition: color .12s;
  }
  .tclose:hover, .helpclose:hover, .ct-close:hover { color: var(--orange); }
  /* Touch gets a 44px target without a 44px button. These are ~30x24 drawn,
     which is a fine size to LOOK at in a panel corner but well under the
     44px guideline to hit with a thumb -- and simply growing the box was
     the wrong lever here: each of these is absolutely positioned against a
     corner whose offsets are tuned to it, and the help panel's scan row
     already carries a hand-measured margin to clear the button beneath it.
     An overlay pseudo-element takes no space in any of those layouts, so
     the hit area grows and nothing moves. Centered on the button, so the
     growth is even on all sides rather than reaching further one way. */
  @media (hover: none) {
    /* Only bites on the one that is still in flow (the popup close above
       600px -- a touch tablet is wide enough to reach that branch); the
       three absolute ones are already their own positioning context, and
       their own rules outrank this one. */
    .tclose, .helpclose, .ct-close { position: relative; }
    .tclose::after, .helpclose::after, .ct-close::after {
      content: ""; position: absolute; top: 50%; left: 50%;
      transform: translate(-50%, -50%);
      width: 44px; height: 44px;
    }
  }

  /* Shares the app-wide dismiss treatment (see .dismiss) -- only the scale
     and corner offset are its own, since this popover is a fraction of a
     modal's size and its corner is much tighter. */
  .chiptip .ct-close {
    position: absolute; top: 2px; right: 2px;
    font-size: 16px; padding: 5px 8px;
  }
  .chiptip.show { opacity: 1; }
  /* Title line: the headword itself, above everything else -- a popover can
     be reached via keyboard focus or land far from the chip it came
     from (Your List), so the word it's about shouldn't be implicit. Serif,
     like a printed dictionary's headword, rather than matching the rest of
     the popover's sans -- one deliberate accent, not a font switch. */
  /* Word + stress dots on one line: the dots sit to the right of the
     headword rather than on their own row below it (see buildInsight's
     .ct-word-row). .ct-word itself no longer carries the row's bottom
     margin -- that's on the row now, so it still separates from the
     respelling line beneath. */
  /* Names the card: "Word Insight" here, "Similar words" on the thesaurus
     floatcard, "Show all" on the show-all modal (.tkicker below) -- every
     popup shares this look, so each needs its own label to say which one
     you're looking at. */
  .ct-kicker, .tkicker {
    display: block; font: 700 13px var(--sans); letter-spacing: .3px;
    color: var(--green); margin-bottom: 2px;
  }
  .chiptip .ct-word-row { display: flex; align-items: center; gap: 16px; margin-bottom: 3px; }
  .chiptip .ct-word { color: var(--text); font: 700 21px var(--serif); }
  .chiptip .ct-word-row .scan { margin-top: 0; }
  /* Line 1: the plain-English respelling in slashes, then the stress shape. */
  /* Shared by the insight popover's sound line and the page header's own
     (.say below) -- one stress/gender treatment, not two. */
  .ct-stress { color: var(--green); font-weight: 600; letter-spacing: .3px; }
  .ct-gender { color: var(--muted); font-weight: 500; }
  /* Both the stress spelling and the rhyme gender are terms most people
     haven't seen before -- make them click through to the Glossary rather
     than leaving the learning to guesswork or an external search. */
  .ct-stress.glossterm, .ct-gender.glossterm {
    cursor: pointer; border-bottom: 1px dotted currentColor;
  }
  .ct-stress.glossterm:hover, .ct-gender.glossterm:hover { color: var(--orange); }
  .scan.glossterm { cursor: pointer; }
  .scan.glossterm:hover .dot { filter: brightness(1.25); }
  /* Cross-references between Glossary entries themselves -- bridge's
     definition mentions nucleus, rime's mentions all four sound-structure
     parts, and so on. Without a link, a term used inside another entry's
     definition is a dead end for anyone who doesn't already know it. */
  .helptext dd .glossterm {
    cursor: pointer; color: var(--green); border-bottom: 1px dotted currentColor;
  }
  .helptext dd .glossterm:hover { color: var(--orange); }
  /* Brief highlight on the glossary entry a click landed on, so the jump
     from term to definition is visible, not just an anchor scroll. */
  .helptext dt.glossflash { animation: glossflash 1.8s ease-out; }
  @keyframes glossflash {
    /* Full-saturation brand orange (not a dimmed rgba fallback), fading out
       -- the flash itself should read as unmistakably "you landed here",
       full strength at the start rather than already muted. */
    0% { background: color-mix(in srgb, var(--orange) 65%, transparent); }
    100% { background: transparent; }
  }
  /* Caveats that change how the word reads at all (letters-not-a-word, soft
     rhyme, register) -- quieter than the sound line, above the definitions. */
  .chiptip .ct-note { color: var(--orange-dark); font-size: 12px; margin-top: 2px; }
  /* Part of speech, standalone (a word with no senses gets only this) --
     italic serif, the same convention a printed dictionary uses to mark a
     grammatical label apart from running prose. */
  .chiptip .ct-pos {
    color: var(--muted); font: italic 500 12px var(--serif); margin-top: 3px;
  }
  /* Provenance note: this POS/sense was inherited from a base word, not
     authored for this exact inflected form -- quieter still than ct-pos,
     since it's a footnote, not something to scan first. */
  .chiptip .ct-inherited { color: var(--muted); font-size: 12px; font-style: italic; margin-top: 3px; }
  /* Senses, grouped by part of speech. <ol> because they're genuinely
     ordered -- senses.tsv's file order IS priority order, most common
     meaning first -- and each group restarts at 1, since a noun's second
     meaning has nothing to do with a verb's first. */
  .chiptip-senses {
    margin: 7px 0 0; padding-top: 7px; border-top: 1px solid var(--border);
  }
  /* POS heading per group: italic serif again, quiet rather than shouting
     (no caps/letter-spacing) -- a dictionary's own part-of-speech label,
     not a UI section header. */
  .chiptip-senses .ct-posgroup {
    color: var(--green); font: italic 600 12px var(--serif);
  }
  .chiptip-senses .ct-posgroup:not(:first-child) { margin-top: 7px; }
  /* Hanging-indent numbered list -- the number sits in its own column so a
     wrapped definition's second line lands under the first word of the
     definition, not back under the number. */
  .chiptip-senses ol {
    margin: 2px 0 0; padding: 0; list-style: none; counter-reset: ct-sense;
  }
  /* Regular weight, against .chiptip's own 500 that everything else in the
     panel inherits. The definitions are the longest run of text on the card
     and the only one set in full-strength --text, so at 500 they read as one
     heavy block and pull the eye before the word and its sound -- which are
     what the panel is actually for. Dropping just this run to 400 leaves the
     headword (700) and the POS/stress labels as the things that stand out,
     and makes the definitions comfortable to actually read. */
  .chiptip-senses li {
    margin: 0; padding: 0 0 0 17px; position: relative;
    counter-increment: ct-sense; font-weight: 400;
  }
  /* 500, not 600: once the definitions lighten, a 600 counter becomes the
     boldest thing in the list and numbers start out-shouting meanings. */
  .chiptip-senses li::before {
    content: counter(ct-sense) ".";
    position: absolute; left: 0; color: var(--muted); font-weight: 500;
  }
  .chiptip-senses li + li { margin-top: 4px; }
  /* Etymology sits right below the say/stress line, its own section set
     off by a rule -- wrapped in .ct-etym-block (rather than putting the
     rule on .ct-etym itself) because the "from '<base>'" note now LEADS
     the section (see buildInsight), and the rule needs to sit above
     whichever of the two actually comes first, not always the text row
     specifically. */
  .chiptip .ct-etym-block {
    margin-top: 8px; padding-top: 7px; border-top: 1px solid var(--border);
  }
  .chiptip .ct-etym-block .ct-inherited { margin-top: 0; margin-bottom: 3px; }
  /* Etymology: prose, so left-aligned and readable rather than the quiet
     centered footer treatment Groups gets further down the panel. */
  .chiptip .ct-etym {
    color: var(--muted); font-size: 12px; line-height: 1.4;
    /* Clamped to 3 lines. Etymologies are wildly uneven -- "cat" is a
       sentence, "egg" is thirty lines of cognates from Saterland Frisian to
       Serbo-Croatian -- and unclamped the long ones bury the senses below
       them, which are what most people opened the panel for.

       Pinned, a more/less toggle lifts the clamp (see setupEtymToggle);
       the hover form stays clamped with no control, since it is
       pointer-events: none and a click could not land on one anyway. */
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .chiptip.pinned .ct-etym.open {
    -webkit-line-clamp: unset; overflow: visible;
  }
  /* The disclosure control. A ▾ that flips, same mark and same flip as the
     Layers and Filters dropdowns (.layerdd summary::after) -- this is the
     third place in the app that hides something behind an arrow, so it
     borrows the existing one rather than introducing a fourth idiom.
     Rendered only when the text actually overflows, so a one-line origin
     never grows a control that would do nothing. */
  .chiptip .ct-etym-more {
    border: 0; background: none; padding: 3px 0 0; margin: 0;
    display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
    color: var(--muted); font: 600 12px var(--sans);
  }
  .chiptip .ct-etym-more:hover { color: var(--orange); }
  .chiptip .ct-etym-more::after {
    content: "▾"; font-size: 12px; transition: transform .12s;
  }
  .chiptip .ct-etym-more[aria-expanded="true"]::after { transform: rotate(180deg); }
  /* Words within the origin that are themselves real lexicon headwords
     (see item.etym_words / buildEtymRow) -- stand out a little from the
     surrounding muted prose even on hover so the eye can find them, but
     only actually act clickable (cursor, underline) once pinned, since
     the hover form is pointer-events: none and a click couldn't land. */
  .chiptip .ct-etym-word { color: var(--text); font-weight: 500; }
  .chiptip.pinned .ct-etym-word {
    cursor: pointer; text-decoration: underline;
    text-decoration-color: var(--border); text-underline-offset: 2px;
  }
  .chiptip.pinned .ct-etym-word:hover {
    color: var(--orange); text-decoration-color: var(--orange);
  }
  /* Group footer: dot-separated, left-aligned, set off by its own thin rule
     -- a footnote about register, not a labelled field, so it reads more
     like a dictionary entry's usage tags than a form row. Lighter and
     smaller than the rest of the popover's muted text so it reads as the
     least important line in it, not a peer of senses/etymology.

     Two columns (icon | list), not an icon inline with the text. The list
     can run to four registers -- common informal dialect vulgar -- which
     wraps in a popover this narrow, and inline the wrapped lines tucked
     back under the icon so the icon stopped reading as a marker for the
     whole row. In its own column every line of the list starts at the same
     left edge, verified at 120px: all four lines begin at x=16, the icon
     ends at x=11.

     Was centred until 2026-09-02. Left suits a list that wraps; centring a
     ragged two- or three-line list left nothing to read down.

     ---- the icon's vertical position (skip unless changing it) ----

     Measured at 10.5px in the default stack, against the small-caps ink:
         was  vertical-align: -1px    icon centre 1.5px HIGH
         now  one-line box + .09em    icon centre 0.06px low

     1. `vertical-align` was the wrong instrument. It aligned the 11px icon
        box to the text BASELINE, which says nothing about where the
        letters' middle is; every offset from there is a guess that needs
        re-tuning per font.
     2. The icon's box is one line tall (1.35em, the row's own
        line-height), with the SVG centred inside it. So it centres on the
        FIRST line of the list, and stays there when the list wraps --
        `align-items: start` would otherwise drift it against a block that
        changes height word by word.
     3. margin-top pays back the descender space. Centring in that box
        centres on the LINE BOX, but small-caps has no descenders, so the
        ink sits entirely in the top of it and the empty ~3px below lifts
        the line box's centre above the letters' own. In em so it tracks
        font-size. */
  .chiptip .ct-layers-icon {
    display: inline-flex; align-items: center; justify-content: center;
    height: 1.35em; margin-top: .09em;
  }
  .chiptip .ct-layers {
    display: grid; grid-template-columns: auto 1fr; column-gap: 5px;
    align-items: start; line-height: 1.35;
    margin-top: 8px; padding-top: 6px; border-top: 1px solid var(--border);
    text-align: left; color: color-mix(in srgb, var(--muted) 65%, transparent);
    font-size: 12px; letter-spacing: .4px;
    font-variant-caps: small-caps; text-transform: lowercase;
  }
  /* Mark the matched sound with a quiet underline instead of brackets, so the
     word reads unbroken. The underline is a shade of orange whose intensity
     tracks closure (--uc, set per rung): bright for perfect, greying out toward
     consonance / identity. */
  .chip b {
    font-weight: 600; color: inherit;
    border-bottom: 2px solid var(--uc);
    padding-bottom: 1px;
  }
  /* The letters outside the matched sound sit back a step, so the underlined
     part reads first. Colour, not font-weight: the serif stack here (Iowan,
     Palatino, Georgia) has no light weight, so 300 would silently render as
     400. The step is small enough to hold 4.5:1 in the four themes (checked
     2026-10-12); the high-contrast themes keep full strength on purpose. */
  .chip:not(.soft) .wtext { color: color-mix(in srgb, var(--text) 82%, var(--surface)); }
  .chip:not(.soft) b { color: var(--text); }
  :root[data-theme="hc"] .chip .wtext,
  :root[data-theme="hc-light"] .chip .wtext { color: inherit; }
  .chip:hover { background: color-mix(in srgb, var(--rc) 18%, transparent); }
  /* Soft rhyme: anchor syllable carries only secondary stress, so the rhyme
     closes softer — dimmed, and its underline fades to match. */
  .chip.soft { color: var(--muted); }
  .chip.soft b { border-bottom-color: color-mix(in srgb, var(--uc) 45%, transparent); }
  /* Optional syllable separators (view option): a quiet dictionary-style
     middle dot between spelling syllables — dimmed so the word still reads
     as one piece. */
  .chip .syl { color: var(--muted); opacity: .65; font-weight: 400; }
  .empty { color: var(--muted); font-style: italic; font-size: 14px; }
  /* Nudge shown when a search comes back thin/empty: points at the
     Vocabulary/Layers filters instead of reading as "the lexicon lacks this word". */
  .widenhint {
    border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2);
    padding: 12px 16px; margin-bottom: 16px; font: 500 13px var(--sans); color: var(--muted);
    display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px;
  }
  /* WCAG 1.4.3: bare --green on --surface is 3.71:1 on the light theme -- fine
     as an accent, short of 4.5:1 as 12px button text. Seated toward --text the
     same way the rung headings are, so it stays recognisably the green accent
     in all four themes instead of being swapped for a flat text colour in one.
     80/20 clears 4.5:1 everywhere (worst case light, 5.07:1). */
  .widenhint button {
    border: 1px solid var(--border); background: var(--surface);
    color: color-mix(in srgb, var(--green) 80%, var(--text));
    border-radius: 8px; cursor: pointer; font: 600 12px var(--sans); padding: 4px 10px;
    transition: border-color .12s;
  }
  .widenhint button:hover { border-color: var(--green); }
  /* Quiet variant: shown under results that DID come back, so it's an FYI
     rather than a dead end.

     This has been round the loop twice, so the reasoning matters. It began
     borderless and dimmed to .85, and disappeared -- sitting above three
     paragraphs of footer boilerplate in plain muted text, it read as more
     boilerplate and got skipped, losing the one control that explains a
     short ladder. So it became a tinted, bordered card. That was the wrong
     half of the problem: once the footer was cut back and "Rhyme candidates"
     started landing as a card directly above, the tip was a third bordered
     box in a stack of three.

     Now: no box, but it keeps the "Tip" label below, which is what stopped
     it reading as boilerplate in the first place. A name, not armour. */
  .widenhint.quiet {
    border: 0; background: none; padding: 2px 0; margin: 14px 0 16px;
    font-size: 13px; opacity: 1;
  }
  .widenhint.quiet::before {
    content: "Tip"; flex: none;
    font: 700 12px var(--sans); letter-spacing: .5px; text-transform: uppercase;
    color: var(--orange-dark);
  }
  .widenhint.quiet button { font-size: 12px; padding: 3px 8px; }
  /* "Showing N of M" + reveal controls at the foot of a rung. */
  .morebar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; margin-top: 12px; }
  .moreinfo { color: var(--muted); font: 500 12px var(--mono); }
  .morebtn {
    border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
    border-radius: 8px; cursor: pointer; font: 600 12px var(--sans); padding: 5px 12px;
    transition: border-color .12s, color .12s;
  }
  .morebtn:hover { border-color: var(--rc); color: var(--rc); }
  .morebtn.ghost { background: transparent; color: var(--muted); }
  .morebtn.ghost:hover { color: var(--text); }
  .morebtn:disabled { opacity: .55; cursor: default; }
  .morenudge { color: var(--muted); font: italic 12px var(--sans); }

  /* The word chip IS the add-to-list toggle: click it to add, click again to
     remove. Picked chips fill green and show a ✓, so your list stands out as
     you scroll a long rung. Adding is the frequent, forgiving action, so it
     gets the big target; looking a word up (which repaints the page) is the
     small deliberate "→" — an accidental click adds a word, not loses your place. */
  .chip.picked, .chip.picked:hover {
    background: var(--green-glow);
    box-shadow: inset 0 0 0 1.5px var(--green);
    color: var(--text);
  }
  /* Leads the chip rather than trailing it: a trailing ✓ wedged itself between
     the part-of-speech marker and the spyglass/≈ controls, splitting the word
     and its modifier from the actions. The slot is always reserved and merely
     hidden when unpicked, so picking a word doesn't shove it sideways and
     leave the column's left edge ragged. */
  .chip .tick {
    display: inline-block; width: .8em; margin-right: 3px;
    color: var(--green); font-weight: 700; visibility: hidden;
  }
  .chip.picked .tick { visibility: visible; }
  /* The three per-word icon controls -- lookup (spyglass), thesaurus ("≈"),
     word insight (eye) -- are peers now: same box size, same resting/hover
     opacity, same spacing, so they read as one level row of equal controls
     instead of two SVG icons flanking an oddly-perched text glyph. Quiet
     until you hover the word, warming to their own accent color when you
     point at one directly. SVGs use currentColor so they ride along. */
  /* 24px, not the 20px this sat at: WCAG 2.2 added 2.5.8 Target Size
     (Minimum), which asks for 24x24 CSS px -- and unlike the older 2.5.5 it is
     Level AA, so it is a conformance requirement rather than a guideline. It
     also has no pointer-type exemption, which is what the old sizing assumed:
     the 26px boxes below are behind @media (hover: none), so a touchscreen
     laptop or a tablet in desktop layout got the 20px ones and nothing else.
     20px boxes at 2px margins also failed 2.5.8's *spacing* alternative (22px
     pitch, 24 needed), so growing the box is what fixes both halves at once --
     24px boxes touching at 0 margin give exactly the 24px pitch the spacing
     exception wants, and the icons inside are unchanged, so the row looks the
     same weight it did, just with no dead gap between the hit areas. */
  .chip .look, .iw .eye, .chip .thes {
    cursor: pointer; border: 0; background: transparent; color: var(--muted);
    display: inline-flex; align-items: center; justify-content: center;
    width: 24px; height: 24px; line-height: 1; vertical-align: middle;
    padding: 0; margin-left: 0; opacity: .3;
    transition: opacity .12s, color .12s;
  }
  .chip .thes { font: 600 13px var(--sans); }
  /* Placeholder for an absent control -- keeps the icon column aligned down
     the list (see actGap). It inherits the box from the rules above by
     carrying the same .thes/.eye class, and only opts out of being seen or
     hit; visibility (not opacity) so the :hover rules above can't reveal it. */
  .chip .act-gap, .iw .act-gap { visibility: hidden; pointer-events: none; }
  .chip:hover .look, .iw:hover .eye, .chip:hover .thes { opacity: .85; }
  .chip .look:hover { color: var(--orange); opacity: 1; }

  /* Word-insight "eye": opens the insight popover on click/tap, on every
     device. Was briefly gated to .has-senses (chips with a definition) on
     the theory that hover already covered everything else for a mouse --
     but that hid it on ~70% of words, since only about a third of the
     lexicon has senses, and the popover's other content (pronunciation,
     stress, part of speech) is there for ALL of them. A control that
     vanishes on most words reads as broken, not as tidy. It stays quiet
     until you hover the chip, same treatment as its icon-row neighbors. */
  .iw .eye:hover { color: var(--orange); opacity: 1; }
  /* The source-word title and its left-gutter pin aren't chips in a dense
     list -- the quiet-until-hover treatment exists to keep a crowded rung
     calm, which doesn't apply to a single headline word. Stays visible. */
  .qeye .eye { opacity: .55; }
  .qeye .eye:hover { color: var(--orange); opacity: 1; }
  .qeye .eye svg { width: 20px; height: 20px; }
  .qeye .eye { vertical-align: middle; margin-left: 8px; }

  /* Touch devices never fire :hover, so the quiet-until-hover treatment above
     would leave the spyglass, "≈", and eye permanently at their faint resting
     state -- the main affordances, invisible. Reveal them there instead. */
  @media (hover: none) {
    .chip .look, .iw .eye, .chip .thes, .thes { opacity: .6; }
    .iw .eye { display: inline-flex; }
    .iw .eye:active { color: var(--orange); opacity: 1; }
    /* Bigger boxes, not padding -- every icon now shares one fixed-size
       box (see the shared rule above), so growing the box itself (rather
       than padding around a smaller one, which used to leave a stray tap
       landing on the wrong neighbor) is what actually grows the tap
       target. Still short of the 44px guideline -- three of these plus
       the word itself have to fit one chip -- but meaningfully easier to
       land a thumb on than the 20px desktop size. Margin widens to match
       so the bigger boxes don't read as crowded against each other. */
    /* 26px, not the 30px this started at: three 30px boxes plus their
       margins came to 102px of a 235px phone column, which pushed the icon
       row onto its own line for 55 words in 1000 -- and since the boxes sit
       side by side, the axis being squeezed is the same one that decides
       whether a thumb lands on the neighbour icon. 26px costs 4px of target
       and takes the wrapping down to ~20 in 1000, which is rare enough that
       the wrapped case doesn't need a grouping marker of its own. */
    .chip .look, .iw .eye, .chip .thes { width: 26px; height: 26px; margin-left: 3px; }
    .chip .look svg, .iw .eye svg { width: 16px; height: 16px; }
    .chip .thes { font-size: 16px; }
    .navbtn { width: 44px; height: 44px; }
    .morebtn { padding: 10px 14px; }
    /* The base rule is 24x24 now (see .pg-word .rm below), which already
       clears 2.5.8 -- touch gets a little more room on top of it rather
       than the padding that used to do the whole job. */
    .pg-word .rm { width: 28px; height: 28px; }
  }

  /* "Your list" -- a worksheet you fill while browsing, grouped by search
     word. It stays put in the flow, right below the Help row. */
  .picks {
    /* Same card as the Filters and Help drawers it sits beside: the toned
       background, 1px border, 12px corners, shadow, and green border on
       hover and while open. */
    background: color-mix(in srgb, var(--surface) 80%, var(--border) 20%);
    border: 1px solid var(--border);
    border-radius: 12px; padding: 0; margin: 6px 0;
    box-shadow: var(--shadow);
    transition: border-color .15s;
  }
  .picks:hover, .picks.open { border-color: var(--green); }
  .picks[hidden] { display: none; }
  .picks-head { display: flex; align-items: center; gap: 10px; padding: 8px 12px; }
  .picks-toggle {
    display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
    border: 0; background: transparent; padding: 4px 2px; font: inherit;
  }
  /* The drawers' own chevron: points down when closed, turns up when open. */
  .picks.open .picks-caret { transform: rotate(180deg); }
  /* WCAG 1.4.3: same bare --green-on-light problem the widenhint button had
     (3.71:1 at 13px bold), same fix, same ratio -- see that rule for why the
     accent is seated toward --text rather than replaced outright. */
  .picks-title {
    font: 600 13px var(--sans); color: var(--text);
  }
  .picks-title .picks-n { color: var(--muted); font-weight: 400; }
  /* Copy is the primary action in this footer, so it takes the same filled
     gradient treatment as Rhyme (#go) rather than staying outlined -- Clear
     stays a quiet ghost button since it's the deliberate, infrequent one. */
  .picks-btn {
    border: 1px solid var(--orange); color: var(--btn-text); cursor: pointer;
    background: linear-gradient(180deg, color-mix(in srgb, var(--orange) 70%, white) 0%, var(--orange) 100%);
    border-radius: 8px; font: 600 12px var(--sans); padding: 5px 12px;
    transition: background .12s, border-color .12s;
  }
  .picks-btn:hover {
    background: linear-gradient(180deg, color-mix(in srgb, var(--orange-dark) 70%, white) 0%, var(--orange-dark) 100%);
    border-color: var(--orange-dark);
  }
  .picks-btn.ghost {
    border: 1px solid var(--border); background: var(--surface-2); color: var(--muted);
    box-shadow: none;
  }
  .picks-btn.ghost:hover { border-color: var(--border); background: var(--surface-2); color: var(--text); }
  /* Footer bar: bleeds to the card's own edges (cancels .picks's padding)
     and rounds only its bottom corners to nest inside it. The darker
     top border is the seam that separates it from the list above --
     .picks-body[hidden] hides it too so a collapsed list has no dangling
     footer left showing. */
  .picks-foot {
    margin: 0; padding: 7px 12px;
    background: var(--surface-2); border-top: 1px solid var(--border);
    border-radius: 0 0 11px 11px;
  }
  #picks-body[hidden] + .picks-foot { display: none; }
  /* Label on its own line, select+? on the one below -- the select sizes
     itself to its longest option ("by source word and rhyme type"), and on
     narrow phones that plus a same-line label plus the ? button doesn't fit
     one row without an awkward mid-word wrap. Stacking sidesteps it instead
     of fighting for the width. */
  .picks-fmt {
    display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
    max-width: 100%;
    font: 500 12px var(--sans); color: var(--muted); user-select: none;
  }
  .picks-fmt-label { font-weight: 600; }
  .picks-fmt-row {
    display: flex; align-items: center; gap: 10px;
    max-width: 100%;
  }
  /* Its own field-style background/border, matching the main search box,
     rather than the browser's default select chrome -- against the footer's
     --surface-2 tint, an unstyled select was nearly the same color and read
     as flat background instead of a control you could click. */
  #picks-fmt-select {
    font: 600 12px var(--sans); padding: 3px 6px; color: var(--text);
    background: var(--field-bg); border: 1px solid var(--field-border);
    border-radius: 6px;
    min-width: 0; flex: 0 1 auto;
  }
  .qwrap { position: relative; display: inline-flex; }
  .qmark {
    cursor: pointer; border: 0; background: transparent; color: var(--muted);
    font: 700 12px var(--sans); line-height: 1; width: 24px; height: 24px;
    display: inline-flex; align-items: center; justify-content: center; padding: 0;
    position: relative; transition: color .12s;
  }
  /* The ring is drawn at 14px by ::before while the button itself stays 24px:
     the mark looks the same size it always did, but the tap target clears the
     24x24 minimum without a bigger circle crowding the text beside it. */
  .qmark::before {
    content: ""; position: absolute; inset: 5px;
    border: 1px solid currentColor; border-radius: 50%;
  }
  .qmark:hover, .qmark:focus-visible, .qmark[aria-expanded="true"] { color: var(--text); }
  /* Opens downward: the panel rests near the top of the page now, so there is
     room below it, and an upward popover would cover the search area. */
  .fmt-pop {
    position: absolute; top: 22px; right: 0; z-index: 6; width: 260px;
    max-height: min(400px, 70vh); overflow-y: auto;
    background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
    box-shadow: var(--shadow); padding: 10px 12px; font: 400 12px var(--sans);
    color: var(--text); line-height: 1.5; cursor: auto;
  }
  .fmt-pop[hidden] { display: none; }
  .fmt-pop code {
    display: inline-block; margin-top: 2px; font: 400 12px var(--mono, monospace);
    color: var(--muted); line-height: 1.5;
  }
  .fmt-pop b { color: var(--rc, var(--green)); }
  .picks-body { margin: 0; padding: 4px 12px 10px; max-height: 42vh; overflow-y: auto; overscroll-behavior: contain; }
  .picks-body[hidden] { display: none; }
  .picks-list { display: flex; flex-direction: column; gap: 8px; }
  .pg-rung { display: block; margin-top: 2px; }
  .pg-rung:first-of-type { margin-top: 0; }
  .pg-tag {
    display: inline-block;
    font: 700 12px var(--sans); color: var(--rc); opacity: .9;
    border: 1px solid var(--rc); border-radius: 4px; padding: 1px 6px;
    margin-right: 6px; cursor: help;
  }

  /* One row per search word: "shore:  floor  four  oar". */
  .pick-group { line-height: 1.7; }
  .pg-head {
    border: 0; background: transparent; cursor: pointer; padding: 0;
    font: 700 19px var(--sans); color: var(--graphite);
  }
  .pg-head:hover { text-decoration: underline; }
  /* Matches .pg-words' font/size, not .pg-head's -- it reads as part of the
     flowing candidate list, not as a label switch. Went unnoticed as a bare
     ":" inheriting the page's default sans, but " :: " is wide enough that
     the mismatch shows. */
  .pg-colon { font: 400 17px var(--serif); color: var(--muted); margin: 0 6px 0 1px; }
  .pg-words { font: 400 17px var(--serif); }
  /* plain-col mirrors the copy format exactly -- source word, then one
     candidate per line -- rather than the usual flowing " / " row. */
  .pg-words-col { display: flex; flex-direction: column; }
  .pg-word { display: inline-flex; align-items: center; white-space: nowrap; }
  .pg-word .pw { cursor: pointer; }
  .pg-word .pw:hover { color: var(--green); }
  /* WCAG 2.2 2.5.8: this measured 16x14, and it is a *destructive* control --
     it drops a word off the list with no undo, so it is exactly the kind of
     target the criterion exists for (and the accessibility doc's tremor-
     tolerance note calls out by name). Sized to the 24px minimum as an
     inline-flex box; the glyph inside is unchanged, so it looks the same and
     simply stops being a 16px thing to hit. */
  .pg-word .rm {
    cursor: pointer; border: 0; background: transparent; color: var(--muted);
    font: 600 14px var(--sans); line-height: 1; padding: 0; margin-left: 1px;
    width: 24px; height: 24px; flex: 0 0 auto;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 6px; opacity: .35; transition: opacity .12s, color .12s;
  }
  .pg-word:hover .rm { opacity: .9; }
  .pg-word .rm:hover { color: var(--orange); }

  /* Thesaurus "≈" beside each word: opens synonyms + word forms in a popup.
     Same quiet treatment as the "+" — faint until the word is hovered. */
  .thes {
    cursor: pointer; border: 0; background: transparent; color: var(--muted);
    font: 600 12px var(--sans); line-height: 1; vertical-align: super;
    padding: 4px 5px; margin-left: -3px; opacity: .35;
    transition: opacity .12s, color .12s;
  }
  .chip .thes { opacity: .25; }
  .chip:hover .thes { opacity: .8; }
  .thes:hover { color: var(--orange); opacity: 1; }
  .query .thes { font-size: 15px; }

  /* The thesaurus popup: a scrollable panel over a dimmed page. Words inside
     reuse .chip, so click-to-rhyme and the "+" behave exactly like the main
     list (--rc gives their hover tint). */
  .tmodal {
    position: fixed; inset: 0; z-index: 50; display: flex;
    align-items: flex-start; justify-content: center;
    background: rgba(0,0,0,.55); padding: 7vh 16px 16px;
  }
  .tmodal[hidden] { display: none; }
  /* #tmodal (thesaurus) and #allmodal (show-all) share this class and its
     z-index, so with both open at once (thesaurus opened for a word INSIDE
     the show-all list) CSS falls back to DOM order, not open order --
     #allmodal comes later in the markup, so it always won regardless of
     which was actually opened last. The thesaurus popup is always the
     "drill deeper" action here; it's never opened from something that
     should stay above it, so a targeted bump is enough -- no need for
     dynamic z-index management. */
  #tmodal { z-index: 60; }
  .tpanel {
    --rc: var(--green);
    background: var(--surface); border: 1px solid var(--border);
    /* Same 8px radius as Word Insight's .chiptip -- one card language for
       every popup/popover, not a bigger radius just because this one's a
       full dialog. */
    border-radius: 8px; box-shadow: var(--shadow);
    width: 100%; max-width: 660px; max-height: 80vh; overflow-y: auto;
  }
  /* Word Insight's own dark-theme surface swap (see .chiptip) -- reused
     here so "Show all" reads as the same raised-card family in dark mode
     too, not just in light mode where --surface already contrasts fine. */
  :root[data-theme="dark"] .tpanel { background: var(--surface-2); }
  /* Every popup header (thesaurus, "show all") shares one shape: a small
     meta row on top (source/label context, the instructional hint, the
     close button), the headword large underneath. Splitting the word onto
     its own line -- rather than cramming it beside the meta row on one
     baseline -- is what keeps this from wrapping awkwardly at narrow
     widths; a meta row alone wraps cleanly onto whole chips, but a row
     that also carries a 30px+ headword doesn't. */
  .thead {
    position: sticky; top: 0; z-index: 1; background: var(--surface);
    display: flex; flex-direction: column; align-items: stretch; gap: 4px;
    padding: 16px 22px 10px; border-bottom: 1px solid var(--border);
  }
  .thead .ttop, .thead .atop { display: flex; align-items: baseline; gap: 10px; }
  .thead .tword { font: 400 30px/1 var(--serif); }
  /* The thesaurus popup's source word is the whole reason the panel is open,
     so it gets extra weight and size beyond the shared .tword default (which
     the "Show all" popup also uses, but for a full heading sentence). */
  #tmodal .thead .tword { font: 700 38px/1 var(--serif); }
  .thead .thint { color: var(--muted); font-size: 12px; margin-left: auto; }
  /* Which word you were rhyming when you opened this popup. Two or three hops
     into a thesaurus detour it's easy to forget, so keep it on screen -- quiet
     enough not to compete with the heading it sits beside. */
  .thead .tsrc { color: var(--muted); font-size: 12px; white-space: nowrap; }
  .thead .tsrc b { color: var(--text); font-weight: 600; }
  .thead .tsrc[hidden] { display: none; }
  .thead .tclose { margin-left: 0; }
  /* Sits on its own line under the headword, right-aligned -- a rare,
     deliberate action (Simple/Intuitive: one dropdown, not a setting per
     rhythm term), not worth crowding onto the ttop row with the close
     button. */
  .thead .tsort { display: flex; align-items: center; gap: 6px; justify-content: flex-end;
    color: var(--muted); font-size: 12px; }
  .thead .tsort[hidden] { display: none; }
  .thead .tsort select { font: inherit; color: inherit; background: var(--surface);
    border: 1px solid var(--border); border-radius: 6px; padding: 2px 6px; }
  /* .thint already claims the row's margin-left: auto push; when it's
     hidden (no hint on this header, or the narrow-width rule below drops
     it) the close button takes that job instead. */
  .thead .ttop .tclose, .thead .atop .tclose { margin-left: auto; }
  .thead .ttop .thint ~ .tclose, .thead .atop .thint ~ .tclose { margin-left: 0; }
  #tbody, #allbody { padding: 6px 22px 20px; }

  /* Thesaurus popup ("≈"): a floating card anchored to the button that
     opened it, matching Word Insight's .chiptip (same colors, radius,
     shadow, fade-in, hover-preview/click-to-pin split) instead of a
     centered full-viewport dialog -- the "Show all" popup below keeps the
     dialog treatment since its content (every word in a rung) genuinely
     needs the room; this one's content is small enough to float. #tmodal
     itself becomes a layout no-op (display: contents) so the existing
     DOM/JS structure (thead, tsort, tbody, focus trap) didn't need to
     change shape -- only .floatcard, the actual visible box, moves and
     sizes itself the way .chiptip does. */
  #tmodal:not([hidden]) { position: static; inset: auto; z-index: 60; display: contents; background: none; padding: 0; }
  .floatcard {
    /* Same 320px cap as Word Insight's .chiptip -- one popup size, not two. */
    position: fixed; z-index: 60; width: auto; max-width: min(320px, calc(100vw - 16px));
    max-height: 320px; overflow-y: hidden; border-radius: 8px;
    opacity: 0; pointer-events: none; transition: opacity .12s ease;
  }
  .floatcard.show { opacity: 1; }
  /* Pinned (clicked, not just hovered): stays open till its own × closes
     it, and unlike the hover preview it's worth scrolling through in full
     -- see .chiptip.pinned's own reasoning. */
  .floatcard.pinned { pointer-events: auto; max-height: 70vh; overflow-y: auto; }
  /* #tmodal ancestor prefix, not just .floatcard -- #tmodal .thead .tword's
     ID selector above outranks a plain .floatcard .thead .tword by CSS
     specificity regardless of source order, so without it this override
     silently lost and the floatcard rendered the full modal's 38px
     headword instead of this card's own size. */
  #tmodal .floatcard .thead .tword { font: 700 21px/1.2 var(--serif); }
  .floatcard .thead { padding: 10px 14px 6px; }
  .floatcard .thint { display: none; }
  /* .thint is display:none here, not gone -- the ".thint ~ .tclose" rule
     above still matches it as a sibling and cancels the close button's
     margin-left:auto. Restore the push explicitly, matching that rule's
     own specificity (thead+ttop/atop+thint+tclose) so it actually wins,
     so × sits at the card's top-right corner. */
  .floatcard .thead .ttop .thint ~ .tclose,
  .floatcard .thead .atop .thint ~ .tclose { margin-left: auto; }
  .floatcard #tbody { padding: 4px 14px 12px; }
  /* Condensed section/sense spacing -- the full modal's roomy 16px/9px
     margins were sized for a page-width panel; a 400px card needs the same
     information in less vertical run so a synonym list doesn't take three
     scrolls to get through. Chip sizing itself (tap targets) is untouched. */
  .floatcard .tsec { margin: 10px 0 2px; font-size: 11px; }
  .floatcard .tsense { margin: 4px 0; font-size: 15px; line-height: 1.55; }
  .floatcard .thead { gap: 2px; }
  .floatcard .tsort { padding-top: 2px; }

  /* The "Show all" popup's header used to be one run-on sentence ("Consonance
     — all 329 for "read""), which wrapped awkwardly and fought the hint text
     for room. Split it like the inline rung header: a small colored category
     line up top (label · closure · count · hint · close), the source word
     large underneath — same shape as the rest of the page, so it reads at a
     glance instead of like a sentence. */
  .athead .alabel {
    font: 700 13px var(--sans); letter-spacing: .3px; color: var(--rc);
  }
  /* Part-of-speech row under the heading: plain checkboxes, same wording as
     the page-level Part of speech filter, with a count per box. */
  .apos {
    display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px;
    margin-top: 10px; font: 500 13px var(--sans); color: var(--text);
  }
  .apos[hidden] { display: none; }
  .apos .apos-lead { color: var(--muted); font-weight: 600; }
  /* 24px tall at minimum (WCAG 2.5.8) so each box is an easy target on touch. */
  .apos label { display: inline-flex; align-items: center; gap: 4px; min-height: 24px; cursor: pointer; }
  .apos input { width: 16px; height: 16px; margin: 0; }
  .apos .apos-n { color: var(--muted); font: 500 12px var(--mono); }
  .athead .acl { color: var(--muted); font-weight: 500; font-size: 13px; }
  .athead .acount { color: var(--muted); font: 500 12px var(--mono); }
  .athead .tword { font: 700 34px/1.2 var(--serif); }
  /* Below ~600px, the meta row's contents (label/closure/count or "rhyming
     X") plus the hint sentence all crammed onto one baseline row left each
     squeezed item wrapping its OWN text mid-word ("weak / closure") instead
     of the row wrapping cleanly -- reads as broken, not narrow. Drop the
     hint (it's instructional copy already covered by the help panels, not
     load-bearing here) and let the remaining content wrap onto a second row
     as whole chips. The close button comes out of that flow entirely --
     .thead is already a positioning context (position: sticky), so pinning
     it top-right there is the same trick .helpclose uses, and it stops
     competing with the row's free space. */
  @media (max-width: 600px) {
    .thead .thint { display: none; }
    /* min-height reserves the absolutely-positioned close button's own row.
       Without it, a header whose meta row has nothing left to show (the
       thesaurus popup opened from the search box has no "rhyming X", and
       the hint is dropped just above) collapsed that row to zero height --
       and the headword, now starting at the header's top edge, ran straight
       under the close button. 32px is the button's own 24px plus clearance:
       a 38px serif line box overshoots its block top by several pixels, so
       reserving the button's exact height still left ascenders grazing it. */
    .thead .ttop, .thead .atop { flex-wrap: wrap; row-gap: 4px; padding-right: 28px; min-height: 32px; }
    .thead .ttop .tclose, .thead .atop .tclose { position: absolute; top: 16px; right: 22px; margin-left: 0; }
  }

  /* Alphabet jump rail for the "Show all" popup: a vertical A–Z strip like
     iOS Contacts, letting you skip straight to a first letter in a long list
     instead of scrolling past it. Only shown once a rung is long enough that
     scrolling to find a letter is actually a chore. */
  .azpanel { display: flex; flex-direction: column; overflow: hidden; }
  .azrow { display: flex; flex: 1 1 auto; min-height: 0; overflow: hidden; }
  /* This modal is narrower than the main page (660px cap, minus the A-Z
     rail's own width), so the same 3-column cap that fits the main results
     comfortably leaves 3 columns here too tight for longer words
     (wherefore, heretofore, underscore all wrapped). Two wider columns
     instead. */
  .azrow .sylline { column-count: 2; }
  /* Same small-screen rule as the main ladder (see .sylline's own mobile
     media query): within one rung, only the 1-syllable group is short
     enough to hold two columns comfortably -- 2+ syllable groups drop to
     one. Placed after the unconditional 2-column rule above so it wins
     the specificity tie (both are two-class selectors) via source order,
     rather than needing !important. */
  @media (max-width: 600px) {
    .azrow .sylline { column-count: 1; }
    .azrow .sylline[data-syl="1"] { column-count: 2; }
  }
  .azrow #allbody { flex: 1 1 auto; overflow-y: auto; }
  /* Jumping to a letter or syllable group scrolls its first row to the top
     edge, which left it jammed against the header. scroll-padding makes
     every scrollIntoView land with breathing room above, the letter band
     and syllable heading included. */
  .azrow #allbody { scroll-padding-top: 28px; }
  .azrail {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    flex: 0 0 auto; padding: 4px 6px; touch-action: none;
  }
  .azletter {
    border: 0; background: transparent; color: var(--muted);
    font: 700 12px/1 var(--sans); cursor: pointer; padding: 2px 5px; border-radius: 4px;
  }
  .azletter:hover:not(:disabled) { color: var(--orange); }
  .azletter:disabled { color: var(--border); cursor: default; }
  /* Marks the currently-selected letter (A-Z rail) or syllable count
     (syllable rail) -- same treatment for both, since both are "this is
     the active filter" states. */
  .azletter.active { color: var(--orange); font-weight: 700; }
  /* Every word matching the selected A-Z letter reads as one shaded block
     behind the whole group, rather than a per-word outline: the words are
     already alphabetical within a syllable group, so a letter group is a
     contiguous run of chips.

     ONE element per run, positioned by paintLetterBands() -- not a
     background on each chip.

     ---- what this replaced, and why (skip unless changing it) ----

     Until 2026-09-02 the band was faked per chip: a background plus a 6px
     box-shadow spread, so consecutive full-width chips bled into each
     other and merged into a rectangle. It worked, but it could only ever
     be a rectangle. `border-radius: 0` was forced, because rounding every
     chip notches the interior joins, and the browser (not the DOM) decides
     where a column breaks, so which chip ends a visible run wasn't
     knowable in CSS. An outline was impossible for the same reason: a ring
     per chip draws a line across every interior seam.

     Measuring offsetTop/offsetLeft answers the question CSS couldn't. Chips
     in a run are exactly contiguous (65px tall on a 65px pitch), so a run
     is "same offsetLeft, no vertical gap" and its union rect is exact; a
     run split across a column break, or across syllable groups, simply
     comes out as two runs and gets two bands. With a real element the
     corners, the outline and the drop shadow are ordinary CSS.

     The chips need to sit ABOVE the band: a positioned element paints over
     in-flow content whatever the DOM order, so .azrow .chip is positioned
     too and the band is inserted first. */
  .azrow #allbody { position: relative; }
  .azrow #allbody .chip { position: relative; }
  .litband {
    position: absolute; z-index: 0; pointer-events: none;
    border-radius: 10px;
    /* Self-contained (color-mix off --text) rather than var(--surface-2)
       and var(--border), so one pair of values holds up in both themes and
       against the chip's own hover/picked backgrounds. Mixed against
       --surface (opaque), not transparent, so it reads the same wherever
       the modal sits. Border a step darker than the fill -- present, not a
       frame competing with the words. */
    background: color-mix(in srgb, var(--text) 4%, var(--surface));
    border: 1px solid color-mix(in srgb, var(--text) 12%, var(--surface));
  }
  /* Both high-contrast themes are pure black-on-white or white-on-black, so
     a 12%-of-text border lands around #1f1f1f / #e0e0e0 -- far too quiet for
     someone who chose these themes precisely because subtle tints don't
     reach them. --border is pure white in hc, pure black in hc-light.
     Accessibility over the quieter look, per the design principles. */
  :root[data-theme="hc"] .litband,
  :root[data-theme="hc-light"] .litband { border-color: var(--border); }
  /* Hover has to stay legible INSIDE the band -- the base hover tint sits
     at roughly the band's own weight, so a pointer over a lit chip would
     read as no change. */
  .chip.letterlit:hover { background: color-mix(in srgb, var(--rc) 26%, transparent); }
  /* Same rail mechanics as the A-Z jump, one column over -- a thin divider
     keeps the two from reading as a single mixed strip of letters and
     numbers. */
  .sylrail { border-right: 1px solid var(--border); }
  .tsec {
    font: 600 12px var(--sans); letter-spacing: .5px; text-transform: uppercase;
    color: var(--muted); margin: 16px 0 2px;
  }
  /* The classical-foot name for a stress-rhythm group (e.g. "trochaic foot"
     under Feminine) -- small caps instead of the header's full uppercase so
     it reads as a footnote, not a second heading, while still holding its
     own line's weight without italics (which .tsense already uses for
     other asides). */
  .tsec .tfoot-note, .sylhead .tfoot-note {
    display: block; font: 400 11px var(--sans); font-variant: small-caps;
    letter-spacing: normal; text-transform: none; opacity: .75;
  }
  .tsense { margin: 9px 0; font: 400 18px/1.75 var(--serif); word-spacing: 2px; }
  .tsense .tpos {
    font: 600 12px var(--sans); letter-spacing: .5px; text-transform: uppercase;
    color: var(--green); margin-right: 7px;
  }
  .tsense .tvia { color: var(--muted); font: italic 12px var(--sans); margin-right: 7px; }
  .tsense .tlbl {
    font: italic 12px var(--sans); color: var(--muted); margin: 0 4px 0 6px;
  }
  .tsense .tlbl.opp { color: var(--orange); font-style: normal; font-weight: 600; font-size: 12px; letter-spacing: .5px; text-transform: uppercase; }
  .tsense .chip.tdim { color: var(--muted); }

  /* Jump-to-top: small screens only (matches the .filterstatus breakpoint
     elsewhere) -- desktop's page is short enough, and has a mouse
     wheel/Home key, that a floating button there would just be clutter.
     Shown/hidden by .show once the page has scrolled far enough that
     getting back to the search box/filters is actually a scroll, not a
     flick (see the "scroll" listener near #toTop). */
  .totop {
    display: none; position: fixed; right: 16px; bottom: 20px; z-index: 15;
    width: 44px; height: 44px; border-radius: 50%;
    background: var(--surface); border: 1px solid var(--border); color: var(--muted);
    box-shadow: var(--shadow); cursor: pointer;
    align-items: center; justify-content: center;
    font-size: 15px; line-height: 1; opacity: 0;
    transition: opacity .15s, border-color .12s, color .12s;
  }
  .totop:hover { border-color: var(--green); color: var(--green); }
  @media (max-width: 700px) {
    .totop.show { display: flex; opacity: .9; }
  }

  .helpbox {
    background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
    padding: 4px 20px; margin: 18px 0; display: none; box-shadow: var(--shadow);
    position: relative;
  }
  .helpbox.open { display: block; }
  .helpbox dl { margin: 14px 0; }
  .helpbox dt { font-weight: 700; color: var(--green); margin-top: 12px; }
  .helpbox dd { margin: 2px 0 0; color: var(--muted); }
  .helpbox dd b { color: var(--text); font-weight: 600; }
  .helpclose { position: absolute; top: 8px; right: 12px; }

  /* The "Reading a result" diagram: one magnified word with leader lines. All
     colours come from the same variables the real chips use, so it stays
     truthful under either theme. */
  /* Scrolls horizontally rather than the page itself -- the SVG below has a
     fixed min-width on narrow screens (see the phone-width rule) specifically
     so its labels stay legible instead of shrinking into paste, and this is
     what contains that overflow instead of it blowing out .helptext's width. */
  .keydiag-wrap { width: 100%; max-width: 640px; margin: 20px auto 4px; overflow-x: auto; }
  .keydiag { display: block; width: 100%; height: auto; }
  .keydiag .kl polyline { fill: none; stroke: var(--muted); stroke-width: 1.5; }
  .keydiag .kt { font: 13px var(--sans); fill: var(--muted); }
  .keydiag .ke { text-anchor: end; }
  .keydiag .kword { font: 44px var(--serif); fill: var(--text); }
  .keydiag .kdot { fill: var(--muted); opacity: .65; }
  .keydiag .kmark { font-weight: 600; }
  .keydiag .kunder { stroke: var(--u-perfect); stroke-width: 3; }
  .keydiag .ktick { font: 600 20px var(--sans); fill: var(--green); }
  .keydiag .kicon { color: var(--muted); }
  .keydiag .kthes { font: 20px var(--sans); fill: var(--muted); }
  /* Scaled down to phone width the leader labels stop being legible, and
     side-scrolling only shows half a word at a time. The list below says
     everything the diagram does, so drop the diagram rather than ship a
     version of it nobody can read. */
  @media (max-width: 560px) {
    .keydiag { display: none; }
  }

  /* The "Anatomy of a rhyme" diagram: discussion split into its five colored
     pieces, with a brace marking the rime span, and a legend since a
     1-3 letter segment has no room to carry its own label. Same
     hide-below-560px treatment as .keydiag -- the entry's own prose already
     says everything the picture does. */
  .anatdiag-wrap { width: 100%; max-width: 460px; margin: 12px auto 4px; overflow-x: auto; }
  .anatdiag { display: block; width: 100%; height: auto; }
  .anatdiag text { font-family: var(--mono); }
  .anatdiag .aword { font-size: 27px; }
  .anatdiag .abrace { stroke: var(--muted); stroke-width: 1.5; fill: none; }
  .anatdiag .abracelabel { font: italic 12px var(--sans); fill: var(--muted); }
  .anatdiag .aswatch { }
  .anatdiag .alabel { font: 12px var(--sans); fill: var(--text); }
  .anat-prefix { fill: var(--muted); }
  .anat-onset { fill: var(--leaf-dark); }
  .anat-nucleus { fill: var(--orange); }
  .anat-bridge { fill: color-mix(in srgb, var(--orange) 55%, var(--leaf-dark)); }
  .anat-coda { fill: color-mix(in srgb, var(--leaf-dark) 55%, var(--muted)); }
  @media (max-width: 560px) {
    .anatdiag { display: none; }
  }

  /* Stress-analysis panel: type any sentence, get a DUM-dum-da scansion. */
  .scanpanel .scanrow {
    /* .helpclose is position:absolute at top:8px/right:12px -- computed
       against .helpbox's 4px top padding, that spans roughly 12-36px from
       the panel's outer top edge. This row's normal 14px top margin (~18px
       from that same edge) would put its own right-aligned .scanbtns
       column directly under the close button. Cleared with extra
       margin-top rather than discovered as a visual bug later. */
    display: flex; gap: 10px; align-items: stretch; margin: 40px 0 10px;
  }
  .scanpanel textarea {
    flex: 1; min-height: 60px; resize: vertical;
    background: var(--bg); color: var(--text);
    border: 1px solid var(--border); border-radius: 8px;
    padding: 10px 12px; font: inherit; line-height: 1.4;
  }
  .scanpanel textarea:focus { outline: 2px solid var(--green); outline-offset: 1px; }
  .scanpanel .scanbtns { display: flex; flex-direction: column; gap: 6px; }
  .scanpanel .scanbtns button {
    background: var(--btn-orange);
    color: var(--btn-text-onlight, var(--btn-text)); border: 0;
    border-radius: 8px; padding: 8px 14px; font-weight: 700; cursor: pointer;
  }
  .scanpanel .scanbtns button.ghost {
    background: transparent; color: var(--muted); border: 1px solid var(--border);
    font-weight: 500;
  }
  .scanout { margin: 8px 0 14px; line-height: 2.1; }
  .scanout .w {
    display: inline-block; margin: 0 10px 6px 0; padding: 2px 0;
    border-bottom: 1px dashed transparent;
  }
  .scanout .w.unk { border-bottom-color: var(--border); color: var(--muted); }
  .scanout .w.unk::after {
    content: " ?"; color: var(--muted); font-weight: 700;
  }
  .scanout .w.demoted { opacity: .62; }
  .scanpanel .dim { color: var(--muted); font-weight: 400; }
  .scanpanel .scanhint { color: var(--muted); font-size: 12px; margin-top: 6px; }
  .scanout .sp {
    display: block; font-family: ui-monospace, Menlo, Consolas, monospace;
    font-size: 12px; letter-spacing: .04em; color: var(--muted);
  }
  .scanout .sp .s1 { color: var(--orange); font-weight: 700; }
  .scanout .sp .s2 { color: var(--green); font-weight: 600; }
  .scanout .sp .s0 { color: var(--muted); }
  .scanpanel .scannote { color: var(--muted); font-size: 13px; margin-top: 4px; }
  .scanpanel .legend { display: inline-flex; gap: 14px; margin-left: 12px; }
  .scanpanel .legend span { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; }
  .scanpanel .legend .s1 { color: var(--orange); font-weight: 700; }
  .scanpanel .legend .s2 { color: var(--green); font-weight: 600; }
  .scanpanel .legend .s0 { color: var(--muted); }

  /* footer itself stays full width so .studio's divider (below) still spans
     the whole page column -- only .footer-text (the paragraph lines) is
     narrowed to fit-content and centered as its own block. Each wrapped
     line still shares a consistent left edge within that block (easier to
     scan than centered text), while the block as a whole doesn't look
     stranded off to one side of a wide page. */
  footer { color: var(--muted); font-size: 12px; margin-top: 32px; }
  footer .footer-text { text-align: left; width: fit-content; max-width: 100%; margin: 0 auto; }
  footer .mark { color: color-mix(in srgb, var(--green) 55%, var(--muted)); font-weight: 600; letter-spacing: .2px; }
  /* A bit more room than the original cramped 6px, short of a full 18px --
     enough to read as separate thoughts without feeling spread out. */
  footer .footer-text > div { margin-top: 11px; }
  footer .footer-text > div:first-child { margin-top: 0; }
  footer .copyright a, footer .attribution a, footer .a11y-claim a { color: var(--muted); text-decoration: underline; }
  footer .copyright a:hover, footer .attribution a:hover, footer .a11y-claim a:hover { color: var(--orange); }
  /* The conformance claim sits on its own line between the data credits and
     the copyright -- the same quiet weight as its neighbours. A claim in
     larger or louder type would read as a badge, which is the thing this is
     deliberately not (see the note in index.html). */
  footer .a11y-claim { margin-top: 6px; }
  /* Studio credit: Wilder (the person, not the WilderCode tool brand above)
     below the WilderCode-specific lines above -- separate credit, same footer. */
  .studio {
    display: flex; align-items: center; justify-content: center; gap: 12px;
    margin-top: 28px; padding-top: 14px; border-top: 1px solid var(--border);
    text-align: left;
  }
  .studio-lockup { width: 56px; height: 56px; border-radius: 10px; flex: none; }
  .studio-name { color: var(--text); font-weight: 600; font-size: 13px; letter-spacing: .2px; }
  .studio-tag { font-style: italic; }
  .studio-kofi { margin-top: 4px; font-size: 12px; }
  /* Muted like every other footer link, orange on hover. It was orange at
     rest, which made a support link the loudest thing on a page that is
     otherwise trying to read as a reference work. Still there, still the
     same link -- it just no longer outranks the licence it sits beside. */
  .studio-kofi a { color: var(--muted); text-decoration: underline; }
  .studio-kofi a:hover { color: var(--orange); }
  .studio-link { color: inherit; text-decoration: none; }
  .studio-link:hover { color: var(--orange); text-decoration: underline; }
  .sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
  }
  button.link { border: 0; background: transparent; padding: 0; font: inherit; cursor: pointer; }
  /* Searching. Opacity, not blur, on purpose: a filter over the whole
     scrolling results area janks on phones, and an indicator that makes the
     app feel slower defeats itself. Blurred text is also a real problem for
     low-vision readers -- accessibility wins that argument. Dimming keeps
     the previous results readable while saying plainly they are stale, and
     pointer-events stops a click landing on a word that is about to be
     replaced. The reduced-motion block below already clamps the
     transition. */
  #results.busy {
    opacity: .45;
    pointer-events: none;
    transition: opacity .12s ease-out;
  }
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
    #go:active { transform: none; }
    :root[data-theme="dark"] body { background-attachment: scroll; }
  }
