/* OQ! (oqaatsit) demo — semantic CSS consuming --kc-* theme variables */

/* ── Base ────────────────────────────────────────────────────────────────────── */
:root {
  color-scheme: dark;
  --kc-bg: #09090b;
  --kc-bg-card: #18181b;
  --kc-bg-input: #27272a;
  --kc-bg-input-focus: #09090b;
  --kc-bg-tab-active: #3f3f46;
  --kc-border: #3f3f46;
  --kc-border-card: #27272a;
  --kc-border-accent: #047857;
  --kc-text: #e4e4e7;
  --kc-text-muted: #a1a1aa;
  --kc-text-faint: #71717a;
  --kc-text-very-faint: #52525b;
  --kc-accent: #34d399;
  --kc-accent-hover: #10b981;
  --kc-accent-action: #059669;
  --kc-danger: #f87171;
  --kc-danger-hover: #ef4444;
  --kc-btn-primary: #ffffff;
  --kc-btn-primary-text: #09090b;
  /* "Liquid Ass" glass effect (see the PWA update toast / .liquid-ass-surface
     rules further down) — theme-dependent so every opted-in surface
     inherits the right tint without redeclaring it locally. Comma-separated
     (not space-separated) so it plugs into legacy rgba(), which Firefox for
     iOS's WKWebView supports more reliably than the Color-4 rgb(.../ ...)
     syntax. Borders are separate theme-dependent variables (not derived
     from the same rgb triple) since a border needs the *opposite* end of
     the lightness scale from the fill to stay visible against it — a
     white-on-dark-page glass panel wants a light border, but the same
     white fill in light mode sits on a near-white page and needs a dark
     border instead. */
  --kc-liquid-toast-rgb: 0, 0, 0;
  --kc-liquid-border: rgba(255, 255, 255, 0.24);
  --kc-liquid-nested-border: rgba(255, 255, 255, 0.2);
}

:root[data-theme=light] {
  color-scheme: light;
  --kc-bg: #f9f9fb;
  --kc-bg-card: #ffffff;
  --kc-bg-input: #f4f4f5;
  --kc-bg-input-focus: #f9f9fb;
  --kc-bg-tab-active: #e4e4e7;
  --kc-border: #d4d4d8;
  --kc-border-card: #e4e4e7;
  --kc-border-accent: #059669;
  --kc-liquid-toast-rgb: 255, 255, 255;
  --kc-liquid-border: rgba(0, 0, 0, 0.16);
  --kc-liquid-nested-border: rgba(0, 0, 0, 0.12);
  --kc-text: #18181b;
  --kc-text-muted: #52525b;
  --kc-text-faint: #71717a;
  --kc-text-very-faint: #a1a1aa;
  --kc-accent: #059669;
  --kc-accent-hover: #047857;
  --kc-accent-action: #065f46;
  --kc-danger: #dc2626;
  --kc-danger-hover: #b91c1c;
  --kc-btn-primary: #18181b;
  --kc-btn-primary-text: #f9f9fb;
}

/* Slightly larger type everywhere except the smallest screens
   (phones < 480px get their own bump in the mobile media block) */
@media (min-width: 480px) {
  html { font-size: 112.5%; }
}

/* Eliminate 300ms tap delay on mobile browsers */
button, [role="tab"], [role="button"] {
  touch-action: manipulation;
}

body {
  background: var(--kc-bg);
  color: var(--kc-text);
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

.file-help { display: none; }

:root[data-file-help] .file-help {
  display: block;
  max-width: 42rem;
  margin: 0 auto;
  padding: 12vh 1.5rem 3rem;
  color: var(--kc-text);
}

:root[data-file-help] .file-help h1 {
  margin: 0 0 1.5rem;
  font-size: clamp(3rem, 12vw, 6rem);
  line-height: 0.95;
}

:root[data-file-help] .file-help h2 {
  margin: 2.5rem 0 0.75rem;
  color: var(--kc-text-muted);
}

:root[data-file-help] .file-help p {
  max-width: 38rem;
  font-size: 1.1rem;
  line-height: 1.6;
}

:root[data-file-help] .file-help p.file-help-note {
  color: var(--kc-text-muted);
  font-size: 0.95rem;
}

:root[data-file-help] #splash-screen,
:root[data-file-help] .page-wrapper,
:root[data-file-help] #mode-toast,
:root[data-file-help] #pwa-update-toast,
:root[data-file-help] dialog {
  display: none !important;
}

.font-mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}

.hidden { display: none !important; }

/* ── Command palette ─────────────────────────────────────────────────────── */
.command-palette-dialog {
  width: min(36rem, calc(100vw - 2rem));
  max-width: none;
  padding: 0;
  overflow: hidden;
}

.command-palette-shell {
  background: var(--kc-bg-card);
  color: var(--kc-text);
  border: 1px solid var(--kc-border);
  border-radius: inherit;
  box-shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.35);
}

.command-palette-input-wrap {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.9rem 1rem;
  border-bottom: 1px solid var(--kc-border);
  color: var(--kc-text-muted);
}

.command-palette-input-wrap input {
  flex: 1;
  min-width: 0;
  border: 0;
  outline: 0;
  background: transparent;
  color: var(--kc-text);
  font: inherit;
}

[data-slot="command-list"] {
  max-height: min(24rem, 55vh);
  overflow-y: auto;
  padding: 0.5rem;
}

[data-slot="command-item"] {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  gap: 1rem;
  border: 0;
  border-radius: 0.35rem;
  padding: 0.65rem 0.75rem;
  background: transparent;
  color: var(--kc-text);
  text-align: left;
  font: inherit;
  cursor: pointer;
}

[data-slot="command-item"]:hover,
[data-slot="command-item"][data-selected] {
  background: var(--kc-bg-tab-active);
  color: var(--kc-text);
}

[data-slot="command-shortcut"] {
  color: var(--kc-text-faint);
  font-size: 0.8em;
}

[data-slot="command-group-heading"] {
  padding: 0.5rem 0.75rem 0.25rem;
  color: var(--kc-text-faint);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.command-palette-hint {
  margin: 0;
  padding: 0.65rem 1rem;
  border-top: 1px solid var(--kc-border);
  color: var(--kc-text-faint);
  font-size: 0.8rem;
}

.command-palette-trigger kbd {
  font: inherit;
  font-size: 0.75rem;
}

.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;
}

.col-span-full { grid-column: 1 / -1; }

/* ── Layout ──────────────────────────────────────────────────────────────────── */
.page-wrapper {
  max-width: 64rem;
  width: 100%;
  box-sizing: border-box;
  margin: 0 auto;
  padding: 0 2rem 2rem;  /* top padding moved into .sticky-chrome */
  overflow-x: clip;      /* prevent wide child content from widening the viewport */
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
}

:root[data-app-booting] .page-wrapper {
  visibility: hidden;
}

.main-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem;
}

@media (min-width: 1024px) {
  .main-grid { grid-template-columns: 3fr 2fr; }
}

/* ── Sticky chrome (header + search row) ────────────────────────────────────── */
.chrome-surface {
  background: color-mix(in srgb, var(--kc-bg) 25%, transparent);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}

.sticky-chrome {
  position: sticky;
  top: 0;
  z-index: 20;
  border-bottom: 1px solid transparent;
  padding-top: 1.5rem;
  padding-bottom: 0.5rem;
  margin-bottom: 0.75rem;
  transition: padding 0.2s ease, border-color 0.2s ease;
  /* Show All Words on Load's virtualized results grid inserts/removes whole
     batches of cards while scrolling — without its own compositing layer,
     this sticky header can visibly tear during those reflows, letting
     scrolled content flash over the logo/search row for a frame. */
  will-change: transform;
}

:root[data-chrome-booting] .sticky-chrome,
:root[data-chrome-booting] .header-title {
  transition: none !important;
}

.sticky-chrome--compact {
  padding-top: 0.375rem;
  padding-bottom: 0.375rem;
  border-color: var(--kc-border);
}

/* ── Header ──────────────────────────────────────────────────────────────────── */
.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1rem;
  margin-bottom: 0.75rem;
}

.header-title {
  font-size: 2.25rem;
  font-weight: 700;
  letter-spacing: -0.05em;
  line-height: 1.1;
  transition: font-size 0.2s ease, line-height 0.2s ease;
  text-wrap: balance;
}

.sticky-chrome--compact .header-title {
  font-size: 1.125rem;
  line-height: 1;
}

/* Nav always wraps to its own row so the icon toolbar sits at the
   far right of the logo row in every scroll state. */
.view-switch {
  order: 10;
  flex-basis: 100%;
}

/* In compact mode top-align so the icons and the shrunken logo share
   the same top edge (they're different heights when the title is small). */
.sticky-chrome--compact .page-header {
  align-items: flex-start;
}



/* ── Theme picker ─────────────────────────────────────────────────────────────── */
.picker-header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
}

.picker-label { font-size: 0.875rem; font-weight: 600; color: var(--kc-text); text-wrap: balance; }

