/* English Buddy client styles (architecture section 5.2). Phone-first: a phone is the primary
   device; a min-width media query widens the layout for desktop. No external font, no CDN --
   CSP's font-src 'self' allows only same-origin files and none are shipped, so we use the system
   font stack everywhere. No !important anywhere in this file. */

:root {
  color-scheme: light dark;
  --bg: #f5f6f8;
  --surface: #ffffff;
  --text: #16181d;
  --text-dim: #5b6270;
  --border: #dde1e7;
  --accent: #3462eb;
  --accent-text: #ffffff;
  --danger: #c62828;
  --danger-bg: #fdecea;
  --success: #1f8a4c;
  --warn-bg: #fff6e0;
  --chip-bg: #eef1f6;
  --radius: 10px;
  --gap: 12px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14161a;
    --surface: #1d2026;
    --text: #eef0f4;
    --text-dim: #9aa2b1;
    --border: #30343c;
    --accent: #6a8dff;
    --accent-text: #0c1020;
    --danger: #ff6b6b;
    --danger-bg: #3a1f1f;
    --success: #4fd07c;
    --warn-bg: #3a2f14;
    --chip-bg: #262a32;
  }
}

* {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  height: 100%;
}

body {
  font-size: 16px;
  line-height: 1.4;
}

/* height, NOT min-height (bug found 2026-09-16): .chat-screen/.scroll depend on this being a
   BOUNDED height for their own flex:1/overflow-y:auto chain to actually cap content and scroll
   internally, the way the "mobile-bottom-spacing" rule below assumes. With min-height, #app (and
   therefore body, which has no overflow:hidden) simply grows past one viewport once content -- a
   long conversation transcript, in the report that found this -- is taller than the screen, which
   pushes the docked composer/mic bar below the fold instead of it staying pinned at the bottom.
   Reproduced on a real phone-height viewport; a taller desktop window happened to hide it because
   the same content still fit in one screen there.

   100dvh, with 100vh as a fallback declared first (an unsupported value is ignored, leaving the
   earlier declaration; a supported one overrides it -- ordinary cascade, no @supports needed): the
   APK's Capacitor WebView has no address bar, so 100vh there is already correct and stayed fixed
   by the change above -- but the SAME fix then surfaced the well-known mobile-browser version of
   this bug on the desktop-web-mobile build (reported 2026-09-16, same day): a phone browser's
   100vh assumes its address bar is permanently hidden, so real content is taller than what's
   actually visible, pushing the composer behind the browser's own UI chrome. 100dvh tracks the
   ACTUAL visible viewport as that chrome shows/hides. */
#app {
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh;
}

/* Every scrollable screen body uses this class (mobile-bottom-spacing memory rule): padding for
   the Android gesture/nav bar, which is only non-zero because index.html sets
   viewport-fit=cover. Do NOT drop this rule on a screen that scrolls -- content will sit under
   the phone's nav bar with no way to reach it. */
.scroll {
  flex: 1;
  overflow-y: auto;
  padding: var(--gap);
  padding-bottom: calc(env(safe-area-inset-bottom) + 24px);
}

.screen-header {
  display: flex;
  align-items: center;
  gap: var(--gap);
  padding: calc(env(safe-area-inset-top) + 12px) var(--gap) 12px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.screen-header h1 {
  font-size: 1.15rem;
  margin: 0;
  flex: 1;
}

button {
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  min-width: 44px;
  padding: 10px 16px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface);
}

.btn:disabled {
  opacity: 0.55;
  cursor: default;
}

.btn-primary {
  background: var(--accent);
  color: var(--accent-text);
  border-color: var(--accent);
}

.btn-danger {
  background: var(--danger-bg);
  color: var(--danger);
  border-color: var(--danger);
}

.btn-block {
  width: 100%;
}

.btn-back {
  min-width: 44px;
  min-height: 44px;
  padding: 8px 12px;
}

/* De-emphasized secondary action, e.g. applock.js's "Log out instead" -- looks like a text link,
   not a competing button, so the primary action (Unlock) is the obvious one to reach for. Combine
   with .btn for the same 44px touch target as every other button. */
.btn-link {
  background: none;
  border: none;
  color: var(--text-dim);
  text-decoration: underline;
}