.picker-row {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

.picker-select { flex: 1; min-width: 0; }

.btn-secondary {
  padding: 0.5rem 1rem;
  background: var(--kc-bg-input);
  border: 1px solid var(--kc-border);
  border-radius: 0.75rem;
  font-size: 0.875rem;
  color: var(--kc-text-muted);
  cursor: pointer;
  transition: border-color 0.1s, color 0.1s;
  white-space: nowrap;
}

.btn-secondary:hover { border-color: var(--kc-text-faint); color: var(--kc-text); }

.btn-secondary--danger {
  border-color: var(--kc-danger, #f87171);
  color: var(--kc-danger, #f87171);
}
.btn-secondary--danger:hover { border-color: var(--kc-danger, #f87171); color: var(--kc-danger, #f87171); }

.btn-secondary--update-ready {
  background: var(--kc-accent);
  border-color: var(--kc-accent);
  /* Same pairing as .filter-badge: --kc-bg-card contrasts reliably against
     --kc-accent in both themes (dark text on dark theme's light-green accent,
     light text on light theme's darker accent) — a hardcoded #fff doesn't. */
  color: var(--kc-bg-card);
}
.btn-secondary--update-ready:hover {
  background: var(--kc-accent-hover);
  border-color: var(--kc-accent-hover);
  color: var(--kc-bg-card);
}

/* ── Tab strip ───────────────────────────────────────────────────────────────── */
.tab-strip {
  display: flex;
  flex-wrap: wrap;
  background: var(--kc-bg-input);
  border-radius: 0.75rem;
  padding: 0.125rem;
  gap: 0.125rem;
  font-size: 0.75rem;
  font-weight: 600;
}

.tab-btn {
  padding: 0.375rem 0.75rem;
  border-radius: 0.5rem;
  color: var(--kc-text-muted);
  background: none;
  border: none;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s, color 0.15s;
}

.tab-btn:hover    { color: var(--kc-text); }
.tab-btn--active  { background: var(--kc-bg-tab-active); color: var(--kc-text); }

/* An inert "class type" header inside a .tab-strip (word builder's segmented
   category selector, complex mode) -- same non-selectable-header treatment
   as the <select>'s <optgroup> labels, just for the tab-strip rendering. */
.tab-strip-group-label {
  display: flex;
  align-items: center;
  padding: 0.375rem 0.5rem 0.375rem 0.625rem;
  color: var(--kc-text-faint);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  width: 100%;
  flex-basis: 100%;
}

.source-badge {
  font-size: 0.75rem;
  padding: 0.125rem 0.5rem;
  background: var(--kc-bg-input);
  border-radius: 0.25rem;
  color: var(--kc-text-muted);
}

/* ── Form elements ───────────────────────────────────────────────────────────── */
.search-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
  transition: margin-top 0.25s ease;
}

/* Idle dictionary (no query, no bookmarks/show-all): park the search bar near
   the vertical center of the viewport, Google-homepage style. The chrome stops
   being sticky so the pushed-down search doesn't pin a tall header block.
   dictionary.js owns the data-search-idle attribute. */
:root[data-search-idle] .sticky-chrome {
  position: static;
}

:root[data-search-idle] #dict-search-row {
  margin-top: max(0rem, calc(50dvh - 13rem));
}

:root[data-chrome-booting] .search-row {
  transition: none !important;
}

.search-row > .input-field { flex: 1; }

/* The custom × replaces the native one */
.search-input::-webkit-search-cancel-button { display: none; }

/* Wrapper that hosts the inline clear (×) button inside the search bar */
.search-wrap {
  position: relative;
  flex: 1;
  display: flex;
  align-items: center;
}

.search-wrap .input-field {
  flex: 1;
  padding-right: 2.5rem;
  font-size: 1.25rem;
  /* No margin — layout is handled by .search-row */
  margin: 0;
}

.search-clear {
  position: absolute;
  right: 0.375rem;
  top: 50%;
  transform: translateY(-50%);
  width: 1.75rem;
  height: 1.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  border-radius: 0.5rem;
  color: var(--kc-text-muted);
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}

.search-clear svg { width: 1rem; height: 1rem; }

.search-clear:not(:disabled):hover { color: var(--kc-danger); }

.search-clear:disabled {
  color: var(--kc-text-very-faint);
  cursor: default;
}

.search-clear:focus-visible {
  outline: 2px solid var(--kc-accent);
  outline-offset: 2px;
}

/* Filter + clear-filter icon buttons sit inline with the search bar;
   match the input's height so the row stays on one line at all widths */
.search-row .icon-btn {
  flex-shrink: 0;
  align-self: stretch;
  height: auto;
}

/* Link-style button inline within a status message — the "random word" link
   in the "Type to search" empty state, "load more" on a capped result list. */
.inline-link-btn {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--kc-accent);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.inline-link-btn:hover { color: var(--kc-accent-hover); }

.inline-link-btn:focus-visible {
  outline: 2px solid var(--kc-accent);
  outline-offset: 2px;
  border-radius: 2px;
}

.input-field {
  width: 100%;
  background: var(--kc-bg-input);
  border: 1px solid var(--kc-border);
  border-radius: 0.75rem;
  padding: 0.5rem 0.75rem;
  font-size: 0.875rem;
  color: var(--kc-text);
  outline: none;
  box-sizing: border-box;
  display: block;
  color-scheme: inherit;
}

.input-field::placeholder { color: var(--kc-text-faint); }
.input-field:focus         { border-color: var(--kc-text-faint); }

.input-field--dark           { background: var(--kc-bg-input-focus); }
.input-field--dark:focus     { border-color: var(--kc-accent-hover); }

.liquid-ass-range {
  --kc-range-thumb: 1rem;
  --kc-range-track: 0.35rem;
  width: 100%;
  height: var(--kc-range-thumb);
  padding: 0;
  margin: 0;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  color: var(--kc-accent);
  cursor: pointer;
}

.liquid-ass-range-label {
  display: flex;
  justify-content: space-between;
  gap: 0.75rem;
  width: 100%;
}

.liquid-ass-range-label output {
  min-width: 4ch;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.liquid-ass-range:disabled {
  cursor: default;
}

.liquid-ass-range:focus-visible {
  outline: 2px solid var(--kc-accent);
  outline-offset: 4px;
  border-radius: 999px;
}

.liquid-ass-range::-webkit-slider-runnable-track {
  height: var(--kc-range-track);
  border-radius: 999px;
  background: var(--kc-bg-input);
  border: 1px solid var(--kc-border);
}

.liquid-ass-range::-webkit-slider-thumb {
  width: var(--kc-range-thumb);
  height: var(--kc-range-thumb);
  margin-top: calc((var(--kc-range-track) - var(--kc-range-thumb)) / 2);
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.45);
  background: var(--kc-accent);
  box-shadow: 0 0.15rem 0.5rem rgba(0, 0, 0, 0.28);
  appearance: none;
  -webkit-appearance: none;
}

.liquid-ass-range::-moz-range-track {
  height: var(--kc-range-track);
  border-radius: 999px;
  background: var(--kc-bg-input);
  border: 1px solid var(--kc-border);
}

.liquid-ass-range::-moz-range-thumb {
  width: var(--kc-range-thumb);
  height: var(--kc-range-thumb);
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.45);
  background: var(--kc-accent);
  box-shadow: 0 0.15rem 0.5rem rgba(0, 0, 0, 0.28);
}

.select-field {
  width: 100%;
  background: var(--kc-bg-input);
  border: 1px solid var(--kc-border);
  border-radius: 0.75rem;
  padding: 0.5rem 0.75rem;
  font-size: 0.875rem;
  color: var(--kc-text);
  outline: none;
  cursor: pointer;
  color-scheme: inherit;
}

.select-field:focus { border-color: var(--kc-text-faint); }

@supports (appearance: base-select) {
  .select-field {
    appearance: base-select;
  }

  .select-field::picker-icon {
    color: var(--kc-text-faint);
    transition: rotate 0.2s ease;
  }

  .select-field:open::picker-icon {
    rotate: 180deg;
  }

  .select-field::picker(select) {
    background: var(--kc-bg-card);
    border: 1px solid var(--kc-border-card);
    border-radius: 0.75rem;
    padding: 0.25rem;
    box-shadow: 0 8px 24px -4px color-mix(in srgb, currentColor 12%, transparent);
  }

  .select-field option {
    background: transparent;
    color: var(--kc-text);
    padding: 0.375rem 0.625rem;
    border-radius: 0.5rem;
    cursor: pointer;
  }

  .select-field option:hover,
  .select-field option:focus {
    background: color-mix(in srgb, var(--kc-accent) 12%, transparent);
  }

  .select-field option:checked {
    background: color-mix(in srgb, var(--kc-accent) 20%, transparent);
    font-weight: 500;
  }
}

/* ── Scroll area ─────────────────────────────────────────────────────────────── */
.scroll-area {
  overflow-y: auto;
  border-radius: 1rem;
  scrollbar-width: thin;
  scrollbar-color: var(--kc-border) transparent;
}

.scroll-area::-webkit-scrollbar { width: 6px; }
.scroll-area::-webkit-scrollbar-track { background: transparent; }
.scroll-area::-webkit-scrollbar-thumb {
  background: var(--kc-border);
  border-radius: 3px;
}
.scroll-area::-webkit-scrollbar-thumb:hover { background: var(--kc-text-faint); }

/* ── Preset / morpheme grids ─────────────────────────────────────────────────── */
.preset-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.75rem;
  padding: 0.25rem;
}

@media (min-width: 640px) {
  .preset-grid { grid-template-columns: 1fr 1fr; }
}

/* ── Cards ───────────────────────────────────────────────────────────────────── */
.preset-card,
.morpheme-card {
  text-align: left;
  background: var(--kc-bg-card);
  border: 1px solid var(--kc-border-card);
  border-radius: 1rem;
  padding: 1rem;
  display: flex;
  flex-direction: column;
  cursor: pointer;
  transition: all 0.1s cubic-bezier(0.4, 0, 0.2, 1);
  min-width: 0; /* grid items default to min-width:auto; reset so 1fr columns are respected */
}

.preset-card   { border-left-width: 3px; border-left-color: var(--preset-border, var(--kc-border)); position: relative; }

/* Bookmark star, top-right of each dictionary card */
.card-star {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  width: 1.75rem;
  height: 1.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  border-radius: 0.375rem;
  color: var(--kc-text-faint);
  cursor: pointer;
  padding: 0;
}

.card-star svg {
  width: 1rem;
  height: 1rem;
  fill: none;
}

.card-star:hover { color: var(--kc-accent); }

.card-star:focus-visible {
  outline: 2px solid var(--kc-accent);
  outline-offset: 2px;
}

.card-star--active { color: var(--kc-accent); }
.card-star--active svg { fill: currentColor; }

/* Keep long headwords clear of the star */
.preset-card .card-word { padding-right: 2.25rem; }

.preset-card:hover {
  background: var(--kc-bg-input);
  border-color: var(--kc-border);
  /* border-color shorthand above resets all four edges — keep the
     word-class indicator on the left edge */
  border-left-color: var(--preset-border, var(--kc-border));
  transform: translateY(-1px);
  box-shadow: 0 10px 15px -3px color-mix(in srgb, var(--kc-accent-hover) 10%, transparent);
}

.morpheme-card:hover {
  background: var(--kc-bg-input);
  border-color: var(--kc-border-accent);
  transform: translateY(-1px);
}

/* Browse-only morphemes (katersat morphophonemic affixes): not addable to the
   builder sequence, so drop the clickable affordances. */
.morpheme-card--reference {
  cursor: default;
  opacity: 0.9;
}
.morpheme-card--reference:hover {
  background: var(--kc-bg-card);
  border-color: var(--kc-border-card);
  transform: none;
}
.morpheme-card--reference:hover .card-word { color: var(--kc-text); }

/* Buildable but can't legally follow the current sequence: greyed + inert. */
.morpheme-card--blocked {
  cursor: not-allowed;
  opacity: 0.4;
}
.morpheme-card--blocked:hover {
  background: var(--kc-bg-card);
  border-color: var(--kc-border-card);
  transform: none;
}

.card-word {
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  font-size: calc(0.875rem * var(--kc-card-word-scale, 1));
  color: var(--kc-text);
  transition: color 0.1s;
  overflow-wrap: break-word; /* fallback for edge cases where no soft hyphen lands */
  hyphens: manual; /* use soft hyphens injected by syllabify() */
}

.preset-card:hover  .card-word,
.morpheme-card:hover .card-word { color: var(--kc-accent); }

.card-gloss   { font-size: calc(0.625rem * var(--kc-card-gloss-scale, 1)); color: var(--preset-text, var(--kc-text-muted)); margin-top: 0.125rem; }
.card-desc    { font-size: calc(0.625rem * var(--kc-card-desc-scale, 1)); color: var(--kc-text-faint); margin-top: 0.25rem; }

.card-type-label { font-size: 0.625rem; color: var(--kc-text-muted); }

.card-meta {
  margin-top: 0.5rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

.card-example {
  margin-top: 0.5rem;
  font-size: 0.5625rem;
  color: var(--kc-text-faint);
  font-style: italic;
  border-left: 2px solid var(--kc-border);
  padding-left: 0.5rem;
}

/* ── Pill ────────────────────────────────────────────────────────────────────── */
.pill {
  font-size: 0.5625rem;
  padding: 0.125rem 0.375rem;
  background: var(--kc-bg-input);
  color: var(--kc-text-muted);
  border-radius: 0.375rem;
}

/* ── Status / attribution ────────────────────────────────────────────────────── */
.status-msg {
  font-size: 0.75rem;
  color: var(--kc-text-faint);
  font-style: italic;
  padding: 0.5rem;
}

/* A bordered .btn-secondary (the "load more" affordance) sitting inline in a
   status-msg sentence shouldn't inherit its italic style or crowd the text
   before it. */
.status-msg .btn-secondary {
  font-style: normal;
  margin-left: 0.25rem;
}

.error-text {
  font-size: 0.75rem;
  color: var(--kc-danger);
  font-style: italic;
  padding: 0.5rem;
}

.error-text-action {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  padding: 0;
  text-decoration: underline;
  cursor: pointer;
}
.error-text-action:disabled {
  cursor: default;
  text-decoration: none;
  opacity: 0.7;
}

/* A load failure that leaves zero addable stems needs to stand out from the
   morpheme grid it sits inside of, not blend in as one more faint line. */
.morpheme-warning--blocking {
  font-style: normal;
  font-size: 0.875rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--kc-danger);
  border-radius: 0.5rem;
  background: color-mix(in srgb, var(--kc-danger) 12%, transparent);
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}

/* .add-btn defaults to width:100%/no horizontal padding for its usual full-
   width form placement; override both for its inline use as the warning's
   Retry button so it doesn't stretch to fill the flex-wrapped row. */
.morpheme-warning--blocking .add-btn {
  width: auto;
  padding: 0.375rem 1rem;
}

.attribution { font-size: 0.5625rem; color: var(--kc-text-very-faint); margin-top: 0.5rem; }

/* ── Custom Builder ──────────────────────────────────────────────────────────── */
.section-title { font-size: 1.25rem; font-weight: 600; margin-bottom: 1rem; text-wrap: balance; }

.builder-card {
  background: var(--kc-bg-card);
  border: 1px solid var(--kc-border-card);
  border-radius: 1rem;
  padding: 1.25rem;
}

.form-section > * + * { margin-top: 1rem; }

.form-label {
  display: block;
  font-size: 0.75rem;
  color: var(--kc-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.375rem;
  text-wrap: balance;
}

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
}

.add-btn {
  width: 100%;
  padding: 0.625rem 0;
  background: var(--kc-btn-primary);
  color: var(--kc-btn-primary-text);
  font-weight: 600;
  border-radius: 1rem;
  border: none;
  font-size: 0.875rem;
  cursor: pointer;
  transition: transform 0.1s;
}

.add-btn:active { transform: scale(0.985); }

/* ── Sequence ────────────────────────────────────────────────────────────────── */
.seq-section { margin-top: 1.5rem; }

.seq-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 0.75rem;
  color: var(--kc-text-muted);
  margin-bottom: 0.5rem;
}

.clear-btn {
  background: none;
  border: none;
  color: var(--kc-text-muted);
  cursor: pointer;
  font-size: 0.75rem;
  padding: 0;
  transition: color 0.1s;
}

.clear-btn:hover { color: var(--kc-danger); }

.sequence-list {
  min-height: 7.5rem;
  background: var(--kc-bg-input-focus);
  border: 1px solid var(--kc-border-card);
  border-radius: 1rem;
  padding: 0.75rem;
  font-size: 0.875rem;
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}

.sequence-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.375rem;
  background: var(--kc-bg-input);
  padding: 0.375rem 0.75rem;
  border-radius: 0.75rem;
  font-size: 0.75rem;
}

.sequence-item-text {
  flex: 1;
}

.sequence-item--dragging {
  position: relative;
  z-index: 5;
  opacity: 0.85;
  box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.35);
  cursor: grabbing;
  will-change: transform;
}

.sequence-drag-handle {
  cursor: grab;
  color: var(--kc-text-muted);
  /* The handle is a small, dedicated drag target, not a scroll surface — this
     hands the whole gesture to our own pointer handlers instead of letting
     the browser start a native scroll/zoom from a touch that begins here. */
  touch-action: none;
  padding: 0 0.125rem;
  line-height: 1;
}

.sequence-move-btn {
  background: none;
  border: none;
  color: var(--kc-text-muted);
  cursor: pointer;
  padding: 0 0.25rem;
  transition: color 0.1s;
  line-height: 1;
  font-size: 0.7rem;
}

.sequence-move-btn:hover:not(:disabled) { color: var(--kc-text); }
.sequence-move-btn:disabled { opacity: 0.3; cursor: default; }

.remove-btn {
  background: none;
  border: none;
  color: var(--kc-danger);
  cursor: pointer;
  padding: 0 0.25rem;
  transition: color 0.1s;
  line-height: 1;
}

.remove-btn:hover { color: var(--kc-danger-hover); }

/* ── Build button ────────────────────────────────────────────────────────────── */
.build-btn {
  margin-top: 1rem;
  width: 100%;
  padding: 0.75rem 0;
  background: var(--kc-accent-action);
  color: var(--kc-text);
  font-weight: 600;
  border-radius: 1rem;
  border: none;
  font-size: 0.875rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  transition: background 0.1s;
}

.build-btn:hover { background: var(--kc-accent-hover); }

/* ── Result ──────────────────────────────────────────────────────────────────── */
#result { animation: fadeIn 0.2s ease forwards; }

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

.result-box {
  margin-top: 2rem;
  background: var(--kc-bg-card);
  border: 1px solid var(--kc-border-card);
  border-radius: 1.5rem;
  padding: 2rem;
}

.result-header { display: flex; align-items: center; gap: 0.75rem; }

.result-label {
  font-size: 0.875rem;
  color: var(--kc-accent);
}

.result-word {
  font-size: 1.875rem;
  font-weight: 600;
  color: var(--kc-text);
  hyphens: manual;
  overflow-wrap: break-word;
  min-width: 0;
}

.result-meta { margin-top: 0.75rem; font-size: 0.75rem; color: var(--kc-text-muted); }

.result-glosses {
  margin-top: 0.5rem;
  font-size: 0.8125rem;
  color: var(--kc-text-muted);
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.result-dict-match {
  margin-top: 0.5rem;
  font-size: 0.8125rem;
  color: var(--kc-accent);
}

.result-footer {
  margin-top: 1.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--kc-border-card);
  font-size: 0.625rem;
  color: var(--kc-text-faint);
}

/* ── Word-class legend tree ──────────────────────────────────────────────────── */
.legend-header {
  margin-bottom: 20px;
}

.legend-title {
  font-size: 1.2rem;
  font-weight: 700;
  color: var(--legend-heading-color, var(--kc-text));
  margin-bottom: 4px;
}