/* Fingerprint lock gate (redesigned 2026-09-19: "it should be smooth like other apps" -- the old
   version was ui.screen()'s ordinary header+scroll chrome with two stacked full-width buttons of
   equal visual weight). Deliberately NOT ui.screen() -- a lock gate is not a content page, so it
   gets a full-height, vertically centered layout instead, matching how most apps present one. */
.lock-screen {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: var(--gap);
  text-align: center;
}

.lock-screen .btn {
  width: 100%;
  max-width: 320px;
}

.lock-icon {
  font-size: 2.75rem;
  line-height: 1;
  margin-bottom: 4px;
}

.lock-screen h1 {
  margin: 0 0 4px;
  font-size: 1.25rem;
}

/* Hold-to-talk relies on pointer capture staying on this element and never producing a text
   selection callout or page scroll while the finger is down (architecture section 5.8). */
.mic {
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: var(--gap);
}

.field label {
  font-size: 0.85rem;
  color: var(--text-dim);
}

.field input,
.field textarea,
.field select {
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px 12px;
  min-height: 44px;
}

.field textarea {
  min-height: 120px;
  resize: vertical;
}

/* A checkbox + its label, as a row -- deliberately NOT `.field` (settings.js, 2026-09-15): that
   class's vertical layout and `.field input` sizing (meant for text inputs) inflated a checkbox
   into a huge centered square, found via a real-device screenshot. */
.checkbox-row {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 10px;
  margin-bottom: var(--gap);
}

.checkbox-row input[type='checkbox'] {
  width: 20px;
  height: 20px;
  margin: 0;
  flex-shrink: 0;
}

.checkbox-row label {
  font-size: 0.95rem;
  color: var(--text);
}

.field .hint {
  font-size: 0.8rem;
  color: var(--text-dim);
}

.field .error {
  font-size: 0.85rem;
  color: var(--danger);
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--gap);
  margin-bottom: var(--gap);
}

.chip {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--chip-bg);
  color: var(--text-dim);
  font-size: 0.78rem;
  margin: 2px 4px 2px 0;
}

.chip-danger {
  background: var(--danger-bg);
  color: var(--danger);
}

.list-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
}

.list-row:last-child {
  border-bottom: none;
}

.muted {
  color: var(--text-dim);
}

.pull-refresh-indicator {
  text-align: center;
  margin: 0 0 8px;
}

.spinner {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--text-dim);
  font-size: 0.9rem;
}

.spinner::before {
  content: "";
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  animation: eb-spin 0.8s linear infinite;
}

@keyframes eb-spin {
  to { transform: rotate(360deg); }
}

#toasts {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(env(safe-area-inset-bottom) + 12px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  pointer-events: none;
  z-index: 1000;
}

/* ROOT CAUSE, found on a real device 2026-09-19 (the "Delete this word" dialog that appeared to
   never close): pointer-events is an INHERITED property -- #toasts' own `none` above (there so an
   auto-dismissing .toast message never blocks taps on the page behind it) was silently inherited
   by ui.confirmAction()'s dialog too, since nothing re-enabled it. A real tap/click is genuinely
   never delivered to an element with pointer-events:none (or that inherits it); every automated
   test that exercised this used element.click()/dispatchEvent(), which BYPASSES pointer-events
   entirely -- that gap is exactly why this was never caught before a real device hit it. Anything
   with actual interactive content inside #toasts (currently just the confirm dialog's .card) must
   opt back in explicitly. */
#toasts .card {
  pointer-events: auto;
}

.toast {
  max-width: min(90vw, 420px);
  padding: 10px 16px;
  border-radius: var(--radius);
  background: var(--text);
  color: var(--bg);
  font-size: 0.9rem;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
}

.toast-error {
  background: var(--danger);
  color: #ffffff;
}

/* Restored 2026-09-15 (a7-home-nav fix pass, 05_verification.md Issue #1): A7's development pass
   deleted this rule believing it was dead once home.js dropped its old nav grid. It was wrong --
   client/www/screens/stats.js still builds its four headline tiles inside a `.nav-grid` container
   for the 2x2 layout. Do NOT delete again without grepping `nav-grid` across client/www/screens/
   first. home.js does NOT use this class any more; it has its own `.action-card`/`.stat-strip`. */