.legend-subtitle {
  font-size: 0.7rem;
  color: var(--legend-subtitle-color, var(--kc-text-muted));
  margin: 0;
}

.legend-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
  gap: 20px;
  align-items: start;
}

.legend-col {
  padding: 0;
  list-style: none;
  margin: 0;
}

.legend-node {
  margin-bottom: 4px;
  list-style: none;
}

.legend-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 12px;
  border-radius: 10px;
  border-left: 3px solid var(--legend-border, var(--kc-border));
  background: var(--legend-fill, transparent);
}

.legend-row[role="button"] {
  cursor: pointer;
}

.legend-row--expanded {
  flex-wrap: wrap;
}

.legend-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--legend-border, var(--kc-border));
  flex-shrink: 0;
}

.legend-label {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--legend-text, var(--kc-text));
  white-space: nowrap;
}

.legend-desc {
  font-size: 0.68rem;
  color: var(--legend-desc-color, var(--kc-text-faint));
  margin-left: auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 340px;
  flex-shrink: 1;
  min-width: 0;
}

.legend-desc--expanded {
  white-space: normal;
  max-width: none;
  flex-basis: 100%;
  margin-left: 0;
}

.legend-nested {
  margin-left: 18px;
  padding-left: 10px;
  border-left: 1px solid var(--legend-connector-color, var(--kc-text-faint));
  margin-top: 4px;
  list-style: none;
  padding-right: 0;
}

/* ── Footer ──────────────────────────────────────────────────────────────────── */
.page-footer {
  position: fixed;
  z-index: 30;
  left: 50%;
  bottom: 0;
  width: min(64rem, calc(100% - 2rem));
  box-sizing: border-box;
  transform: translateX(-50%);
  padding: 0.5rem 1rem calc(0.5rem + env(safe-area-inset-bottom));
  text-align: center;
  line-height: 1.25;
  font-size: 0.625rem;
  color: var(--kc-text-very-faint);
  border-top: 1px solid color-mix(in srgb, var(--kc-border) 70%, transparent);
  transition: opacity 0.15s ease, visibility 0.15s ease;
}

.footer-link { color: inherit; text-decoration: underline; transition: color 0.1s; }
.footer-link:hover { color: var(--kc-text-muted); }

:root[data-keyboard-visible] .page-footer {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .page-footer { transition: none; }
}

/* ── Scroll-area heights ─────────────────────────────────────────────────── */
/* Lexeme list: body scrolls, cards flow to natural length */
.scroll-area--lexeme   { height: auto; overflow-y: visible; }
.scroll-area--morpheme { height: 436px; }

/* Always-visible styled scrollbar on the page scroller.
   Non-transparent track forces WebKit off its overlay (fade-on-idle) style. */
html {
  scrollbar-width: thin;
  scrollbar-color: var(--kc-border) var(--kc-bg-input);
}
html::-webkit-scrollbar        { width: 6px; }
html::-webkit-scrollbar-track  {
  background: var(--kc-bg-input);
  border-radius: 3px;
}
html::-webkit-scrollbar-thumb  {
  background: var(--kc-border);
  border-radius: 3px;
}

/* ── Mobile scale-down (≤ 767px) ─────────────────────────────────────────── */
@media (max-width: 767px) {
  .page-wrapper { padding: 1.25rem 1rem; }

  .main-grid { gap: 1.5rem; }

  .scroll-area--morpheme {
    height: clamp(260px, 52vh, 440px);
    height: clamp(260px, 52dvh, 440px);
  }

  .result-box    { padding: 1.5rem; margin-top: 1.5rem; }

  .page-footer {
    width: calc(100% - 1rem);
    padding: 0.3rem 0.5rem calc(0.3rem + env(safe-area-inset-bottom));
    font-size: 0.5625rem;
  }

  /* Word Builder: shrink the search/filter row once scrolled past, so the
     morpheme grid and sequence builder start higher on a small screen —
     builder.js's handleBuilderScroll() toggles this. */
  #builder-search-row.builder-search--compact {
    margin-bottom: 0.375rem;
  }
  #builder-search-row.builder-search--compact > .input-field,
  #builder-search-row.builder-search--compact > .select-field {
    padding-top: 0.375rem;
    padding-bottom: 0.375rem;
    font-size: 0.875rem;
  }
  /* The segmented-selector row (#builder-category-row) shrinks alongside
     #builder-search-row so the two rows stay visually in sync on scroll. */
  #builder-category-row.builder-search--compact {
    margin-bottom: 0.375rem;
  }
  /* Undo the ≤479px .tab-btn touch-padding bump (line ~1155) while compact —
     otherwise the buttons stay full height and the row never actually
     shrinks, falling out of sync with the search row's own padding cut. */
  #builder-category-row.builder-search--compact .tab-btn {
    padding-top: 0.375rem;
    padding-bottom: 0.375rem;
  }
}

/* ── Cover / foldable outer screen (≤ 320px) — bottom sheet modal ───────── */
@media (max-width: 320px) {
  dialog.modal-dialog {
    width: 100%;
    max-width: 100%;
    margin: auto 0 0;
    border-radius: 1.25rem 1.25rem 0 0;
  }
  dialog.modal-dialog[open] {
    height: 90dvh;
  }
}

/* ── Narrow phone (≤ 479px) ─────────────────────────────────────────────── */
@media (max-width: 479px) {
  /* Nudge all rem-based text up by bumping the root font size */
  html { font-size: 106.25%; }

  .header-title { font-size: 1.5rem; }

  /* Tighter side padding — content runs closer to the screen edge */
  .page-wrapper { padding-left: 0.625rem; padding-right: 0.625rem; }

  /* Keep the narrow sticky chrome compact without moving the toolbar
     away from the logo row.
     :not(.sticky-chrome--compact) avoids overriding the compacted state. */
  .sticky-chrome:not(.sticky-chrome--compact) { padding-top: 0.75rem; }

  /* Stretch tab strip across full width and give buttons a comfortable tap area */
  .tab-strip  { flex: 1 1 100%; }
  .tab-btn    { flex: 1; text-align: center; padding-top: 0.5rem; padding-bottom: 0.5rem; }

  /* Stack the two-column form row */
  .form-row   { grid-template-columns: 1fr; }
  /* Do NOT stack the dictionary search row — it must stay on one line */

  .builder-card   { padding: 1rem; }
  .result-box     { padding: 1rem; margin-top: 1.25rem; }
}

/* ── Icon toolbar ────────────────────────────────────────────────────────────── */
.icon-toolbar {
  display: flex;
  gap: 0.25rem;
  align-items: center;
}

.icon-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: 0.5rem;
  border: 1px solid var(--kc-border);
  background: var(--kc-bg-input);
  color: var(--kc-text-muted);
  cursor: pointer;
  transition: background 0.1s, color 0.1s, border-color 0.1s;
  padding: 0;
  flex-shrink: 0;
}

.icon-btn:hover {
  background: var(--kc-bg-tab-active);
  color: var(--kc-text);
  border-color: var(--kc-text-faint);
}

.icon-btn:focus-visible {
  background: var(--kc-bg-tab-active);
  color: var(--kc-text);
  border-color: var(--kc-text-faint);
  outline: 2px solid var(--kc-accent);
  outline-offset: 2px;
}

.icon-btn--active {
  background: var(--kc-bg-tab-active);
  color: var(--kc-accent);
  border-color: var(--kc-accent);
}

.icon-btn--active:hover {
  color: var(--kc-accent);
  border-color: var(--kc-accent);
}

.icon-btn svg {
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
}

/* Placed after the base .icon-btn rule (not inside the earlier mobile
   scale-down block) so this override actually wins the cascade tie —
   an equal-specificity .icon-btn rule declared later always wins. */
@media (max-width: 767px) {
  /* Touch target for icon buttons — 2rem (≈32px) is below the 44px minimum */
  .icon-btn { width: 2.75rem; height: 2.75rem; }

  /* .search-row .icon-btn (higher specificity, see line ~356) stretches these
     buttons to match the search input's height, which is well under 44px —
     override explicitly so filter/clear buttons also meet the touch-target
     minimum on mobile instead of silently staying stretch-sized. */
  .search-row .icon-btn { height: 2.75rem; align-self: auto; }
}

.toolbar-divider {
  display: inline-block;
  width: 1px;
  height: 1.25rem;
  background: var(--kc-border);
  margin: 0 0.125rem;
}

/* Prevent any overflowing content from widening the layout viewport and
   the initial containing block that top-layer elements (dialog) size against.
   scrollbar-gutter: stable permanently reserves scrollbar space so opening
   a modal never triggers a layout shift. */
html { overflow-x: clip; scrollbar-gutter: stable; }

/* ── Modal dialog (shared) ───────────────────────────────────────────────────── */
/* Target html (not body): body's overflow won't propagate to the viewport
   scroll container when html already has an explicit overflow-x set. */
html:has(dialog[open]) { overflow-y: hidden; }

dialog.modal-dialog {
  box-sizing: border-box;
  margin: auto;
  width: min(640px, calc(100% - 2rem));
  max-width: calc(100% - 2rem);
  max-height: calc(100dvh - 4rem);
  background: var(--kc-bg-card);
  color: var(--kc-text);
  border: 1px solid var(--kc-border-card);
  border-radius: 1.25rem;
  padding: 0;
  overflow: hidden;
  box-shadow: 0 24px 48px -12px rgb(0 0 0 / 0.5);
}

dialog.modal-dialog[open] {
  display: flex;
  flex-direction: column;
  /* Explicit height so flex: 1 on .modal-body has a container to fill.
     Without this, the auto-height dialog gives modal-body nothing to grow into
     and it collapses to its flex-basis (0) due to min-height: 0. */
  height: min(720px, calc(100dvh - 4rem));
  animation: modal-in 0.15s ease forwards;
}

@keyframes modal-in {
  from { opacity: 0; transform: translateY(-8px) scale(0.98); }
  to   { opacity: 1; transform: translateY(0)   scale(1); }
}

dialog.modal-dialog::backdrop {
  background: rgb(0 0 0 / 0.45);
  backdrop-filter: blur(8px) saturate(0.7);
  animation: backdrop-in 0.15s ease forwards;
}

@keyframes backdrop-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Replace animation-only approach with transitions that handle both open and
   close for browsers that support @starting-style + transition-behavior. */
@supports (transition-behavior: allow-discrete) {
  dialog.modal-dialog {
    opacity: 0;
    transform: translateY(-8px) scale(0.98);
    transition:
      opacity    0.15s ease,
      transform  0.15s ease,
      display    0.15s allow-discrete,
      overlay    0.15s allow-discrete;
  }

  dialog.modal-dialog[open] {
    animation: none;
    opacity: 1;
    transform: none;
  }

  @starting-style {
    dialog.modal-dialog[open] {
      opacity: 0;
      transform: translateY(-8px) scale(0.98);
    }
  }

  dialog.modal-dialog::backdrop {
    opacity: 0;
    animation: none;
    transition:
      opacity  0.15s ease,
      display  0.15s allow-discrete,
      overlay  0.15s allow-discrete;
  }

  dialog.modal-dialog[open]::backdrop {
    opacity: 1;
  }

  @starting-style {
    dialog.modal-dialog[open]::backdrop {
      opacity: 0;
    }
  }
}

@media (prefers-reduced-motion: reduce) {
  dialog.modal-dialog[open] { animation: none; }
  dialog.modal-dialog::backdrop { animation: none; }

  @supports (transition-behavior: allow-discrete) {
    dialog.modal-dialog,
    dialog.modal-dialog::backdrop {
      transition: none;
    }
  }
}

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.25rem 1.25rem 0;
  flex-shrink: 0;
}

.modal-title {
  font-size: 1rem;
  font-weight: 600;
  color: var(--kc-text);
  hyphens: manual;
  min-width: 0;
}

.modal-close {
  flex-shrink: 0;
}

.modal-body {
  padding: 1rem 1.25rem 1.25rem;
  overflow: hidden auto;
  overscroll-behavior: contain;
  flex: 1;
  min-height: 0;
}

.modal-section + .modal-section {
  margin-top: 1.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--kc-border-card);
}

/* The Look & Feel Save button sticks to the bottom of the scrolling panel so
   it never hides below the fold on small screens. Negative side margins bleed
   it across the modal-body padding for a full-width shelf. */
.modal-section.look-save-section {
  position: sticky;
  bottom: 0;
  z-index: 2;
  /* Negative bottom margin eats the modal-body bottom padding so the shelf
     sits flush with the scrollport edge at the end of the scroll. */
  margin: 1rem -1.25rem -1.25rem;
  padding: 0.75rem 1.25rem;
  background: var(--kc-bg-card);
  border-top: 1px solid var(--kc-border-card);
}

/* Word-type display: per-surface rows of segmented selectors (language + form) */
.wordclass-surface {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
  margin-top: 0.75rem;
}

.wordclass-surface .form-label {
  flex-basis: 100%;
}

.modal-section-title {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--kc-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.75rem;
  text-wrap: balance;
}

/* Groups several related settings under one heading within a .modal-section,
   e.g. "Word Builder" or "Dictionary Data" in the Experimental tab. */
.modal-group-title {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--kc-text);
  margin-bottom: 0.9rem;
  text-wrap: balance;
}

/* Indents a setting under the one it depends on (e.g. Show All Words under
   Offline Dictionary Cache) so the dependency reads visually, not just via
   its description text. */
.modal-subgroup {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  margin-top: 0.9rem;
  margin-left: 0.15rem;
  padding-left: 1rem;
  border-left: 2px solid var(--kc-border-card);
}

/* Applied to a toggle row (or field) whose setting is forced to an
   unchangeable state because a setting it depends on is off (or, for
   mutually-exclusive settings, on). */
.modal-toggle-row--disabled {
  opacity: 0.5;
  pointer-events: none;
}

.modal-toggle-row {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
}

.modal-toggle-content {
  flex: 1;
  min-width: 0;
}

.modal-toggle-detail {
  margin-top: 0.75rem;
}

.modal-toggle-detail--compact {
  margin-top: 0.5rem;
}

.modal-toggle-progress {
  margin-bottom: 0.5rem;
}

.modal-toggle-meta {
  display: block;
  margin-top: 0.25rem;
}

.modal-toggle-action {
  margin-top: 0.75rem;
}

.modal-toggle-row + .modal-toggle-row,
.modal-toggle-row + .modal-subgroup,
.modal-subgroup + .modal-toggle-row,
.wordclass-surface + .modal-toggle-row,
.modal-section > div + .modal-section-title {
  margin-top: 1.25rem;
}

.modal-toggle-checkbox {
  width: 1.1rem;
  height: 1.1rem;
  accent-color: var(--kc-accent);
  cursor: pointer;
  flex-shrink: 0;
  margin-top: 0.2rem;
}

.text-size-field + .text-size-field {
  margin-top: 1.25rem;
}

.modal-toggle-checkbox:disabled {
  cursor: not-allowed;
}

/* Two selector groups (Colour Mode, Preview Animation) on one line */
.settings-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1rem;
  align-items: flex-start;
}

.settings-row .tab-strip { width: fit-content; }

.settings-pickers-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  margin-bottom: 1rem;
}

.settings-pickers-row > * { flex: 1; min-width: 8rem; }

.tab-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* ── Compact modals (filter, entry detail) ──────────────────────────────────── */
/* Same width as the other modals (settings/about); height is content-sized.
   fit-content (not auto) is required here: the dialog's UA styles set
   position:fixed with inset-block:0, and `auto` resolves against that
   containing block by stretching to max-height with the slack absorbed by
   auto margins — it does not shrink to content the way a normal block does. */
dialog.modal-dialog--compact[open] {
  height: fit-content;
  max-height: min(720px, calc(100dvh - 4rem));
}

/* Under fit-content, the base rule's .modal-body flex: 1 (basis 0) leaves
   the dialog's intrinsic height to the engine's flex intrinsic-sizing rules,
   which don't reliably account for the body's real content height — in the
   field the debug inspector collapsed to little more than its header while
   the entry modal (patched with an explicit body cap in the oq#140 audit)
   rendered fine in the same session. Generalize that working pattern to
   every compact modal: basis auto makes the body's content height count in
   the dialog's fit-content sizing, the explicit cap (dialog cap minus ~4rem
   of header) keeps it inside the dialog, and shrink (with the base rule's
   min-height: 0) absorbs the arithmetic slack — the cap is content-box
   while the header + body padding are a bit over 4rem — so content scrolls
   instead of being clipped by the dialog's overflow: hidden. */
dialog.modal-dialog--compact[open] .modal-body {
  flex: 0 1 auto;
  overflow-y: auto;
  max-height: calc(min(720px, 100dvh - 4rem) - 4rem);
}

/* Entry detail modal: credit-card proportions, strictly content-sized */
#entry-modal[open] {
  width: min(380px, calc(100% - 2rem));
  height: fit-content;
  max-height: min(72dvh, 560px);
  border-top: 3px solid var(--entry-border, var(--kc-border-card));
}

/* Entry modal caps lower than the shared compact cap — mirror its own. */
#entry-modal .modal-body {
  max-height: calc(min(72dvh, 560px) - 4rem);
}

#entry-modal .modal-title {
  color: var(--entry-text, var(--kc-text));
}

.related-modal[open] { width: min(94vw, 1100px); max-height: min(82dvh, 760px); }
.related-list { display: grid; gap: .5rem; padding-left: 1.25rem; }
.related-controls { display:flex; gap:.5rem; align-items:center; flex-wrap:wrap; margin-bottom:1rem; }
.related-word-link { border:0; background:none; color:var(--kc-accent); cursor:pointer; font:inherit; padding:0; text-decoration:underline; }
.related-graph { overflow:hidden; width:100%; }
.related-graph svg { display:block; min-height:360px; width:100%; }
.related-graph line { stroke:var(--kc-border-card); opacity:.8; }
.related-graph-source { fill:var(--kc-accent); font-size:24px !important; font-weight:700; }
.related-graph-node { cursor:pointer; }
.related-graph-node:focus-visible { outline: none; }
.related-graph-node:focus-visible text { paint-order:stroke; stroke:var(--kc-text); stroke-width:3px; }
.related-graph text { fill:var(--kc-text); font-size:16px; }
.related-graph-gloss { font-size:10px !important; opacity:.8; }

.filter-btn {
  position: relative;
}

/* Toolbar "clear filters" X: dead while no filters are set, danger-red once
   any are active */
.icon-btn--danger:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.icon-btn--danger:not(:disabled) {
  color: var(--kc-danger);
  border-color: var(--kc-danger);
}

.icon-btn--danger:not(:disabled):hover,
.icon-btn--danger:not(:disabled):focus-visible {
  background: var(--kc-bg-tab-active);
  color: var(--kc-danger);
  border-color: var(--kc-danger);
}

.filter-badge {
  position: absolute;
  top: -0.375rem;
  right: -0.375rem;
  min-width: 1rem;
  height: 1rem;
  padding: 0 0.25rem;
  border-radius: 0.5rem;
  background: var(--kc-accent);
  color: var(--kc-bg-card);
  font-size: 0.625rem;
  font-weight: 700;
  line-height: 1rem;
  text-align: center;
}