.nav-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
}

.nav-grid .btn {
  min-height: 72px;
  flex-direction: column;
  gap: 4px;
}

/* Desktop keyboard-shortcut hints and a wider single column (architecture section 5, AC section 1
   "desktop gets keyboard shortcuts"). */
.kbd-hint {
  display: none;
  color: var(--text-dim);
  font-size: 0.78rem;
}

@media (min-width: 720px) {
  #app {
    max-width: 640px;
    margin: 0 auto;
    width: 100%;
  }

  .kbd-hint {
    display: block;
  }
}

/* Pass 2: conversation transcript, mic recording states, feedback score (architecture section 5.2
   / 5.10). Appended rather than interleaved above, per the "modify at the end" convention. */
.transcript {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: var(--gap);
}

.bubble {
  max-width: 82%;
  padding: 10px 14px;
  border-radius: var(--radius);
  white-space: pre-wrap;
}

.bubble-user {
  align-self: flex-end;
  background: var(--accent);
  color: var(--accent-text);
}

.bubble-tutor {
  align-self: flex-start;
  background: var(--surface);
  border: 1px solid var(--border);
}

/* Visual state for the hold/tap mic button while createRecorder() is arming/recording/uploading
   (voice.js section 5.8) -- .mic's touch-action:none rule above still applies. */
.mic[data-state="recording"] {
  background: var(--danger);
  color: #ffffff;
  border-color: var(--danger);
}

.mic[data-state="arming"],
.mic[data-state="uploading"] {
  opacity: 0.6;
}

/* A5: level meter (card 12). `.hf-panel`/`.hf-state`/`.hf-controls`/`.hf-why` and the
   `.hf-panel[data-hf=...]` rules were retired with `screens/chatpanel.js` in A6 (2026-09-14,
   motspilot-data/tasks/a6-conversation-ux) -- `.hf-meter`/`.hf-meter > i` stay: hftuning.js's own
   "Test microphone" card still uses this class, and the composer's level strip (below) reuses it
   rather than inventing a second meter. */
.hf-meter {
  height: 8px;
  border-radius: 999px;
  background: var(--chip-bg);
  overflow: hidden;
  margin: 10px 0;
}

.hf-meter > i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--success);
  transition: width 80ms linear;
}

/* ---- Phase B ---- */
/* Review + Stats screens (architecture section 5.15). Appended, per this file's own convention
   (no !important, no inline style= -- guard rules 1/8); .bar's fill is set via `.style.width =`
   in screens/stats.js, which CSSOM property assignment is explicitly exempt from (guard rule 8). */
.stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.stat b {
  font-size: 1.4rem;
}

.stat span {
  color: var(--text-dim);
  font-size: 0.8rem;
}

.bar {
  flex: 1;
  height: 8px;
  border-radius: 999px;
  background: var(--chip-bg);
  overflow: hidden;
  margin: 0 var(--gap);
}

.bar > i {
  display: block;
  height: 100%;
  background: var(--accent);
}

.grade-row {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: center;
}

.grade-row .btn {
  flex: 1;
  min-width: 40px;
}

/* ---- A6: conversation UX (Concept 2 -- docked composer bar + status pill) ---- */
/* motspilot-data/tasks/a6-conversation-ux/02_architecture.md sections 6.1/6.3, 2026-09-14. Appended
   per this file's own convention (no !important, no inline style= -- guard rules 1/8). */

/* The dock: `.scroll` (chattranscript's home) keeps flex:1/overflow-y:auto from its own rule above;
   this just makes chat.js's screen root a flex column so `.composer`, a SIBLING of `.scroll` (not a
   child), lands at the bottom instead of scrolling away with the conversation. */
.chat-screen {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.composer {
  background: var(--surface);
  border-top: 1px solid var(--border);
  padding: 10px var(--gap) calc(env(safe-area-inset-bottom) + 12px);
}

.composer[data-mode="handsfree"] .hf-meter {
  margin: 0 0 8px;
}

.composer-status {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
}

/* The status pill (AC3) -- one line narrating idle/listening/sending/thinking/speaking/paused/
   stopped for ALL of hold/tap/handsfree, via turn-status.js's labelFor()/CHIP_LABELS. */
.pill {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--chip-bg);
  color: var(--text-dim);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.04em;
}