.filter-class-list {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}

.filter-check-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.875rem;
  cursor: pointer;
}

.filter-check-row input[type="checkbox"] {
  accent-color: var(--kc-accent);
  cursor: pointer;
}

/* Text variant of the danger icon button, for "Clear all filters" in the
   filter modal — same chrome and disabled/active behaviour as the X */
.filter-clear--text {
  width: auto;
  padding: 0 0.75rem;
  font-size: 0.75rem;
  font-weight: 600;
}

/* Inline "↩ Deselected automatically" note shown when picking a setting
   auto-resets an incompatible one. Pops in pointing at the affected strip,
   wiggles its arrow, then fades out on its own. */
.filter-autonote {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  margin-left: 0.5rem;
  padding: 0.125rem 0.5rem;
  /* Cancel the vertical padding+border out of the line box so the pill
     popping in doesn't change the heading's line height (page reflow) */
  margin-block: calc(-0.125rem - 1px);
  border-radius: 999px;
  font-size: 0.6875rem;
  font-weight: 500;
  text-transform: none;
  letter-spacing: normal;
  color: var(--kc-danger);
  border: 1px solid color-mix(in srgb, var(--kc-danger) 45%, transparent);
  background: color-mix(in srgb, var(--kc-danger) 12%, transparent);
}

/* The leading ↩ arrow gets its own span-less wiggle via ::before */
.filter-autonote::before {
  content: "↩";
  display: inline-block;
  font-weight: 700;
}

/* Durations are set as inline custom properties by filter-modal.js
   (NOTE_DURATION_MS / NOTE_WIGGLE_MS) — the values here are fallbacks */
.filter-autonote--show {
  animation: autonote-pop var(--autonote-duration, 6s) ease forwards;
}

.filter-autonote--show::before {
  animation: autonote-wiggle var(--autonote-wiggle, 0.9s) ease 2;
}

@keyframes autonote-pop {
  0%   { opacity: 0; transform: translateX(0.5rem) scale(0.85); }
  8%   { opacity: 1; transform: translateX(0) scale(1.05); }
  14%  { transform: translateX(0) scale(1); }
  75%  { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes autonote-wiggle {
  0%, 100% { transform: translateX(0) rotate(0deg); }
  25%      { transform: translateX(-0.15rem) rotate(-12deg); }
  75%      { transform: translateX(0.1rem) rotate(8deg); }
}

@media (prefers-reduced-motion: reduce) {
  .filter-autonote--show,
  .filter-autonote--show::before {
    animation: none;
  }
}

.filter-check-row--standalone {
  margin-top: 0.75rem;
}

.filter-hint {
  margin-top: 0.75rem;
  font-size: 0.75rem;
  color: var(--kc-text-faint);
}

.filter-len-row {
  display: flex;
  gap: 1rem;
}

.filter-len-field {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.filter-len-field .input-field {
  width: 5rem;
}

.add-btn:disabled {
  opacity: 0.45;
  cursor: default;
}

.add-btn:disabled:active { transform: none; }

/* ── About modal tab strip ───────────────────────────────────────────────── */
.modal-tab-area {
  padding: 0.625rem 1.25rem;
  flex-shrink: 0;
  border-bottom: 1px solid var(--kc-border-card);
}

/* Sub-tab strip inside a tab panel (e.g. About → About/Sources/Charts) */
.modal-tab-area--sub {
  margin: -1rem -1.25rem 1.25rem;
  border-top: none;
}

.about-sub-panel {
  flex: 1;
  overflow-y: auto;
}

/* ── Sources tab table ───────────────────────────────────────────────────── */
.sources-table {
  width: 100%;
  table-layout: auto;
  border-collapse: collapse;
  font-size: 0.8125rem;
}

.sources-table th,
.sources-table td {
  text-align: left;
  padding: 0.375rem 0.5rem;
  border-bottom: 1px solid var(--kc-border-card);
  vertical-align: top;
  overflow-wrap: anywhere;
}

.sources-table th:first-child,
.sources-table td:first-child {
  padding-left: 0;
}

.sources-table th:last-child,
.sources-table td:last-child {
  padding-right: 0;
}

.sources-table th {
  font-weight: 600;
  color: var(--kc-text-muted);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* ── Charts tab ──────────────────────────────────────────────────────────── */
.chart-item {
  margin-bottom: 2rem;
}

.chart-item:last-child {
  margin-bottom: 0;
}

.chart-item > h3 {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--kc-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.5rem;
}

.chart-img {
  display: block;
  width: 100%;
  height: auto;
  min-width: 0;
  border-radius: 0.5rem;
  border: 1px solid var(--kc-border-card);
  background: #ffffff;
  padding: 1rem;
  box-sizing: border-box;
}

.chart-svg-container {
  box-sizing: border-box;
  width: 100%;
  overflow-x: auto;
  border: 1px solid var(--kc-border-card);
  border-radius: 0.75rem;
  padding: 1rem;
  background: var(--kc-bg-input);
}

.chart-svg-container > svg {
  display: block;
  width: 100%;
  min-width: 480px;
  height: auto;
}

/* ── Related tab cards ───────────────────────────────────────────────────── */
.related-card-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.75rem;
}

.related-card {
  min-width: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background: var(--kc-bg-card);
  border: 1px solid var(--kc-border-card);
  border-radius: 0.5rem;
}

.related-card-img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: var(--kc-bg-input);
  border-bottom: 1px solid var(--kc-border-card);
}

.related-card-body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem;
}

.related-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.related-card-kind {
  flex: 0 0 auto;
  color: var(--kc-text-muted);
  background: var(--kc-bg-input);
  border: 1px solid var(--kc-border-card);
  border-radius: 0.25rem;
  padding: 0.125rem 0.375rem;
  font-size: 0.625rem;
  font-weight: 600;
  line-height: 1.2;
  text-transform: uppercase;
}

.related-card-date {
  min-width: 0;
  color: var(--kc-text-faint);
  font-size: 0.625rem;
  line-height: 1.2;
  text-align: right;
}

.related-card-title {
  display: block;
  min-width: 0;
  color: var(--kc-text);
  font-size: 0.875rem;
  font-weight: 600;
  line-height: 1.25;
  overflow-wrap: anywhere;
}

.related-card-author,
.related-card-description {
  margin: 0;
  color: var(--kc-text-muted);
  font-size: 0.75rem;
  line-height: 1.35;
  overflow-wrap: anywhere;
}

.related-card-description {
  color: var(--kc-text-faint);
}

/* ── Theme preview widget (adapted from jandahl/acl-inspector) ───────────── */
.theme-preview-radar {
  position: relative;
  width: 100%;
  border: 1px solid var(--kc-border);
  border-radius: 14px;
  padding: 18px;
  pointer-events: none;
  background: var(--preview-dark, var(--kc-bg));
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.28);
  /* overflow: clip (not hidden) so animated pseudo-elements with blend modes
     are paint-clipped without creating a scroll container that leaks width. */
  overflow: clip;
  margin-bottom: 1rem;
}

.theme-preview-radar::before {
  content: "";
  position: absolute;
  inset: -35%;
  background:
    radial-gradient(circle at 22% 24%, color-mix(in srgb, var(--preview-dark, var(--kc-bg)) 80%, white) 0%, transparent 58%),
    radial-gradient(circle at 74% 18%, color-mix(in srgb, var(--preview-accent, var(--kc-accent)) 75%, transparent) 0%, transparent 60%),
    radial-gradient(circle at 45% 78%, var(--preview-dark, var(--kc-bg)) 0%, transparent 68%),
    radial-gradient(circle at 30% 68%, color-mix(in srgb, var(--preview-accent, var(--kc-accent)) 45%, transparent) 0%, transparent 72%);
  mix-blend-mode: screen;
  opacity: 0.9;
  /* animation disabled — see issue #138 */
}

.theme-preview-radar::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.18);
  mix-blend-mode: overlay;
  opacity: 0.55;
  /* animation disabled — see issue #138 */
}

.preview-content {
  position: relative;
  z-index: 1;
}

.preview-grid {
  display: flex;
  flex-direction: column;
}

.preview-canvas {
  position: relative;
  border-radius: 10px;
  overflow: hidden;
}

.preview-card {
  border-radius: 10px;
  padding: 10px;
  background: var(--preview-bg, var(--kc-bg));
  border: 1px solid var(--preview-border, var(--kc-border));
  color: var(--preview-text, var(--kc-text));
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25);
  font-size: 0.875rem;
  overflow: hidden;
}

/* Dark card is flow-positioned — its content drives the container height */
.preview-card[data-theme-preview="dark"] { position: relative; z-index: 1; }
/* Light card is an absolute overlay over the dark card */
.preview-card[data-theme-preview="light"] { position: absolute; inset: 0; z-index: 2; }

/* Both cards are visible in radar mode; individual show/hide rules removed */

/* Lexeme columns inside preview cards — two columns, same gap as the real card grid.
   Content is populated at runtime by theme-preview.js using createEntryCard(). */
.preview-lexemes { display: flex; flex-direction: row; gap: 0.5rem; }

/* Reserve height before async card load completes so there is no layout shift.
   Card height is derived from the actual .preset-card/.card-* values, each
   multiplied by its Text Size scale var so this reservation self-adjusts
   instead of silently desyncing when a user changes those sliders — this
   preview is filled with real createEntryCard() output, so it's subject to
   the same scale vars as the main results grid:
     2×1rem (padding-block) + 2px (borders)
     + 0.875rem×1.2×word-scale  (card-word font-size × line-height)
     + 0.125rem                (card-gloss margin-top)
     + 0.625rem×1.2×gloss-scale (card-gloss font-size × line-height)
     + 0.25rem                 (card-desc  margin-top)
     + 0.625rem×1.2×desc-scale  (card-desc  font-size × line-height)
   If any of those literal values change in the card CSS above, update them here too. */