.pill[data-status="listening"],
.pill[data-status="speaking-detected"] {
  background: var(--success);
  color: #ffffff;
}

.pill[data-status="paused"],
.pill[data-status="stopped"] {
  background: var(--chip-bg);
  color: var(--text-dim);
  opacity: 0.85;
}

.btn-icon {
  min-width: 32px;
  min-height: 32px;
  padding: 0;
  border-radius: 50%;
}

.composer-why {
  font-size: 0.85rem;
  margin: 0 0 6px;
}

/* One docked bar (AC2): the field, then EITHER the two-state mic OR Send (D-2), never both. */
.composer-bar {
  display: flex;
  align-items: center;
  gap: 8px;
}

.composer-bar input {
  flex: 1;
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 10px 16px;
  min-height: 44px;
}

.composer .mic {
  border-radius: 50%;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
}

.composer-send {
  border-radius: 50%;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
}

/* Day-mode inline recast (AC5) -- a distinct amber block under the user's OWN bubble, never a
   tutor bubble (rejected alternative, architecture section 12/Alt list). */
.recast {
  max-width: 82%;
  align-self: flex-end;
  padding: 10px 14px;
  border-radius: var(--radius);
  background: var(--warn-bg);
  border: 1px solid var(--border);
}

.recast .diff {
  white-space: pre-wrap;
}

.recast del {
  text-decoration: line-through;
  color: var(--text-dim);
  background: none;
}

.recast ins {
  text-decoration: none;
  font-weight: 600;
  color: var(--success);
  background: none;
}

.recast-note {
  font-size: 0.85rem;
  margin: 6px 0 0;
}

/* A7 (2026-09-15): Home's two action cards + compact stat strip; Settings' quick-links row.
   motspilot-data/tasks/a7-home-nav/02_architecture.md section 5.4. Appended per this file's own
   "modify at the end" convention -- only existing tokens are used, so dark mode keeps working
   automatically via the prefers-color-scheme block above. */
.screen-header .btn-icon {
  min-width: 40px;
  min-height: 40px;
  font-size: 1.05rem;
}

.stat-strip {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 2px;
}

.home-recap {
  font-size: 0.85rem;
  margin: 0 0 var(--gap);
}

.chip-due {
  border: none;
  background: var(--chip-bg);
  color: var(--accent);
  font-weight: 700;
}

.action-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  width: 100%;
  text-align: left;
  padding: 20px 18px;
  margin-bottom: var(--gap);
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--surface);
  color: var(--text);
}

.action-card .action-title {
  font-size: 1.15rem;
  font-weight: 700;
}

.action-card .action-sub {
  font-size: 0.8rem;
  color: var(--text-dim);
}

.action-primary {
  background: var(--accent);
  color: var(--accent-text);
  border-color: var(--accent);
}

.action-primary .action-sub {
  color: var(--accent-text);
  opacity: 0.85;
}

.quick-links {
  display: flex;
  gap: 8px;
  margin-bottom: var(--gap);
}

/* min-height covers the Review link's longest wording ("Review (13 due)") wrapping to two lines
   at typical due-counts, so all three buttons stay the same height either way (2026-09-15 fix --
   found via real-device screenshot: at the default .btn font size, the wrap made Review look
   uneven next to the single-line Stats/History). */
.quick-links .btn {
  flex: 1;
  min-height: 56px;
  padding: 8px 6px;
  font-size: 0.85rem;
  text-align: center;
}

/* ---- Pass 3: proofread smoothness (2026-09-15) ---- */
/* Style toggle as a sliding segmented control (screens/proofread.js) -- replaces two chips that
   only swapped color, no motion. Generic enough other screens can reuse it later. */
.segmented {
  position: relative;
  display: flex;
  padding: 3px;
  border-radius: 999px;
  background: var(--chip-bg);
  width: fit-content;
  margin-bottom: var(--gap);
}