.preview-col {
  display: flex; flex-direction: column; gap: 0.5rem; flex: 1; min-width: 0;
  min-height: calc(
    3 * (
      2rem + 2px
      + 0.875rem * var(--kc-card-word-scale, 1) * 1.2
      + 0.125rem
      + 0.625rem * var(--kc-card-gloss-scale, 1) * 1.2
      + 0.25rem
      + 0.625rem * var(--kc-card-desc-scale, 1) * 1.2
    )
    + 2 * 0.5rem
  );
}

/* On narrow screens (portrait phones up to ~480px) show one preview column */
@media (max-width: 480px) {
  .preview-col:last-child { display: none; }
}

@keyframes preview-warp {
  0%   { transform: scale(1)    rotate(0deg);   filter: hue-rotate(0deg); }
  35%  { transform: scale(1.12) rotate(120deg); filter: hue-rotate(25deg); }
  70%  { transform: scale(1.05) rotate(240deg); filter: hue-rotate(15deg); }
  100% { transform: scale(1)    rotate(360deg); filter: hue-rotate(0deg); }
}

@keyframes preview-pulse {
  0%   { opacity: 0.35; }
  100% { opacity: 0.65; }
}

/* ── Radar sweep: light card sweeps over dark card via rotating half-mask ─── */

@property --sweep-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@keyframes preview-sweep {
  0%   { --sweep-angle: 0deg; }
  100% { --sweep-angle: 360deg; }
}

/* ── Side-by-side mode: static 50/50 split, no animation ─────────────────── */
.preview-canvas[data-preview-mode="split"] .preview-card[data-theme-preview="light"] {
  clip-path: inset(0 0 0 50%);
}

.preview-canvas[data-preview-mode="radar"] .preview-card[data-theme-preview="light"] {
  z-index: 2;
  --sweep-angle: 0deg;
  /* Rotating half-mask: opaque half reveals the light card like a radar beam */
  -webkit-mask-image: linear-gradient(
    var(--sweep-angle),
    transparent 0%, transparent 49.99%,
    black 50%, black 100%
  );
  mask-image: linear-gradient(
    var(--sweep-angle),
    transparent 0%, transparent 49.99%,
    black 50%, black 100%
  );
  -webkit-mask-mode: alpha;
  mask-mode: alpha;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  animation: preview-sweep var(--preview-speed, 12s) linear infinite;
}

/* The ambient glow behind both cards uses --preview-dark for dark half
   and --preview-light for light half to give both sides context */
.theme-preview-radar::before {
  background:
    radial-gradient(circle at 22% 24%, color-mix(in srgb, var(--preview-dark, var(--kc-bg)) 80%, white) 0%, transparent 58%),
    radial-gradient(circle at 74% 18%, color-mix(in srgb, var(--preview-accent, var(--kc-accent)) 75%, transparent) 0%, transparent 60%),
    radial-gradient(circle at 45% 78%, var(--preview-light, var(--kc-bg)) 0%, transparent 68%),
    radial-gradient(circle at 30% 68%, color-mix(in srgb, var(--preview-accent, var(--kc-accent)) 45%, transparent) 0%, transparent 72%);
}

/* ── Theme fade transition ───────────────────────────────────────────────────── */
/* Applied around color-mode switches for a cross-fade of theme colors, default
   0.5s and adjustable via the Experimental "Theme fade duration" slider —
   --kc-theme-fade-ms is a unitless ms count set by docs/color-mode.js, so
   calc(...*1ms) is what turns it into an actual CSS duration.
   !important is intentional: overrides per-element transition shorthands only
   while the class is present, then removes itself. Excludes animations so
   @keyframes-driven effects (modal-in, preview-warp, etc.) aren't disrupted. */
html.theme-transitioning *:not([style*="animation"]) {
  transition:
    background-color calc(var(--kc-theme-fade-ms, 500) * 1ms) ease,
    color calc(var(--kc-theme-fade-ms, 500) * 1ms) ease,
    border-color calc(var(--kc-theme-fade-ms, 500) * 1ms) ease,
    box-shadow calc(var(--kc-theme-fade-ms, 500) * 1ms) ease,
    fill calc(var(--kc-theme-fade-ms, 500) * 1ms) ease,
    stroke calc(var(--kc-theme-fade-ms, 500) * 1ms) ease !important;
}

@media (prefers-reduced-motion: reduce) {
  html.theme-transitioning * {
    transition: none !important;
  }
}

/* Bounce animation for the newly-active mode button */
@keyframes mode-btn-pop {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.22); }
  65%  { transform: scale(0.92); }
  85%  { transform: scale(1.06); }
  100% { transform: scale(1); }
}

.icon-btn--mode-pop {
  animation: mode-btn-pop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}

/* ── PWA update toast ────────────────────────────────────────────────────────── */
/* [popover]'s UA default stylesheet sets inset:0, margin:auto, and
   color:CanvasText (among other resets) — explicitly override all of these
   so nothing is left over to fight the positioning below or bypass the
   app's theme (CanvasText ignores our --kc-text variable entirely, since it's
   a directly-set UA property rather than inherited).
   Scoped to :popover-open — an unconditional `display: flex` here is an
   AUTHOR-origin declaration, which always beats the UA's own
   `[popover]:not(:popover-open) { display: none }` regardless of
   specificity (author-normal outranks user-agent-normal in the cascade),
   so without this the toast would render permanently regardless of whether
   an update is actually available. */
.pwa-update-toast:popover-open {
  position: fixed;
  inset: auto;
  margin: 0;
  bottom: 1.25rem;
  left: 50%;
  transform: translateX(-50%);
  color: inherit;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  background: var(--kc-bg-card);
  border: 1px solid var(--kc-border);
  border-radius: 0.75rem;
  padding: 0.625rem 1rem;
  font-size: 0.875rem;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
  z-index: 100;
  white-space: nowrap;
}

.pwa-update-btn {
  padding: 0.25rem 0.75rem;
  background: var(--kc-accent, #34d399);
  color: #fff;
  border: none;
  border-radius: 0.5rem;
  font-size: 0.8125rem;
  font-weight: 600;
  cursor: pointer;
}

.pwa-update-close {
  background: none;
  border: none;
  color: var(--kc-text-muted);
  cursor: pointer;
  padding: 0 0.25rem;
  font-size: 1rem;
  line-height: 1;
}

/* ── Colour-mode switch toast ────────────────────────────────────────────────── */
.mode-toast {
  position: fixed;
  top: 3.75rem;
  right: 1rem;
  left: auto;
  transform: none;
  width: max-content;
  min-width: 0;
  max-width: calc(100vw - 2rem);
  box-sizing: border-box;
  background: var(--kc-bg-card);
  border: 1px solid var(--kc-border);
  border-radius: 999px;
  padding: 0.375rem 0.875rem;
  font-size: 0.9375rem;
  font-weight: 700;
  color: var(--kc-text);
  text-align: center;
  box-shadow: 0 0.75rem 2.5rem rgba(0, 0, 0, 0.28);
  z-index: 30;
  pointer-events: none;
  opacity: 0;
  overflow-wrap: anywhere;
  transition: opacity 0.2s ease;
}

.mode-toast--liquid {
  /* --kc-liquid-toast-rgb comes from :root (theme-dependent, above);
     --kc-liquid-ass-reaction/-fill-alpha come from docs/liquid-ass.js,
     which sets them on <html> so every opted-in surface shares one live
     value, not just this toast — do not redeclare either here, it would
     shadow the inherited value with a stale local one and the reaction
     slider would stop doing anything. The effect's on/off state isn't
     gated by a CSS attribute selector at all: it's the .mode-toast--liquid/
     .liquid-ass-surface classes being present or absent, and
     --kc-liquid-ass-fill-alpha being removed entirely (not zeroed) when
     disabled. Background/border/box-shadow/backdrop-filter/isolation/
     overflow are shared with every other glass surface — see the "Liquid
     Ass" section below; this rule only keeps the toast's own unique
     geometry. */
  transform: scale(0.96);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.14);
  transition:
    opacity 0.2s ease,
    transform 0.36s cubic-bezier(0.2, 1.25, 0.32, 1);
}

.mode-toast--visible {
  opacity: 1;
}

.mode-toast--liquid.mode-toast--visible {
  transform: scale(1);
}

@media (prefers-reduced-motion: reduce) {
  .mode-toast--liquid {
    transform: none;
    transition: opacity 0.2s ease;
  }

  .mode-toast--liquid.mode-toast--visible {
    transform: none;
  }
}

/* ── Liquid Ass: one shared glass recipe for every surface ───────────────────────
   Originally each surface had its own variant of a decorative recipe (two
   fixed-position colored gradient "light source" blobs + a diagonal sheen
   highlight + an inset-highlight box-shadow trio) copied from the original
   colour-mode toast. That recipe was never actually glass — a fixed overlay
   unrelated to what's behind the element — and looked wrong even on the
   single toast it started from, let alone tiled across many dictionary
   cards (an identical repeating "stamp") or clipped to a header bar
   (fading out before the edges). Unified: every surface now gets the exact
   same flat, uniform translucent tint + a plain border + a plain outer
   shadow + real backdrop-filter blur — the blur is what actually reads as
   "glass"; no positioned gradients, no sheen pseudo-elements anywhere.
   Compound selectors (not a standalone .liquid-ass-surface rule) so this
   reliably wins over each surface's own same-or-lower-specificity rules
   (e.g. .preset-card:hover) regardless of source order. */