.segmented .indicator {
  position: absolute;
  top: 3px;
  bottom: 3px;
  left: 3px;
  width: var(--seg-w, 50%);
  border-radius: 999px;
  /* var(--accent), not var(--surface) (bug reported 2026-09-16, "not clear; no color highlight"):
     --surface (white) against .segmented's own --chip-bg (near-white) read as barely any contrast
     at all -- the sliding motion worked, the "this one is selected" signal didn't. */
  background: var(--accent);
  box-shadow: 0 1px 3px rgba(22, 24, 29, 0.16);
  transition: transform 220ms cubic-bezier(0.3, 0.7, 0.4, 1), width 220ms cubic-bezier(0.3, 0.7, 0.4, 1);
  transform: translateX(var(--seg-x, 0px));
}

.segmented button {
  position: relative;
  z-index: 1;
  border: none;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 0.85rem;
  font-weight: 650;
  padding: 8px 18px;
  border-radius: 999px;
  transition: color 160ms ease;
}

.segmented button[aria-pressed="true"] {
  color: var(--accent-text);
}

/* Right-aligns a lone action button under a field (proofread.js's Correct button, reported
   2026-09-16: "extreme right, below the text field") without a new generic ".actions" class that
   would invite unrelated reuse -- named for exactly this one job. */
.field-actions {
  display: flex;
  justify-content: flex-end;
}

/* Mic (+ its countdown chip) and Correct sharing one row (reported 2026-09-16: still stacked on
   two lines after the first pass) -- mic stays left, Correct right, vertically centered against
   each other regardless of the mic button's taller label ("Hold to talk"/"Tap to talk"). */
.field-actions-split {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
}

.mic-group {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* The result card fades/slides in on reveal instead of an instant hidden -> visible snap. Applied
   alongside the existing .card class (screens/proofread.js). */
.result-card {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 220ms ease, transform 220ms cubic-bezier(0.2, 0.7, 0.3, 1);
}

.result-card.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.result-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
}

/* Generalized from ".result-head .eyebrow" (2026-09-16): the Tabs redesign reuses this small-caps
   label for "Original" too, outside .result-head, so it needs to work unscoped. */
.eyebrow {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.result-actions {
  display: flex;
  gap: 2px;
}

/* Tabs redesign (2026-09-16, "Option B" prototype -- tmp/local/proofread-redesign -- approved):
   Compare (original + corrected) vs. What changed (the diff explanation alone). */
.tabbar {
  display: flex;
  gap: 20px;
  margin-bottom: 10px;
  border-bottom: 1px solid var(--border);
}

.tab-btn {
  background: none;
  border: none;
  font: inherit;
  cursor: pointer;
  padding: 8px 2px 10px;
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text-dim);
  border-bottom: 2.5px solid transparent;
  margin-bottom: -1px; /* sits on top of .tabbar's own border so the active underline replaces it */
}

.tab-btn.active {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

.tab-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--chip-bg);
  color: var(--text-dim);
  font-size: 0.7rem;
  font-weight: 700;
  vertical-align: middle;
}

.original-block {
  margin: 6px 0 12px;
  background: var(--chip-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 14px;
}

.original-block p {
  margin: 0;
}

.corrected-block {
  background: var(--surface);
  border: 1.5px solid var(--accent);
  border-radius: var(--radius);
  padding: 14px;
  margin: 0;
}

/* Streaming caret appended after the corrected text while token/corrected events are still
   arriving (screens/proofread.js) -- removed once `corrected` lands, so it never shows on the
   already-finished text a fresh mount renders from history. */
.caret {
  display: inline-block;
  width: 2px;
  height: 1em;
  background: currentColor;
  vertical-align: -0.1em;
  margin-left: 1px;
  animation: eb-caret-blink 0.9s steps(1) infinite;
}

@keyframes eb-caret-blink {
  50% { opacity: 0; }
}

/* Each correction as a real strikethrough/insertion pair (matching .recast's del/ins treatment
   above) instead of plain "from -> to" text -- scannable at a glance. Rows stagger in on arrival. */
.diff-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 8px;
  margin-bottom: 8px;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 200ms ease, transform 200ms ease;
}

.diff-row.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.diff-row del {
  text-decoration: line-through;
  color: var(--text-dim);
  background: var(--danger-bg);
  padding: 1px 5px;
  border-radius: 5px;
}

.diff-row ins {
  text-decoration: none;
  font-weight: 650;
  color: var(--success);
  background: var(--warn-bg);
  padding: 1px 5px;
  border-radius: 5px;
}

.diff-row .why {
  flex-basis: 100%;
  font-size: 0.85rem;
  color: var(--text-dim);
}

/* Inline rename for "This device" in the Devices card (screens/settings.js) -- swaps in for the
   name/chip + Rename button, not a separate dialog. */
.rename-edit {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
}

.rename-edit input {
  flex: 1;
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 10px;
  min-height: 40px;
}

/* Correct/Log in buttons carry their own spinner while busy (screens/proofread.js,
   screens/login.js) instead of a separate "Correcting..." status line -- reuses .spinner but
   needs its own contrast on a solid accent background. */
.btn-primary .spinner {
  color: var(--accent-text);
}

.btn-primary .spinner::before {
  border: 2px solid var(--accent-text);
  border-top-color: transparent;
  opacity: 0.9;
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* GET /admin's log tail (src/Api/AdminHandler.php -- server-rendered, not a client/www module).
   One row per log line (.admin-log-row), each with its own Copy button, rather than one big <pre>
   -- added 2026-09-16 alongside admin-copy.js so a single line can be copied on its own, not just
   the whole tail. */
.log-view {
  max-height: 30vh;
  overflow-y: auto;
}
.admin-log-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 2px 0;
}
.admin-log-text {
  white-space: pre-wrap;
  word-break: break-word;
  font-family: monospace;
  font-size: 0.8rem;
  flex: 1;
}
.admin-copy-btn {
  padding: 2px 8px;
  font-size: 0.75rem;
  flex-shrink: 0;
}

/* GET /admin (src/Api/AdminHandler.php), same reason as .log-view above. On a wide desktop browser
   (this page is never packaged into the APK, so it never needs to fit a phone width) the default
   .card/.list-row rules -- sized for a 420px-wide screen -- stretch edge to edge with mostly empty
   space and tall, sparse rows (reported 2026-09-16: "wasting too much empty space"). This block is
   scoped to .admin-page ONLY, so none of it touches the SPA's own .card/table-less screens. */
.admin-page {
  max-width: 1000px;
  margin: 0 auto;
}
.admin-card {
  padding: 10px 14px;
  margin-bottom: 10px;
}
.admin-card > p:first-child {
  margin: 0 0 6px;
}
/* Caps each section's height and scrolls it internally, the same trick .log-view already used --
   without this, three stacked full-height sections force a PAGE-level scrollbar even after the
   table redesign (reported 2026-09-16, "still scrollbar"). Explicitly not solved with tabs: even
   though admin-copy.js later earned this page a narrow script-src 'self' exception for one-click
   copy, real tabs would still mean either a CSS-only radio/:target hack or a second, bigger JS
   surface -- more moving parts than an ops page like this needs.
   overflow-x too, not just -y: .admin-table's nowrap cells can still exceed a phone-width viewport. */
.admin-scroll {
  max-height: 180px;
  overflow: auto;
}
.admin-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
}
.admin-table th,
.admin-table td {
  padding: 4px 8px;
  text-align: left;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.admin-table th {
  position: sticky;
  top: 0;
  background: var(--surface);
  color: var(--text-dim);
  font-weight: 600;
}
.admin-table tr:last-child td {
  border-bottom: none;
}
.admin-ok {
  color: var(--text-dim);
}
.admin-fail {
  color: var(--danger);
}
.admin-log-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 6px;
}
.admin-inline-form {
  display: inline-block;
  margin: 0;
}

/* A single-selector `[hidden]` rule, placed last so source order breaks its specificity tie with
   every other single-class rule in this file (author declarations always beat the UA stylesheet at
   equal specificity, so `.chip`/`.btn`/`.caret`/`.rename-edit`'s own `display` was winning over a
   hidden element -- confirmed for real on chatcomposer.js's mic/Send pair, which are meant to be
   mutually exclusive (D-2) but both rendered because `.btn { display: inline-flex }` kept `hidden`
   from taking effect). No !important (this file's own rule) -- source position does the work
   instead, so this MUST stay the last rule appended below. Flagged non-blocking in A7's delivery
   notes (2026-09-15) for `.chip` alone; fixed here for every class once the same bug turned up on
   `.btn`, `.caret` and `.rename-edit` too. */
[hidden] {
  display: none;
}