.mode-toast--liquid,
dialog.modal-dialog.liquid-ass-surface,
.sticky-chrome.liquid-ass-surface,
.preset-card.liquid-ass-surface,
#pwa-update-toast.liquid-ass-surface:popover-open {
  background: rgba(var(--kc-liquid-toast-rgb), var(--kc-liquid-ass-fill-alpha));
  isolation: isolate;
}

/* backdrop-filter is deliberately NOT on dialog.modal-dialog here — this
   dialog is the one surface a *nested* glass element (the colour-mode
   toast, reparented into it during the reaction-slider preview) can be
   shown inside. Nesting one backdrop-filter element inside another breaks
   blur compositing for the child in this browser engine (confirmed by
   screenshot testing in an earlier round); the dialog still gets its own
   real blur via the existing dialog.modal-dialog::backdrop pseudo-element,
   so this isn't a loss of the intended look, just avoids reintroducing
   that bug.

   Blur/saturate scale off the same --kc-liquid-ass-reaction number the
   tint alpha already uses (0–100, set by docs/liquid-ass.js), so "Glass
   Reaction" is one true intensity dial instead of only controlling tint —
   at 0% this now resolves to blur(0px) saturate(1), i.e. no blur at all,
   fixing text becoming unreadable at low reaction from a heavy blur
   sitting under a nearly-invisible tint. At 100% this is unchanged from
   before: blur(24px) saturate(1.7). Safe to reference unconditionally:
   when disabled, the element loses the .liquid-ass-surface/
   mode-toast--liquid class this selector requires, so the rule (and its
   var() reference) never applies rather than resolving against a removed
   custom property. */
.mode-toast--liquid,
.sticky-chrome.liquid-ass-surface,
.preset-card.liquid-ass-surface,
#pwa-update-toast.liquid-ass-surface:popover-open {
  -webkit-backdrop-filter: blur(calc(var(--kc-liquid-ass-reaction) * 0.24px))
    saturate(calc(1 + var(--kc-liquid-ass-reaction) * 0.007));
  backdrop-filter: blur(calc(var(--kc-liquid-ass-reaction) * 0.24px))
    saturate(calc(1 + var(--kc-liquid-ass-reaction) * 0.007));
}

/* .page-wrapper centers content with a max-width and 2rem side padding —
   invisible normally, since .sticky-chrome's plain background matches the
   page background exactly. Once it gets a differently-tinted glass
   background, that padding becomes a visible seam between the header and
   the actual screen edge. Standard full-bleed break-out: negative margins
   sized to exactly cancel .page-wrapper's own inset (using percentages,
   which resolve against the *containing block*'s width regardless of this
   element's own width, so the same formula is correct at any viewport
   size), then pad back in by the same amount so the header's own content
   (title/icons/search) stays exactly where it visually sat before. Only
   reaches the true viewport edge below .page-wrapper's max-width (~64rem);
   .page-wrapper's overflow-x: clip suppresses it above that, which is fine
   since every reported case of this is a phone-width viewport anyway. */
.sticky-chrome.liquid-ass-surface {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  padding-left: calc(50vw - 50%);
  padding-right: calc(50vw - 50%);
  box-sizing: border-box;
}

/* Border + plain shadow + clipping — every self-contained "panel" surface
   (a toast, a modal, a card) gets the same values. .sticky-chrome is a
   full-width bar, not a panel — it keeps its own existing border-bottom
   instead (see the base .sticky-chrome rule) and skips this part. */
.mode-toast--liquid,
dialog.modal-dialog.liquid-ass-surface,
.preset-card.liquid-ass-surface,
#pwa-update-toast.liquid-ass-surface:popover-open {
  border: 1px solid var(--kc-liquid-border);
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.32);
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  /* The single colour-mode toast keeps backdrop-filter active under
     reduced-motion (only its transform/transition drop, in the block
     above) — one instance is cheap. The multi-instance surfaces here also
     drop the blur itself, a real compositing cost repeated across however
     many dictionary cards are on screen at once, not just a motion concern. */
  .sticky-chrome.liquid-ass-surface,
  .preset-card.liquid-ass-surface,
  #pwa-update-toast.liquid-ass-surface:popover-open {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* Icon buttons, the search input, and the clear button sit *on top of* a
   glass surface — without their own treatment they stay solid, opaque boxes,
   reading as flat controls glued onto a translucent panel rather than one
   cohesive glass material. A plain descendant selector (not a JS-toggled
   class) is enough, since this only needs to activate wherever an ancestor
   is already glass — lighter blur/tint than the surface itself since these
   are small "pills" riding on glass, not the glass surface itself.
   border-color only (not the border shorthand) so width/style are untouched.
   Blur/saturate scale with --kc-liquid-ass-reaction the same way the parent
   surface's do above, just at this element's own lighter ceiling
   (blur(10px) saturate(1.3) at 100%, none of either at 0%). */
.liquid-ass-surface .icon-btn,
.liquid-ass-surface .input-field,
.liquid-ass-surface .select-field {
  background: rgba(var(--kc-liquid-toast-rgb), var(--kc-liquid-ass-fill-alpha));
  border-color: var(--kc-liquid-nested-border);
  -webkit-backdrop-filter: blur(calc(var(--kc-liquid-ass-reaction) * 0.1px))
    saturate(calc(1 + var(--kc-liquid-ass-reaction) * 0.003));
  backdrop-filter: blur(calc(var(--kc-liquid-ass-reaction) * 0.1px))
    saturate(calc(1 + var(--kc-liquid-ass-reaction) * 0.003));
}

@media (prefers-reduced-motion: reduce) {
  .liquid-ass-surface .icon-btn,
  .liquid-ass-surface .input-field,
  .liquid-ass-surface .select-field {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* ── PWA splash screen ───────────────────────────────────────────────────────── */
#splash-screen {
  position: fixed;
  inset: 0;
  background: #C8102E;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  animation: splash-exit 0.35s ease-in 1.5s forwards;
  cursor: pointer; /* tap/click skips the animation (see splash.js) */
}

.splash-text {
  font-size: clamp(5rem, 28vmin, 22rem);
  font-weight: 900;
  color: #fff;
  letter-spacing: -0.02em;
  line-height: 1;
  user-select: none;
  animation: stamp-in 0.65s cubic-bezier(0.22, 0.61, 0.36, 1) 0.15s both;
}

@keyframes stamp-in {
  0% {
    transform: translateY(-110vh) rotate(-20deg) scale(2.5);
    opacity: 0;
  }
  8% {
    opacity: 1;
  }
  20% {
    transform: translateY(-75vh) rotate(-20deg) scale(2.1);
  }
  38% {
    transform: translateY(-38vh) rotate(-20deg) scale(1.65);
  }
  52% {
    transform: translateY(-12vh) rotate(-20deg) scale(1.2);
  }
  62% {
    transform: translateY(0) rotate(-20deg) scale(0.9);
    opacity: 1;
  }
  76% {
    transform: translateY(0) rotate(-20deg) scale(1.05);
  }
  90% {
    transform: translateY(0) rotate(-20deg) scale(0.98);
  }
  100% {
    transform: translateY(0) rotate(-20deg) scale(1);
  }
}

@keyframes splash-exit {
  to {
    opacity: 0;
    visibility: hidden;
  }
}

#splash-screen[data-splash-variant="stamp-wobble"] {
  animation: splash-exit 0.35s ease-in 2.05s forwards;
}

#splash-screen[data-splash-variant="stamp-wobble"] .splash-text {
  animation: stamp-wobble-in 1.45s cubic-bezier(0.19, 0.74, 0.32, 1) 0.12s both;
  text-shadow:
    0.035em 0.045em 0 rgba(126, 7, 30, 0.32),
    -0.018em -0.012em 0 rgba(255, 255, 255, 0.18);
}

@keyframes stamp-wobble-in {
  0% {
    transform: translateY(-72vh) rotate(-12deg) scale(1.48);
    opacity: 0;
  }
  12% {
    opacity: 1;
  }
  32% {
    transform: translateY(-26vh) rotate(-12deg) scale(1.26);
  }
  52% {
    transform: translateY(0) rotate(-12deg) scale(0.94);
  }
  64% {
    transform: translateY(0) rotate(-15deg) scale(1.04);
  }
  76% {
    transform: translateY(0) rotate(-10deg) scale(0.985);
  }
  88% {
    transform: translateY(0) rotate(-13deg) scale(1.012);
  }
  100% {
    transform: translateY(0) rotate(-12deg) scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  #splash-screen {
    animation: none;
    display: none;
  }
}

/* ── OQAA animation variant ─────────────────────────────────────────────────── */
/* Two absolutely-positioned spans meet at the horizontal screen centre:
   .oqaa-left (right:50%) grows leftward as A's are injected, scrolling OQ
   off the left edge; .oqaa-excl (left:50%) holds "!" fixed at centre. */
#splash-screen[data-splash-variant="oqaa"] {
  animation: none;
  overflow: hidden; /* clip .oqaa-left as it extends past the left viewport edge */
}

[data-splash-variant="oqaa"] .splash-text {
  animation: none;
  position: absolute;
  inset: 0; /* fill the splash screen so children can be absolutely centred */
  font-size: clamp(2.5rem, 14vmin, 11rem); /* ~half the stamp-in size */
}

[data-splash-variant="oqaa"]:not([data-oqaa-ready]) .splash-text {
  visibility: hidden;
}

.oqaa-left {
  position: absolute;
  right: 50%; /* right edge pinned at screen centre */
  top: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
}

.oqaa-excl {
  position: absolute;
  left: 50%; /* left edge at screen centre, immediately after .oqaa-left */
  top: 50%;
  transform: translateY(-50%);
}
