/* ── Design Tokens ──────────────────────────────────── */
/* Dark is the default theme; @media (prefers-color-scheme: light) below
   overrides these same custom properties for users whose system prefers
   light. Colors that get reused at arbitrary opacities elsewhere (accent,
   danger, done) also expose a "-rgb" triplet so rgba(var(--x-rgb), a) calls
   stay theme-aware without hardcoding the color a second time. */
:root {
  color-scheme: dark;

  --bg:       #111116;
  --surface:  #18181d;
  --surface2: #1f1f25;
  --surface3: #27272e;
  --border:   rgba(255,255,255,0.08);
  --border2:  rgba(255,255,255,0.14);
  --border-focus: rgba(255,255,255,0.2);
  --text:     #f0ede8;
  --text2:    #9b9890;
  --text3:    #848180;  /* raised from #5a5855 — passes WCAG AA (4.7:1) */
  --accent:   #7ab8e8;
  --accent-rgb: 122,184,232;
  --danger:   #e87864;
  --danger-rgb: 232,120,100;
  --r:  10px;
  --r2: 14px;
  --nav-h: 60px;    /* mobile bottom nav height */
  --nav-w: 200px;   /* desktop sidebar width */

  /* memo type colors */
  --task:            #e8c17a;
  --task-bg:         rgba(232,193,122,0.08);
  --task-border:     rgba(232,193,122,0.2);
  --note:            #7ab8e8;
  --note-bg:         rgba(122,184,232,0.08);
  --note-border:     rgba(122,184,232,0.2);
  --question:        #b07ae8;
  --question-bg:     rgba(176,122,232,0.08);
  --question-border: rgba(176,122,232,0.2);
  /* Green, so a finding reads as a conclusion rather than as another kind of
     question -- the two sit next to each other in the pill row. Worn by the
     `finding` badge whichever of the three kinds wrote it, and by the neutral
     `insight` button, which is the kind this hue started out naming. */
  --finding:         #6bbf8a;
  --finding-bg:      rgba(107,191,138,0.08);
  --finding-border:  rgba(107,191,138,0.2);

  /* The other two kinds the generate card makes. Deliberately *not* memo type
     colors: no badge ever wears these, and the three buttons are the only
     place they appear. Findings all carry the green `finding` badge whichever
     kind wrote them, so a hue here is about telling three buttons apart at the
     moment of pressing, not about labelling what comes back.

     Warm amber for feedback and pink for hype, both kept clear of --task's
     gold and --question's purple so that a glance at the palette elsewhere in
     the app never has to disambiguate. */
  --feedback:        #e8956b;
  --feedback-bg:     rgba(232,149,107,0.08);
  --feedback-border: rgba(232,149,107,0.2);
  --hype:            #e87ab8;
  --hype-bg:         rgba(232,122,184,0.08);
  --hype-border:     rgba(232,122,184,0.2);
}

@media (prefers-color-scheme: light) {
  :root {
    color-scheme: light;

    --bg:       #f7f6f3;
    --surface:  #ffffff;
    --surface2: #f0efe9;
    --surface3: #e6e4dc;
    --border:   rgba(0,0,0,0.08);
    --border2:  rgba(0,0,0,0.14);
    --border-focus: rgba(0,0,0,0.25);
    --text:     #1c1b18;
    --text2:    #57544d;
    --text3:    #66625a;  /* darkened from a lighter gray to keep WCAG AA on white */
    --accent:   #2f6fa8;
    --accent-rgb: 47,111,168;
    --danger:   #b8402a;
    --danger-rgb: 184,64,42;

    /* memo type colors */
    --task:            #96691a;
    --task-bg:         rgba(150,105,26,0.08);
    --task-border:     rgba(150,105,26,0.2);
    --note:            #2f6fa8;
    --note-bg:         rgba(47,111,168,0.08);
    --note-border:     rgba(47,111,168,0.2);
    --question:        #7a3fc4;
    --question-bg:     rgba(122,63,196,0.08);
    --question-border: rgba(122,63,196,0.2);
    --finding:         #2f7d4f;
    --finding-bg:      rgba(47,125,79,0.08);
    --finding-border:  rgba(47,125,79,0.2);
    --feedback:        #b8541f;
    --feedback-bg:     rgba(184,84,31,0.08);
    --feedback-border: rgba(184,84,31,0.2);
    --hype:            #b8236e;
    --hype-bg:         rgba(184,35,110,0.08);
    --hype-border:     rgba(184,35,110,0.2);
    --done:            #1f8a52;
    --done-rgb:        31,138,82;
    --done-bg:         rgba(31,138,82,0.08);
    --done-border:     rgba(31,138,82,0.2);
  }
}

/* ── Reset ──────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
html, body { height: 100%; background: var(--bg); }
body { font-family: 'DM Sans', sans-serif; color: var(--text); font-size: 15px; line-height: 1.5; }
a { color: var(--accent); text-decoration: none; }

/* ── Dev banner (Vite dev server only, see DevBanner.tsx) ─────────── */
.dev-banner {
  position: sticky;
  top: 0;
  z-index: 1000;
  padding: 6px 12px;
  text-align: center;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--task);
  background: var(--task-bg);
  border-bottom: 0.5px solid var(--task-border);
}

/* ── App shell (wraps nav + page) ───────────────────── */
.app-shell { min-height: 100vh; }

/* ── Page content ───────────────────────────────────── */
.page {
  max-width: 680px;
  margin: 0 auto;
  padding: 16px 14px calc(var(--nav-h) + 20px);
  min-height: 100vh;
}
@media (min-width: 600px) {
  .page { padding-left: 24px; padding-right: 24px; }
}

/* ── Mobile: bottom nav ─────────────────────────────── */
.bottom-nav {
  position: fixed; bottom: 0;
  left: 50%; transform: translateX(-50%);
  width: 100%; max-width: 680px;
  height: var(--nav-h);
  background: var(--bg);
  border-top: 0.5px solid var(--border);
  display: flex; justify-content: space-around; align-items: center;
  padding-bottom: env(safe-area-inset-bottom, 0);
  z-index: 100;
}
.nav-wordmark { display: none; }  /* hidden on mobile */
.nav-footer { display: none; }  /* hidden on mobile, shown in desktop sidebar */
.nav-item {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  color: var(--text3); font-size: 10px; font-family: 'DM Mono', monospace;
  letter-spacing: 0.06em; padding: 4px 16px;
  transition: color 0.15s; text-decoration: none;
  background: none; border: none; cursor: pointer;
}
.nav-item svg { display: block; }
.nav-item.active { color: var(--text); }

/* ── Mobile: "more" overflow menu ───────────────────── */
.nav-more-group { display: contents; }
.nav-more-backdrop {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.45);
  z-index: 90;
}
.nav-more-panel {
  display: none;
  position: fixed;
  left: 50%; transform: translateX(-50%);
  bottom: var(--nav-h);
  width: 100%; max-width: 680px;
  flex-direction: column;
  background: var(--bg);
  border-top: 0.5px solid var(--border);
  padding: 6px 0;
  padding-bottom: env(safe-area-inset-bottom, 0);
  z-index: 95;
}
.nav-more-panel.open { display: flex; }
.nav-more-panel .nav-item {
  flex-direction: row;
  justify-content: flex-start;
  gap: 10px;
  padding: 12px 20px;
  font-size: 12px;
  width: 100%;
}
.nav-more-panel .nav-footer {
  display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 6px;
  padding: 10px 20px 6px;
}
.nav-more-panel .nav-footer a {
  font-family: 'DM Mono', monospace; font-size: 11px;
  letter-spacing: 0.06em; color: var(--text3);
  text-decoration: none; transition: color 0.15s;
}
.nav-more-panel .nav-footer a:hover { color: var(--text2); }
.nav-more-panel .nav-footer-sep { color: var(--border2); font-size: 10px; }

/* ── Desktop: left sidebar ──────────────────────────── */
@media (min-width: 640px) {
  /* Scoped to `:has(.app-shell)`, not bare `body`/`#root`: this fixed-viewport-
     with-internal-scroll trick belongs to the authenticated app's sidebar shell
     (`.page` does the actual scrolling), and used to apply globally regardless
     of which layout was mounted -- which silently broke scrolling on the public
     marketing pages (`.public-shell`) the day they stopped using `.app-shell`,
     since a plain document has no *other* scroll container once `body` is
     pinned shut. */
  body:has(.app-shell) { overflow: hidden; }

  /* #root is the column that owns the viewport, and the shell takes whatever
     is left of it. The shell used to be a hard `height: 100vh`, which made the
     document exactly one dev banner taller than the screen — and a document
     that can scroll at all will, the moment anything calls scrollIntoView,
     sliding the whole app up under the sticky banner. With the banner absent
     in production this is a no-op; with it present there is simply nothing to
     scroll. */
  #root:has(.app-shell) { display: flex; flex-direction: column; height: 100%; }

  .app-shell {
    display: grid;
    grid-template-columns: var(--nav-w) 1fr;
    grid-template-areas: "nav main";
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
  }

  .bottom-nav {
    grid-area: nav;
    /* reset mobile positioning */
    position: static;
    transform: none;
    left: auto;
    bottom: auto;
    /* sidebar layout */
    width: var(--nav-w);
    max-width: var(--nav-w);
    height: 100%;
    flex-direction: column;
    justify-content: flex-start;
    align-items: stretch;
    border-top: none;
    border-right: 0.5px solid var(--border);
    padding: 0;
    padding-bottom: 20px;
  }

  .nav-wordmark {
    display: block;
    text-decoration: none;
    font-family: 'DM Mono', monospace;
    font-size: 22px;
    font-weight: 300;
    color: var(--text3);
    letter-spacing: -0.02em;
    padding: 28px 20px 24px;
  }

  .nav-item {
    flex-direction: row;
    justify-content: flex-start;
    gap: 10px;
    padding: 9px 20px;
    font-size: 11px;
    width: 100%;
  }
  .nav-item:hover { color: var(--text2); }
  .nav-logout { margin-top: auto; }

  /* the mobile "more" menu becomes a no-op on desktop — its contents
     render inline in the sidebar like any other nav item */
  .nav-more-toggle { display: none; }
  .nav-more-backdrop { display: none; }
  .nav-more-panel,
  .nav-more-panel.open {
    display: contents;
  }

  .nav-footer {
    display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 6px;
    padding: 10px 20px 14px;
  }
  .nav-footer a {
    font-family: 'DM Mono', monospace; font-size: 10px;
    letter-spacing: 0.06em; color: var(--text3);
    text-decoration: none; transition: color 0.15s;
  }
  .nav-footer a:hover { color: var(--text2); }
  .nav-footer-sep { color: var(--border2); font-size: 10px; }

  .page {
    grid-area: main;
    max-width: none;
    margin: 0;
    width: 100%;
    /* own scroll context */
    height: 100%;
    overflow-y: auto;
    /* center a 720px content column using padding (not max-width + margin)
       so the native scrollbar sits flush against the true viewport edge
       instead of floating next to the centered content */
    padding-top: 28px;
    padding-bottom: 28px;
    padding-left: max(32px, calc(50% - 360px));
    padding-right: max(32px, calc(50% - 360px));
    min-height: unset;
  }
}

/* ── Memo Cards (Memos) ──────────────────────────────── */
.memo-card {
  background: var(--surface); border: 0.5px solid var(--border);
  border-radius: var(--r2); margin-bottom: 8px;
  transition: border-color 0.12s; cursor: pointer;
}
.memo-card:hover { border-color: var(--border2); }
/* Centered, not `flex-start`: the type badge is a pill (padding + border,
   ~17px) while the timestamp beside it is bare 10px text, so top-aligning
   the two hangs the time off the badge's top edge instead of reading as one
   line. MemosView happens to be immune -- it wraps its time and badges in
   their own `alignItems: center` flex row -- but HeroMemoStream puts them
   here as direct children, which is where the misalignment showed. */
.memo-head { display: flex; align-items: center; gap: 8px; padding: 10px 13px 0; }
.memo-time { font-size: 10px; font-family: 'DM Mono', monospace; color: var(--text3); }
.memo-summary { padding: 8px 13px 10px; font-size: 14px; color: var(--text2); line-height: 1.5; }
.memo-summary.unread { font-weight: 700; color: var(--text); }
.memo-response { padding: 0 13px 10px; font-size: 13px; color: var(--text3); line-height: 1.5; }
.memo-actions {
  border-top: 0.5px solid var(--border); background: var(--surface2);
  padding: 8px 13px; display: flex; gap: 8px;
  border-radius: 0 0 var(--r2) var(--r2);
}

/* ── Capture / Input Box ────────────────────────────── */
.wordmark {
  display: block; width: 100%;
  text-align: center; padding: 48px 0 0;
  font-family: 'DM Mono', monospace; font-size: 48px;
  color: var(--text3); letter-spacing: -0.02em; font-weight: 300;
}
.wordmark-sub {
  text-align: center; margin-top: 6px; margin-bottom: 28px;
  font-family: 'DM Mono', monospace; font-size: 10px;
  color: var(--text3); letter-spacing: 0.14em;
}
/* tone down the big wordmark on desktop — sidebar already has it */
@media (min-width: 640px) {
  .wordmark { font-size: 32px; padding-top: 20px; }
}

.input-box {
  background: var(--surface); border: 0.5px solid var(--border2);
  border-radius: var(--r2); padding: 14px 14px 52px; position: relative;
  transition: border-color 0.2s;
}
.input-box:focus-within { border-color: var(--border-focus); }
/* The preview stands in for the textarea, so it has to be typed like one:
   inheriting body's 15px/1.5 instead would drop each line ~0.75px and the
   text would visibly shift on toggle. Same rule, so the two cannot drift. */
.input-box textarea, .input-box .md-preview, .input-box input[type="text"], .input-box input[type="email"], .input-box input[type="password"], .input-box input[type="tel"], .input-box select {
  width: 100%; background: transparent; border: none; outline: none;
  color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 15px;
  line-height: 1.6; resize: none; min-height: 48px;
}
.input-box textarea::placeholder,
.input-box input::placeholder { color: var(--text3); }
.input-box select { cursor: pointer; }

/* ── Buttons ────────────────────────────────────────── */
.btn {
  height: 32px; border-radius: 20px; border: none; cursor: pointer;
  font-family: 'DM Mono', monospace; font-size: 11px; letter-spacing: 0.06em;
  padding: 0 14px; transition: all 0.15s; user-select: none;
  background: var(--surface3); color: var(--text2);
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  text-decoration: none;
}
.btn-primary { background: var(--text); color: var(--bg); }
.btn:disabled, .btn-primary:disabled { opacity: 0.35; cursor: not-allowed; }
.btn-text:disabled { opacity: 0.35; cursor: not-allowed; }
.btn-ghost { background: transparent; border: 0.5px solid var(--border2); color: var(--text3); }
.btn-ghost:hover { color: var(--text2); }
.kabob-btn { width: 28px; height: 24px; padding: 0; justify-content: center; font-size: 14px; }

/* Favorite toggle — a bare icon rather than a bordered button, so it reads as
   part of the memo card's header rather than a second kabob. -webkit-appearance
   matters here: without it Safari/iOS paints its own button chrome (a grey
   rounded box) straight over the transparent background. */
.star-btn {
  width: 24px; height: 24px; padding: 0; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  -webkit-appearance: none; appearance: none;
  background: transparent; border: none; box-shadow: none; cursor: pointer;
  color: var(--text3); -webkit-tap-highlight-color: transparent;
  transition: color 0.12s, transform 0.12s;
}
.star-btn:hover { color: var(--text2); }
.star-btn:active { transform: scale(0.88); }
.star-btn.active { color: var(--task); }
.star-btn.active:hover { color: var(--task); opacity: 0.75; }
.btn-row { display: flex; gap: 8px; margin-top: 12px; }

.btn-text {
  display: inline-flex; align-items: center; gap: 4px; line-height: 1;
  background: none; border: none; padding: 0; cursor: pointer;
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.05em;
  color: var(--text3); transition: color 0.12s;
}
.btn-text:hover { color: var(--text2); }
.btn-text-caret {
  flex-shrink: 0; transform-origin: center; transition: transform 0.15s;
}

.tier-btn { border: 0.5px solid transparent; }
.tier-btn.selected {
  background: rgba(var(--accent-rgb),0.16); color: var(--accent);
  border-color: var(--accent); font-weight: 600;
}

/* ── Recording state ────────────────────────────────── */
.btn.recording { background: rgba(var(--danger-rgb),0.15); color: var(--danger); border: 0.5px solid rgba(var(--danger-rgb),0.3); }
.rec-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.recording .rec-dot { animation: blink 0.9s ease-in-out infinite; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }

/* ── Consent checkbox ───────────────────────────────── */
.consent-checkbox {
  display: flex; align-items: flex-start; gap: 10px; cursor: pointer;
  font-size: 12px; color: var(--text3); line-height: 1.5; margin-top: 4px;
}
.consent-checkbox input[type="checkbox"] {
  width: 16px; height: 16px; margin: 1px 0 0; flex-shrink: 0;
  accent-color: var(--accent); cursor: pointer;
}

/* A switch, its explanation behind a '?', and its own save message under it.
   The '?' sits at the end of the sentence rather than over it, and is a
   disclosure rather than a tooltip: a tooltip is a hover, and half the people
   using this are on a phone. */
/* The label shrink-wraps its text so the '?' lands right after the last word
   rather than out at the card's right edge, where it read as a second control
   instead of as the end of that sentence. */
.setting-line { display: flex; align-items: flex-start; gap: 6px; }
.setting-line .consent-checkbox { flex: 0 1 auto; min-width: 0; }
.help-tip {
  /* 5px = the .consent-checkbox row's own 4px top margin plus the 1px on its
     input, so the ? sits on the same line as the box and the label. */
  flex-shrink: 0; width: 16px; height: 16px; margin-top: 5px; padding: 0;
  border-radius: 50%; border: 0.5px solid var(--border2);
  background: transparent; color: var(--text3);
  font-family: 'DM Mono', monospace; font-size: 10px; line-height: 1;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.help-tip:hover { color: var(--text); border-color: var(--text3); }
.help-tip.active { color: var(--accent); border-color: var(--accent); }
/* Indented to the checkbox's label, so it reads as belonging to that switch
   and not to the card. */
.setting-help { margin: 6px 0 0 26px; }
/* The switch's own save line, directly under it. .status-msg's vertical margin
   is sized for a card footer; here it only has to clear the label. */
.setting .status-msg { margin: 4px 0 0 26px; min-height: 0; }

/* ── Integrations list ──────────────────────────────── */
/* Two groups in one box: the connected services, which are a ranking you drag,
   and everything else, which is a menu you pick from. Same card, because they
   are one list — `position` is defined over all of it — and only the half that
   is actionable is worth separating out. */
.integration-group + .integration-group { margin-top: 16px; }
.integration-group-label {
  font-size: 10px; font-family: 'DM Mono', monospace; color: var(--text3);
  letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 8px;
}
.integration-list { display: flex; flex-direction: column; gap: 6px; }
.integration-row {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px;
  background: var(--surface2); border: 0.5px solid var(--border);
  border-radius: var(--r); cursor: grab;
}
.integration-row.drag-over { border-color: var(--accent); }
/* An unconnected integration has no ordering to change, so the grab cursor
   would be promising a drag that does nothing. The handle's width is held so
   the names still line up down the column. */
.integration-row-fixed { cursor: default; }
.integration-drag-handle {
  color: var(--text3); font-size: 14px; line-height: 1;
  min-width: 10px; display: inline-block;
}
/* Flex so the `?` lands right after the name's last word (see .help-tip) and
   not out at the row's far edge beside the button, where it read as a second
   control. */
.integration-name { font-size: 13px; color: var(--text); flex: 1; display: flex; align-items: center; gap: 6px; }
/* .help-tip's top margin is tuned to sit level with a checkbox; this row
   centres its items, so here it would push the ? below the name. */
.integration-row .help-tip { margin-top: 0; }
/* The revealed sentence, indented to the name (handle 10 + gap 10 + padding
   10) so it reads as belonging to that row and not to the card. */
.integration-help { margin: 0 0 2px 30px; }
.integration-status {
  font-size: 10px; font-family: 'DM Mono', monospace; letter-spacing: 0.06em;
  color: var(--text3); text-transform: uppercase;
}
.integration-status.connected { color: var(--accent); }

/* ── Toggle switch ──────────────────────────────────── */
.toggle-row { display: flex; align-items: center; gap: 10px; margin-top: 14px; }
.toggle-switch { position: relative; display: inline-block; width: 38px; height: 22px; flex-shrink: 0; }
.toggle-switch input {
  position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; z-index: 1;
}
.toggle-switch input:disabled { cursor: not-allowed; }
.toggle-track {
  position: absolute; inset: 0;
  background: var(--surface3); border-radius: 999px;
  transition: background 0.15s;
}
.toggle-thumb {
  position: absolute; top: 2px; left: 2px;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--text2); transition: transform 0.15s, background 0.15s;
}
.toggle-switch input:checked ~ .toggle-track { background: rgba(var(--accent-rgb),0.3); }
.toggle-switch input:checked ~ .toggle-track .toggle-thumb { background: var(--accent); transform: translateX(16px); }
.toggle-switch input:disabled ~ .toggle-track { opacity: 0.5; }

/* ── Modal confirm dialog ────────────────────────────
   A three-way "are you sure?" — save it / discard / stay — which is why it is
   a panel and not window.confirm(): a browser dialog has two buttons and the
   middle option is the whole point. Centred and fixed rather than appended to
   the end of the page, where the answer to a question raised by a tap on the
   nav bar landed below the fold on a phone.

   Above the bottom nav (z-index 100) and its backdrop, so the nav cannot be
   tapped through it; the panel's own bottom margin clears the nav and the home
   indicator on an iPhone, since the dialog is centred in the viewport and a
   short one would otherwise sit right on top of both. */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 300;
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
  padding-bottom: calc(var(--nav-h) + 20px + env(safe-area-inset-bottom, 0px));
  background: rgba(0,0,0,0.5);
}
@media (min-width: 640px) {
  /* The nav becomes a left sidebar at this width, so there is nothing at the
     bottom of the viewport to clear. Same breakpoint the sidebar uses. */
  .modal-backdrop { padding-bottom: 20px; }
}
.modal-panel {
  width: 100%; max-width: 380px;
  background: var(--surface); border: 0.5px solid var(--border);
  border-radius: var(--r2); padding: 18px 16px 14px;
  box-shadow: 0 12px 40px rgba(0,0,0,0.35);
  display: flex; flex-direction: column; gap: 14px;
}
/* Stacked, not a row. `.btn` is white-space: nowrap and these labels are
   sentences ("discard and start over"), so a row either overflows or wraps into
   ragged half-rows; a column is the same shape on a laptop and on a phone, and
   every option is a full-width tap target. Primary first, since the choice that
   keeps what is at stake should be the one under the thumb. */
.modal-choices { display: flex; flex-direction: column; gap: 8px; }
.modal-choices .btn { justify-content: center; height: 36px; }

/* ── Status / feedback messages ─────────────────────── */
.status-msg {
  font-size: 12px; font-family: 'DM Mono', monospace;
  color: var(--text3); margin: 10px 0; min-height: 1.4em;
}
.status-msg.error { color: var(--danger); }
.status-msg.success { color: var(--accent); }

/* ── Waveform canvas ────────────────────────────────── */
#waveform { display: block; width: 100%; height: 56px; border-radius: var(--r); background: var(--surface2); margin: 10px 0; }

/* ── Detail cards ───────────────────────────────────── */
.detail-card {
  background: var(--surface); border: 0.5px solid var(--border);
  border-radius: var(--r2); padding: 14px; margin-bottom: 10px;
}
.detail-label {
  font-size: 10px; font-family: 'DM Mono', monospace; color: var(--text3);
  letter-spacing: 0.08em; margin-bottom: 8px; text-transform: uppercase;
}
.detail-text { font-size: 14px; color: var(--text2); line-height: 1.6; overflow-wrap: anywhere; }
/* ── The learn page ─────────────────────────────────────────────────────
   Filters, then a conversation, then a composer. The charts that used to live
   here are gone: the memo list's own filters answer what they asked. */
/* No gap and no margin overrides: the spacing between the pills, the date
   range and the search box is the one the memo list already has, and it lives on
   `.filter-bar`'s own padding-bottom and `.date-range`'s own margin-bottom.
   Adding a flex gap on top of those made the same three controls sit further
   apart here than on the memo list, which is exactly the kind of drift a shared
   control exists to prevent. Only the trailing margin under the search box is
   cancelled -- on the memo list that is the room for the memo count beneath it, and
   here the bar simply ends. */
.memo-filter-bar { display: flex; flex-direction: column; }
/* The search box keeps its own bottom margin on the learn page, because the
   memo count sits under it there and that margin is exactly the room the memo list
   leaves for the same line. It is cancelled only inside a saved record's fold,
   where the bar really is the last thing in the box. Cancelling it everywhere
   is what put the count hard against the search box with all its air below. */
.scope-filters-body .memo-search { margin-bottom: 0; }

/* The conversation moved into AnswerChat's own card, which is the panel a
   question memo and a saved record already used; .insights-chat went with it.
   The empty-state heading above the question box went the same way: the filter
   bar says what is being read and the placeholder says what to do with it. */

/* A saved record's "read from" header is gone. What it was read from now sits
   in the memo's collapsed body, in the same `.scope-filters` fold the
   learn page uses -- one control, so a change to how it reads lands on the
   page a question was asked on and on the memo it became. */
/* The top of the insights card: where a memo's transcript goes, and before
   there is one, where the question is asked. No divider under it -- the card is
   one sentence read top to bottom (this question, of these memos, ask) and a
   rule across the middle of it cuts the sentence in half. */
.learn-question { margin-top: 0; }
.learn-question .inline-chat-input-area { margin-top: 0; }
/* No override on the composer: `send` is inside the box here, exactly as it is
   in AnswerChat's follow-up composer, so `.inline-chat-input-area`'s own 44px of
   bottom padding is the room it needs rather than dead space. The button spent a
   while outside the box, back when the filter fold sat between the two. */

/* The fold on a saved record: what the answer above it was read from, shut by
   default, with the count on the toggle so it is on screen either way. The
   learn page has no fold — its filter bar is at the top of the page, laid
   out exactly as the memo list's is, because there it is a live control rather than
   a record. */
.scope-filters { margin-top: 12px; }
/* Inside a memo's expanded body the 16px flex gap above it is already the
   separation; the fold's own top margin would double it. */
.detail-expanded .scope-filters { margin-top: 0; }
.scope-filters-toggle {
  display: flex; align-items: center; gap: 6px; width: 100%;
  background: none; border: none; padding: 4px 2px; cursor: pointer;
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.08em;
  color: var(--text3); text-transform: lowercase;
}
.scope-filters-toggle:hover { color: var(--text2); }
/* The stored count is a fact about the past, so it gets the same weight as a
   live one rather than more. */
.scope-filters-frozen .memo-count { margin-left: auto; padding: 0; }
/* An SVG caret, the same one the memo head's expand/collapse draws. The '▾' it
   replaced had to be shrunk to 8px to stop it towering over a 10px label, and
   at that size it read as a speck rather than as a control. */
.scope-filters-chev { flex-shrink: 0; transition: transform 0.12s; }
/* The count rides on the toggle's right edge; the class's own bottom padding
   is the memo list's, and here the toggle row provides the spacing. */
.scope-filters-toggle .memo-count { margin-left: auto; padding: 0; }
.scope-filters-body { padding: 10px 0 0; }

/* The second box on the learn page: a label and three buttons, and
   deliberately nothing else. The inset comes from `.detail-card`'s own 14px
   padding, so they line up with the label above them. A centred paragraph under
   them explaining what generate does read as a landing page dropped into a
   form, and was a card refusing to trust its own labels.

   Stacked and full width rather than sharing a row. Stacking is what makes them
   properly equal: three buttons in a row are the same width only until the
   words differ, and these say very different-length things. Full width also
   gives each one room for a whole phrase, which is what lets them be requests
   in the user's own voice ("hype me up") rather than nouns.

   Equal weight is the point — none of the three is the default, and an
   emphasised one with two quieter siblings would make the other two read as
   modes of it. Each carries its own hue instead, so what distinguishes them is
   *which* rather than *how important*. */
.learn-generate-row { display: flex; flex-direction: column; gap: 8px; }
.learn-generate-btn {
  width: 100%; height: 38px; border-radius: 20px; font-size: 12px;
  letter-spacing: 0.02em;
  /* Left-aligned, which `.btn`'s inline-flex already does. Three stacked
     buttons whose labels start at the same x read as one list of things you
     could ask for; centred, each label floats at its own offset and the column
     stops looking like a set. The `✦` landing in a column is what carries it. */
  transition: background 0.12s, border-color 0.12s;
}
.learn-generate-btn:disabled { opacity: 0.35; cursor: not-allowed; }

/* One rule per kind rather than an inline style, so the palette stays in the
   stylesheet where the rest of it lives. Keep the slugs in step with
   `memos/angles.py`'s KINDS — the class is built from the kind. */
.learn-kind-insight {
  color: var(--finding); background: var(--finding-bg);
  border: 0.5px solid var(--finding-border);
}
.learn-kind-insight:hover:not(:disabled) { background: var(--finding-border); }
.learn-kind-feedback {
  color: var(--feedback); background: var(--feedback-bg);
  border: 0.5px solid var(--feedback-border);
}
.learn-kind-feedback:hover:not(:disabled) { background: var(--feedback-border); }
.learn-kind-hype {
  color: var(--hype); background: var(--hype-bg);
  border: 0.5px solid var(--hype-border);
}
.learn-kind-hype:hover:not(:disabled) { background: var(--hype-border); }
/* Only ever on screen to say why the button is off, so it needs the room under
   it that the label above the button would otherwise have given. */
.learn-generate-hint { margin: 0 0 10px; }

/* The thumbs on a Void-written insight, in both places they render: the badge
   row in the memo head and the answer panel's own top-right corner.

   Unrated is greyscale and dimmed, rated is full colour on the insight tint --
   which is the whole state this control has to carry, since null here means
   *unrated* rather than neutral and must not read as a third setting. Emoji
   replaced a 10px ▲/▼ pair that read as sort arrows until you noticed one of
   them was coloured. */
.finding-rating { display: flex; align-items: center; gap: 2px; }
.finding-rating-btn {
  width: 26px; height: 22px; padding: 0; font-size: 13px; line-height: 1;
  justify-content: center; transition: background 0.12s, box-shadow 0.12s;
}
/* The chosen thumb is marked by the pill behind it, never by dimming the other
   one. A greyed-out, desaturated thumb reads as *disabled* rather than as *not
   chosen*, which is the one thing this control must not say about a rating you
   can still give. Inset shadow rather than a border, so nothing shifts by half
   a pixel when it lights up. */
.finding-rating-btn.active {
  background: var(--finding-bg); box-shadow: inset 0 0 0 0.5px var(--finding-border);
}
/* The answer panel's corner: whatever the caller put there, then the × if there
   is one. Nothing passes both today, but they share a row so that whichever
   exists lands in the same place. */
.answer-head-right { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.help-text { font-size: 12px; color: var(--text3); line-height: 1.5; margin-bottom: 10px; }
.detail-title { font-size: 18px; font-weight: 500; color: var(--text); margin-bottom: 4px; overflow-wrap: anywhere; }
.detail-title-input {
  width: 100%; background: var(--surface2); border: 0.5px solid var(--border2);
  border-radius: var(--r); padding: 8px 10px; color: var(--text);
  font-family: 'DM Sans', sans-serif; font-size: 15px; outline: none;
}
.detail-title-input:focus { border-color: var(--border-focus); }
.detail-title-input:disabled { opacity: 0.5; }

.settings-input {
  width: 100%; background: var(--surface2); border: 0.5px solid var(--border2);
  border-radius: var(--r); padding: 8px 10px; margin-bottom: 10px; color: var(--text);
  font-family: 'DM Sans', sans-serif; font-size: 14px; outline: none;
  /* explicit (non-transparent) background/color so the native option-list
     popup themes correctly too — a truly transparent <select> falls back to
     the browser's default popup styling regardless of color-scheme. The
     actual light/dark switch comes from :root's inherited color-scheme. */
}
.settings-input:focus { border-color: var(--border-focus); }
.settings-input::placeholder { color: var(--text3); }
select.settings-input { cursor: pointer; }
.detail-card-head { display: flex; flex-direction: column; gap: 8px; }
/* Card header with a control pinned opposite the label (e.g. the suggested
   action's delete ×). The label keeps its own margin reset inline. */
.detail-card-title-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 8px;
}
.detail-head-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

.verify-nudge {
  background: var(--surface); border: 0.5px solid var(--border2);
  border-radius: var(--r2); padding: 12px 14px; margin: 10px 0;
  display: flex; flex-direction: column; gap: 10px;
}
.verify-nudge-msg {
  font-size: 13px; color: var(--text2); line-height: 1.5;
}

.a2hs-steps {
  list-style: decimal; padding-left: 20px;
  display: flex; flex-direction: column; gap: 6px;
  font-size: 13px; color: var(--text2); line-height: 1.5;
}
.a2hs-steps strong { color: var(--text); font-weight: 600; }

.spinner {
  display: inline-block; width: 12px; height: 12px; flex-shrink: 0;
  border: 1.5px solid var(--border2); border-top-color: var(--text2);
  border-radius: 50%; animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── Empty state ────────────────────────────────────── */
.empty {
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; padding: 60px 0; gap: 8px;
  color: var(--text3); font-family: 'DM Mono', monospace;
  font-size: 11px; letter-spacing: 0.06em;
}
.empty-glyph { font-size: 28px; opacity: 0.3; }

/* ── Audio player ───────────────────────────────────── */
audio { width: 100%; margin-top: 10px; }

/* ── Type badges ─────────────────────────────────────── */
.type-badge {
  font-size: 10px; font-family: 'DM Mono', monospace;
  padding: 2px 7px; border-radius: 8px;
  cursor: pointer; transition: opacity 0.1s;
  font-weight: 500; letter-spacing: 0.03em;
  flex-shrink: 0; user-select: none;
}
.type-pending  { color: var(--text3); background: var(--surface2); border: 0.5px solid var(--border); }
.type-task     { color: var(--task);     background: var(--task-bg);     border: 0.5px solid var(--task-border); }
.type-note     { color: var(--note);     background: var(--note-bg);     border: 0.5px solid var(--note-border); }
.type-question { color: var(--question); background: var(--question-bg); border: 0.5px solid var(--question-border); }
.type-finding  { color: var(--finding);  background: var(--finding-bg);  border: 0.5px solid var(--finding-border); }
/* The second badge on a saved record: which of the three the Void wrote, or
   `context` on a kept conversation. Neutral on purpose, and the same neutral
   `audio` already uses — one hue per memo says what it *is*, and this qualifies
   that rather than competing with it. Two saturated pills at 10px would make
   the colour stop reliably meaning the type. */
.type-kind     { color: var(--text3); background: var(--surface2); border: 0.5px solid var(--border); }
.type-failed   { color: var(--danger); background: rgba(var(--danger-rgb),0.1); border: 0.5px solid rgba(var(--danger-rgb),0.25); }
.type-audio    { color: var(--text3); background: var(--surface2); border: 0.5px solid var(--border); }
.type-loading  { opacity: 0.5; cursor: wait; }

.token-badge {
  display: inline-block; font-size: 10px; font-family: 'DM Mono', monospace;
  padding: 2px 7px; border-radius: 8px; font-weight: 500; letter-spacing: 0.03em;
  color: var(--accent); background: var(--surface2); border: 0.5px solid var(--border);
  margin-top: 8px;
}

/* ── Reclassify picker ───────────────────────────────── */
.memo-head { position: relative; }
.rc-picker {
  position: absolute; top: calc(100% + 4px); left: 60px; z-index: 200;
  background: var(--surface2); border: 0.5px solid var(--border2);
  border-radius: var(--r); padding: 4px; min-width: 110px;
  white-space: nowrap;
  box-shadow: 0 8px 24px rgba(0,0,0,0.5);
}
.rc-opt {
  padding: 7px 10px; border-radius: 6px; cursor: pointer;
  font-size: 12px; font-family: 'DM Mono', monospace;
  display: flex; align-items: center; gap: 7px;
  color: var(--text2); transition: background 0.1s;
}
.rc-opt:hover { background: var(--surface3); }
.rc-opt-danger { color: var(--danger); }
.rc-opt-danger:hover { background: rgba(var(--danger-rgb),0.1); }
.rc-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }

/* ── Tags ────────────────────────────────────────────── */
/* The tag row sits under the date/type line in the detail head, with
   expand/collapse on its right -- so that control stays in the bottom-right
   corner of the head whether the memo has no tags or a full five.
   `min-height` keeps the corner from jumping as chips come and go. */
.detail-head-tags { margin-top: 6px; min-height: 22px; }
.tag-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
/* .memo-card has no padding of its own -- every child brings the card's 13px
   gutter with it (.memo-head, .memo-summary, .memo-response, .memo-actions).
   The tag row is a child like any other, so it carries the same gutter and the
   same 10px bottom as .memo-response, which is what keeps chips off the card's
   left edge and off its bottom when they are the last thing in it. No
   margin-top: .memo-summary's own 10px bottom padding is already the gap. */
.memo-card .tag-row { padding: 0 13px 10px; }
.tag-chip {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: 10px; font-family: 'DM Mono', monospace;
  padding: 2px 7px; border-radius: 8px;
  font-weight: 500; letter-spacing: 0.03em;
  flex-shrink: 0; user-select: none;
  color: var(--text3); background: var(--surface2);
  border: 0.5px solid var(--border);
}
.tag-chip-x {
  background: none; border: none; padding: 0; margin-left: 1px;
  color: var(--text3); cursor: pointer; font-size: 12px; line-height: 1;
  opacity: 0.5; transition: opacity 0.1s;
}
.tag-chip-x:hover { opacity: 1; color: var(--danger); }
/* The '+' is the entire tag UI on a memo that has none, so it stays quiet
   until pointed at rather than advertising itself on every memo -- but quiet
   via colour, not opacity. Opacity dimmed the glyph, the chip's background and
   its border as one group, compositing the '+' to ~1.9:1 against the card in
   dark mode; colouring only the glyph holds 5.7:1 dark / 6.6:1 light while the
   chip stays as recessed as any other. The larger glyph rides on a line-height
   pinned to the 15px the 10px chips already compute, so the button stays
   exactly as tall as the chips beside it. */
.tag-chip-add {
  cursor: pointer; padding: 2px 8px;
  font-size: 13px; line-height: 15px;
  color: var(--text2); transition: color 0.1s, border-color 0.1s;
}
.tag-chip-add:hover { color: var(--text); border-color: var(--border2); }
/* Wraps the '+' as well as the input — see TagChips.tsx. inline-flex rather
   than inline-block so it sizes to the button and adds no baseline slack now
   that it, not the button, is the flex item in .tag-row. */
.tag-add { position: relative; display: inline-flex; align-items: center; }
.tag-input {
  font-size: 10px; font-family: 'DM Mono', monospace;
  padding: 2px 7px; border-radius: 8px; width: 90px;
  color: var(--text); background: var(--surface3);
  border: 1px solid var(--border2); outline: none;
}
/* The only input in the app that had neither of these. Without them a focused
   tag input is surface2 on surface — a ~3% lightness step behind a 0.5px
   border — with no focus ring and a browser-default placeholder, which is
   near enough to invisible that opening it reads as nothing happening. */
.tag-input:focus { border-color: var(--border-focus); }
.tag-input::placeholder { color: var(--text3); }
.tag-picker { left: 0; min-width: 120px; }
.tag-opt-uses { margin-left: auto; opacity: 0.4; padding-left: 10px; }
.rc-dot-task     { background: var(--task); }
.rc-dot-note     { background: var(--note); }
.rc-dot-question { background: var(--question); }

/* Separates the second switch in a card that holds more than one — the
   automation card's two. Enough air to read as its own setting, not enough to
   read as its own card. */
.settings-subsetting { margin-top: 18px; }

/* ── Filter pills ────────────────────────────────────── */
.filter-bar {
  display: flex; gap: 6px; padding: 0 2px 14px;
  overflow-x: auto; scrollbar-width: none;
}
.filter-bar::-webkit-scrollbar { display: none; }
.fpill {
  padding: 5px 12px; border-radius: 20px; font-size: 11px;
  font-family: 'DM Mono', monospace; letter-spacing: 0.04em;
  cursor: pointer; white-space: nowrap; flex-shrink: 0;
  border: 0.5px solid var(--border); color: var(--text3);
  background: transparent; transition: all 0.12s;
}
.fpill.active { border-color: var(--border2); color: var(--text); background: var(--surface); }
.fpill:hover:not(.active) { color: var(--text2); }
/* A frozen filter bar — a saved record's record of what it read. Greyed and
   inert, and `default` rather than `not-allowed`: nothing is being refused
   here, there is simply nothing to press. */
.fpill:disabled { opacity: 0.55; cursor: default; }
.fpill:disabled:hover:not(.active) { color: var(--text3); }

.memo-search {
  width: 100%; height: 32px; border-radius: var(--r); border: 0.5px solid var(--border);
  background: var(--surface2); color: var(--text); padding: 0 10px;
  font-family: 'DM Mono', monospace; font-size: 12px; outline: none; margin-bottom: 10px;
  -webkit-appearance: none;  /* drop Safari's rounded search chrome */
}
.memo-search:focus { border-color: var(--border2); }
.memo-search::placeholder { color: var(--text3); }

/* ── Date range ───────────────────────────────────────
   The from/to pair, shared by the memo list, the learn page and the export.
   Sits above the search box in the first two: the range is the coarser cut. */
.date-range { display: flex; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
.date-range-field {
  display: flex; align-items: center; gap: 6px; flex: 1 1 140px; min-width: 0;
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.06em;
  color: var(--text3); text-transform: lowercase;
}
.date-range-input {
  flex: 1; min-width: 0; height: 32px; border-radius: var(--r);
  border: 0.5px solid var(--border); background: var(--surface2); color: var(--text);
  padding: 0 8px; font-family: 'DM Mono', monospace; font-size: 12px; outline: none;
  -webkit-appearance: none;  /* drop iOS's native date-field chrome */
}
.date-range-input:focus { border-color: var(--border2); }
.date-range-input:disabled { opacity: 0.5; }
/* Safari/Chrome's picker glyph is black by default, which vanishes in dark. */
.date-range-input::-webkit-calendar-picker-indicator {
  filter: invert(0.5); cursor: pointer; opacity: 0.7;
}

.memo-count {
  font-size: 10px; font-family: 'DM Mono', monospace; letter-spacing: 0.06em;
  color: var(--text3); padding: 0 2px 10px;
}
/* The memo list's count with the sort toggle on its right. */
.memo-count-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }

/* Back on the left, previous/next on the right, above a memo. */
.memo-nav-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 16px; }
.memo-nav { display: flex; gap: 6px; }

.memo-sentinel { display: flex; justify-content: center; padding: 12px 0 24px; }

/* ── Page furniture ──────────────────────────
   `.page-scroll` and the cards under it were `.insights-*`, from when the
   learn tab was a wall of charts. The charts are gone -- the memo list
   answers what they asked -- so what is left is named for what still uses
   it: a scrolling page, and the admin panel's sections and stat tiles. */
.page-scroll { padding-bottom: 24px; }

/* The plan card, as the settings page's `premium` link addresses it. Smooth,
   because the jump is within a page the reader is already looking at and a
   hard cut there reads as a navigation that did not happen; and with a little
   margin above, so the card lands under the top edge rather than flush against
   it. Scoped to this one target rather than set globally: nothing else in the
   app scrolls itself, and a global smooth scroll would change every jump
   nobody asked to change. */
#plan { scroll-margin-top: 16px; scroll-behavior: smooth; }
html { scroll-behavior: smooth; }

.admin-card {
  background: var(--surface); border: 0.5px solid var(--border);
  border-radius: var(--r2); padding: 14px 14px 12px; margin-bottom: 10px;
}
.admin-card-label {
  font-size: 10px; font-family: 'DM Mono', monospace; letter-spacing: 0.08em;
  color: var(--text3); margin-bottom: 14px;
}

.stat-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px;
}
.stat-card {
  background: var(--surface); border: 0.5px solid var(--border);
  border-radius: var(--r2); padding: 12px 14px;
}
.stat-value { font-size: 26px; font-family: 'DM Mono', monospace; font-weight: 300; color: var(--text); line-height: 1; margin-bottom: 5px; }
.stat-label { font-size: 10px; font-family: 'DM Mono', monospace; color: var(--text3); letter-spacing: 0.06em; }

/* ── Task workflow ────────────────────────────────────── */
:root {
  --todo:            var(--text3);
  --in-progress:     var(--task);
  --in-progress-bg:  var(--task-bg);
  --in-progress-border: var(--task-border);
  --done:            #7ae8a8;
  --done-rgb:        122,232,168;
  --done-bg:         rgba(122,232,168,0.08);
  --done-border:     rgba(122,232,168,0.2);
}

.task-status-row {
  display: flex; gap: 6px; margin-bottom: 14px;
}
.task-status-btn {
  flex: 1; padding: 7px 4px; border-radius: var(--r);
  font-size: 11px; font-family: 'DM Mono', monospace; letter-spacing: 0.03em;
  cursor: pointer; border: 0.5px solid var(--border); color: var(--text3);
  background: transparent; transition: all 0.12s;
}
.task-status-btn:disabled { opacity: 0.5; cursor: wait; }
.task-status-btn:hover:not(:disabled):not(.active) { border-color: var(--border2); color: var(--text2); }

.task-status-btn.task-status-todo.active    { color: var(--text2); background: var(--surface2); border-color: var(--border2); }
.task-status-btn.task-status-in_progress.active { color: var(--task); background: var(--task-bg); border-color: var(--task-border); }
.task-status-btn.task-status-done.active    { color: var(--done); background: var(--done-bg); border-color: var(--done-border); }

.task-due-row {
  display: flex; align-items: center; gap: 10px;
}
.task-due-label {
  font-size: 10px; font-family: 'DM Mono', monospace; color: var(--text3);
  letter-spacing: 0.06em; white-space: nowrap; flex-shrink: 0;
}
.task-due-inputs { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.task-due-input {
  background: var(--surface2); border: 0.5px solid var(--border2);
  border-radius: var(--r); padding: 6px 10px; color: var(--text);
  font-family: 'DM Mono', monospace; font-size: 12px; outline: none;
}
.task-due-input:disabled { opacity: 0.5; }

/* ── Transcript edit ─────────────────────────────────── */
.transcript-edit-area {
  width: 100%; background: var(--surface2); border: 0.5px solid var(--border2);
  border-radius: var(--r); padding: 10px 12px; color: var(--text);
  font-family: 'DM Sans', sans-serif; font-size: 14px; line-height: 1.6;
  resize: vertical; min-height: 80px; outline: none;
}
.transcript-edit-area:focus { border-color: var(--border-focus); }
.transcript-edit-area:disabled { opacity: 0.5; }

/* ── Inline chat ─────────────────────────────────────── */
.inline-chat-history {
  display: flex; flex-direction: column; gap: 12px;
  max-height: 360px; overflow-y: auto; scrollbar-width: none;
  margin-bottom: 12px;
}
.inline-chat-history::-webkit-scrollbar { display: none; }
.inline-chat-input-area {
  position: relative; margin-top: 12px;
  background: var(--surface2); border: 0.5px solid var(--border2);
  border-radius: var(--r2); padding: 12px 12px 44px;
  transition: border-color 0.2s;
}
.inline-chat-input-area:focus-within { border-color: var(--border-focus); }
/* Who said what is shown by shape: the user's turns sit in a bubble, the
   assistant's are plain body text running the full width. Both used to be
   bubbles under a 9px `you` / `ai` caption, which framed the app's own output as
   one side of a chat with a robot -- and an answer here is the memo's content,
   the thing the digest quotes and a saved record *is*, not a message from
   somebody. Plain text is what it deserves. */
.chat-msg { display: flex; flex-direction: column; }
.chat-msg-user { align-items: flex-end; }
.chat-msg-assistant { align-items: flex-start; }
/* Kept in the DOM and taken off the screen. A screen reader has no bubble to go
   on, so the speaker still has to be said somewhere. */
.chat-msg-label {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.chat-msg-text { font-size: 14px; line-height: 1.6; color: var(--text2); }
.chat-msg-user .chat-msg-text {
  background: var(--surface3); color: var(--text);
  border-radius: var(--r2); padding: 9px 12px; max-width: 85%;
}
.chat-msg-assistant .chat-msg-text { width: 100%; max-width: 100%; }
.chat-thinking {
  display: flex; align-items: center; gap: 7px;
  font-size: 12px; font-family: 'DM Mono', monospace; color: var(--text3);
}
/* markdown inside a turn -- a bubble on the user's side, plain text on the
   assistant's */
.chat-msg-md p { margin: 0 0 0.5em; }
.chat-msg-md p:last-child { margin-bottom: 0; }
.chat-msg-md ul, .chat-msg-md ol { margin: 0.4em 0 0.4em 1.2em; padding: 0; }
.chat-msg-md li { margin-bottom: 0.2em; }
.chat-msg-md h1, .chat-msg-md h2, .chat-msg-md h3 {
  font-size: 13px; font-weight: 600; color: var(--text); margin: 0.6em 0 0.3em;
}
.chat-msg-md h1:first-child, .chat-msg-md h2:first-child, .chat-msg-md h3:first-child { margin-top: 0; }
.chat-msg-md code {
  font-family: 'DM Mono', monospace; font-size: 12px;
  background: var(--surface3); border-radius: 4px; padding: 1px 5px;
}
.chat-msg-md pre {
  background: var(--surface3); border-radius: var(--r); padding: 10px 12px;
  overflow-x: auto; margin: 0.5em 0; scrollbar-width: none;
}
.chat-msg-md pre::-webkit-scrollbar { display: none; }
.chat-msg-md pre code { background: none; padding: 0; font-size: 12px; }
.chat-msg-md blockquote {
  border-left: 2px solid var(--border2); margin: 0.4em 0; padding-left: 10px;
  color: var(--text3);
}
.chat-msg-md a { color: var(--accent); }
.chat-msg-md strong { color: var(--text); font-weight: 600; }
.chat-msg-md hr { border: none; border-top: 0.5px solid var(--border); margin: 0.6em 0; }
/* KaTeX override — inherit our text color */
.chat-msg-md .katex { color: var(--text); font-size: 1em; }

.chat-input {
  width: 100%; background: transparent; border: none; outline: none;
  color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 14px;
  line-height: 1.6; resize: none; min-height: 24px; max-height: 120px;
}
.chat-input:disabled { opacity: 0.5; }
.chat-input::placeholder { color: var(--text3); }
.chat-send {
  position: absolute; bottom: 8px; right: 8px;
  height: 28px; border-radius: 20px; padding: 0 12px;
  font-size: 10px;
}
.chat-send:disabled { opacity: 0.35; cursor: not-allowed; }
.chat-limit-notice {
  margin-top: 12px; padding: 10px 12px; border-radius: var(--r);
  background: var(--surface2); border: 0.5px solid var(--border);
  font-size: 12px; font-family: 'DM Mono', monospace; color: var(--text3);
  line-height: 1.5; letter-spacing: 0.02em;
}
.chat-char-count {
  margin-top: 5px; text-align: right;
  font-size: 10px; font-family: 'DM Mono', monospace; color: var(--text3);
}


/* ── Landing page ───────────────────────────────────────────────
   One width for the whole page, top to bottom -- the flow chart used to be
   the only section allowed past a narrow 480px hero column, which read as
   two different pages stitched together. With no sidebar competing for
   width (see PublicLayout), there's no longer a reason for that: `.landing`
   is a single wide column throughout, and it's the individual *text*
   elements (`.landing-blurb`) that keep their own narrower reading
   measure, centered within it -- not the section around them. */
.landing { width: 100%; max-width: 1400px; text-align: center; }
.landing .wordmark { color: var(--text); padding-top: 12px; }
@media (min-width: 640px) {
  .landing .wordmark { color: var(--text); font-size: 48px; padding-top: 12px; }
}

/* A fixed-height window onto a live stream of real memo cards, rather than a
   framed screenshot to read -- full-height (`.feature-shot`'s treatment)
   took up most of the first scroll and asked to be looked at like
   documentation, and a still picture of an app that's supposed to feel alive
   read as inert either way. `HeroMemoStream` renders the same card markup
   MemosView uses and drops a new one in every couple of seconds; the fixed
   height plus `overflow: hidden` here is what keeps that from growing the
   page -- older cards get pushed down and clipped rather than the box
   stretching to fit them. The bottom fade is decoration only -- it
   dissolves the clipped edge into the page background instead of a hard
   cut. `hero.png` (see
   capture-screenshots.mjs) still exists for anywhere a static image of this
   is wanted, but the landing page itself no longer uses it. */
.landing-hero {
  position: relative; margin: 0 0 40px; height: 310px;
  border-radius: var(--r2); overflow: hidden;
}
.landing-hero::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 70%, var(--bg) 100%);
  pointer-events: none;
}
@media (min-width: 640px) {
  .landing-hero { height: 560px; }
}

/* The stream itself, centered at the app's own content width (`.page`'s
   680px) rather than stretched to the hero box's full width -- these are
   the real cards at the real size they render at everywhere else, not an
   image scaled to fill. */
.hero-stream {
  position: absolute; inset: 0;
  max-width: 680px; margin: 0 auto;
  padding: 14px 16px 0;
}
/* Each card mounts collapsed and grows open -- `grid-template-rows: 0fr ->
   1fr` animates to an unknown height (a fixed max-height guess would either
   clip a long summary or leave a gap under a short one), and it's what
   makes the cards already below it look like they're being pushed down:
   they don't move, the space above them grows into place. `.entered` is
   added a tick after mount so there's a `0fr` frame to transition away from. */
.hero-card-wrap {
  display: grid; grid-template-rows: 0fr; opacity: 0;
  transition: grid-template-rows 480ms cubic-bezier(0.16, 1, 0.3, 1), opacity 420ms ease;
}
.hero-card-wrap.entered { grid-template-rows: 1fr; opacity: 1; }
.hero-card-wrap-inner { overflow: hidden; min-height: 0; }
/* `text-align: left` because `.landing` centers the whole page and the card
   is the one thing inside it that shouldn't inherit that. Only `.memo-summary`
   was actually affected -- the head and the tag row are both flex rows, so
   they were already packed left -- which left a card with left-aligned badges,
   centered text, and left-aligned tags. These are supposed to read as the real
   feed's cards, and there the summary is left-aligned. */
.hero-card-wrap .memo-card {
  margin-bottom: 8px; cursor: default; text-align: left;
}
.hero-card-wrap .memo-card:hover { border-color: var(--border); }

/* Kept in the DOM and taken off the screen -- the stream is `aria-hidden`
   decoration (a scripted loop, not real content to read out card by card),
   so this is the one sentence a screen reader gets instead. */
.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;
}

/* The gap under the blurb is this margin *plus* `.hero-stream`'s own 14px of
   top padding, so the visual distance to the first memo card is 14px more
   than the number here. It used to be 44 (58 all in), tuned back when this
   block also carried the two taglines and had the CTA row above it; with a
   single paragraph left that read as a hole. 28 matches `.landing-section`'s
   rhythm, which is what the rest of the page below it steps to. */
.landing-consolidated { max-width: 640px; margin: 0 auto 28px; text-align: center; }
.landing-blurb {
  color: var(--text2); font-size: 14px; line-height: 1.7;
}
/* Same centered, capped-width treatment as the hero blurb in
   `.landing-consolidated` -- both headings are centered now
   (`.landing-heading`), so the paragraph under this one should read the
   same way, not inherit `.landing-section`'s left alignment (which is
   still right for the flow chart sitting in the section above it). */
.landing-section .landing-blurb { max-width: 640px; margin: 0 auto; text-align: center; }
.landing-privacy-link {
  text-align: center; font-size: 12px; font-family: 'DM Mono', monospace;
  color: var(--text3); margin-top: 8px;
}

.landing-section { margin: 28px 0; text-align: left; }
/* Centered regardless of `.landing-section`'s own left alignment -- a
   heading reads as a section title, not as the paragraph under it, and
   sits over both the flow chart and the pricing card that follow it. */
.landing-heading {
  text-align: center; margin: 0 0 6px;
  font-family: 'DM Mono', monospace; font-size: 24px; font-weight: 300;
  color: var(--text); letter-spacing: -0.01em;
}
.landing-heading-note {
  text-align: center; margin: 0 0 22px;
  font-family: 'DM Mono', monospace; font-size: 12px; color: var(--text3);
}
.landing-section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-bottom: 10px;
}
.landing-section-head .detail-label { margin-bottom: 0; }
.landing-section-link {
  font-family: 'DM Mono', monospace; font-size: 11px; letter-spacing: 0.04em;
  color: var(--text3); white-space: nowrap;
}
.landing-section-link:hover { color: var(--text2); }

/* ── Public site shell (PublicLayout): top header + footer, deliberately
   sharing nothing with `.app-shell`/`.bottom-nav`/`.page` -- see the
   component doc comment for why the authenticated sidebar doesn't fit a
   marketing page. */
.public-shell { display: flex; flex-direction: column; min-height: 100vh; }

.public-header {
  display: flex; align-items: center; gap: 20px;
  padding: 16px 20px; border-bottom: 0.5px solid var(--border);
  position: sticky; top: 0; background: var(--bg); z-index: 50;
}
.public-logo {
  font-family: 'DM Mono', monospace; font-size: 18px; font-weight: 300;
  color: var(--text); text-decoration: none; letter-spacing: -0.02em;
}
.public-nav-links { display: none; }
.public-header-cta { display: flex; gap: 8px; margin-left: auto; }
.public-header-cta .btn { height: 32px; padding: 0 14px; font-size: 12px; }
.public-menu-toggle {
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0;
  background: none; border: none; color: var(--text2); cursor: pointer;
}
.public-menu-panel {
  display: flex; flex-direction: column;
  border-bottom: 0.5px solid var(--border); background: var(--bg);
}
.public-nav-link {
  padding: 12px 20px; font-family: 'DM Mono', monospace; font-size: 12px;
  letter-spacing: 0.04em; color: var(--text2); text-decoration: none;
  border-top: 0.5px solid var(--border);
}
.public-menu-panel .public-nav-link:first-child { border-top: none; }
.public-nav-link.active { color: var(--text); }

.public-main {
  flex: 1 1 auto; width: 100%;
  display: flex; flex-direction: column; align-items: center;
  padding: 32px 20px 48px;
}

.public-site-footer {
  border-top: 0.5px solid var(--border); padding: 20px;
  display: flex; flex-wrap: wrap; justify-content: center; gap: 8px;
}
.public-site-footer a {
  font-family: 'DM Mono', monospace; font-size: 11px; letter-spacing: 0.04em;
  color: var(--text3); text-decoration: none; transition: color 0.15s;
}
.public-site-footer a:hover { color: var(--text2); }
.public-site-footer-sep { color: var(--border2); font-size: 10px; }

@media (min-width: 720px) {
  .public-header { padding: 18px 32px; }
  .public-nav-links { display: flex; align-items: center; gap: 4px; }
  .public-nav-link { border-top: none; padding: 6px 12px; border-radius: var(--r); }
  .public-nav-link:hover { color: var(--text); background: var(--surface2); }
  .public-header-cta .btn { height: 34px; padding: 0 16px; }
  .public-menu-toggle, .public-menu-panel { display: none; }
  .public-main { padding: 48px 32px 64px; }
}

/* ── Shared footer (PublicFooter, all public marketing pages) ─ */
.public-footer {
  margin-top: 36px; padding-top: 20px; border-top: 0.5px solid var(--border);
  text-align: center;
}
.public-footer-tagline {
  font-family: 'DM Mono', monospace; font-size: 12px; color: var(--text3);
  margin-bottom: 12px;
}
.public-footer-cta { justify-content: center; }

/* ── Card gallery (features & uses hubs, embedded on homepage) ─ */
.card-gallery {
  display: grid; grid-template-columns: 1fr; gap: 10px; margin: 4px 0 10px;
}
@media (min-width: 560px) {
  .card-gallery { grid-template-columns: 1fr 1fr; }
}
.feature-card {
  display: block; padding: 14px; border-radius: var(--r);
  background: var(--surface); border: 0.5px solid var(--border);
  color: var(--text); transition: border-color 0.15s;
}
.feature-card:hover { border-color: var(--border2); }
.feature-card-title {
  font-family: 'DM Mono', monospace; font-size: 12px; letter-spacing: 0.03em;
  color: var(--text); margin-bottom: 6px;
}
.feature-card-summary { font-size: 13px; color: var(--text2); line-height: 1.6; }
.feature-card-soon { opacity: 0.7; }
.feature-card-tag {
  margin-left: 6px; font-family: 'DM Mono', monospace; font-size: 9px;
  letter-spacing: 0.04em; color: var(--text3); font-weight: 400;
}

/* ── Feature screenshots (feature detail pages) ────────────────
   The app's own UI is dark, same as this page, so a screenshot dropped in
   with no framing reads as more page rather than as a picture of
   something -- the padded, contrast-toned mat plus its own border and
   shadow is what makes the boundary unmistakable regardless of how dark
   or light the screenshot itself happens to be. Every screenshot is dark
   now, the digest email included (its own mail-client mockup renders it
   the way a dark-mode client actually would), but the mat doesn't assume
   that -- a future light one still reads as a picture, not a hole in
   the page. */
.feature-shot {
  margin-top: 16px; padding: 14px;
  background: var(--surface2); border: 0.5px solid var(--border2);
  border-radius: var(--r2); box-shadow: 0 10px 28px rgba(0, 0, 0, 0.28);
}
.feature-shot img {
  display: block; width: 100%; height: auto;
  border-radius: calc(var(--r2) - 5px); border: 0.5px solid var(--border2);
}

/* ── Flow chart (frontend/src/components/FlowChart.tsx) ───────
   Two independent layouts, not one reflowed: mobile is a rail (one
   continuous line, stages as stops on it); desktop is a React Flow graph --
   edges, arrowheads, and centering/fitting the whole thing are all React
   Flow's job now, on a fixed hand-placed layout, rather than hand-rolled SVG
   bezier math trying to hit computed target points. */
.flow-node {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 14px; border-radius: var(--r);
  background: var(--surface2); border: 0.5px solid var(--border2);
  font-size: 12px; color: var(--text2); text-align: left;
  flex: 0 1 200px;
}
.flow-node svg { flex: none; color: var(--text3); }
/* Pushes the tap chevron to the right edge; the label block is the only
   thing in the box allowed to take the slack. */
.flow-node-body { flex: 1; min-width: 0; }
.flow-node-soon { opacity: 0.75; }
.flow-node-soon .flow-node-tag {
  display: block; font-family: 'DM Mono', monospace; font-size: 9px;
  letter-spacing: 0.04em; color: var(--text3); margin-top: 2px;
}
a.flow-node-link {
  text-decoration: none; cursor: pointer;
  transition: border-color 0.12s, background 0.12s;
}
a.flow-node-link:hover {
  border-color: var(--accent); background: var(--surface3);
}
/* The header over each column of the flow chart ("capture", "forget",
   "review & act"). */
.flow-group-caption {
  text-align: center; font-family: 'DM Mono', monospace; font-size: 11px;
  color: var(--text2); margin-bottom: 6px;
}

/* Mobile: a rail (no React Flow -- see FlowChart.tsx). One unbroken vertical
   line down the left, the three stages as stops on it, full-width boxes
   hanging off to its right, one per line.
   The line replaces the arrow-per-join the stack used to have: three columns
   separated by two 24px glyphs read as three unrelated lists, because the
   only thing carrying the flow was the weakest mark on the screen. Direction
   comes from vertical reading order, so the rail needs no arrowheads.
   Two shapes tried and rejected on the way here, neither worth re-trying:
   boxes wrapping two-per-line into little grids, and a spine with a branch
   tick per box, which read as stray borders -- the branching added noise,
   not the fan-in the desktop edges draw. The rail is that spine with the
   ticks taken off. */
.flow-diagram-mobile {
  display: flex; flex-direction: column; align-items: stretch; gap: 22px;
  /* Clears the rail. Every offset below is in the padded content box's
     coordinates, i.e. 34px to the right of the rail's own. */
  position: relative; padding-left: 34px;
}
/* The rail. Starts at roughly the first stop's centre rather than at the
   wrapper's top edge, so it reads as running *through* the stops instead of
   past the first one; runs to the bottom of the last box. */
.flow-diagram-mobile::before {
  content: ''; position: absolute; left: 12px; top: 6px; bottom: 0;
  width: 1px; background: var(--border2);
}
.flow-mobile-stage {
  display: flex; flex-direction: column; gap: 8px; position: relative;
}
/* Left-aligned against the rail, not centred like the desktop column
   headers -- a stop's label belongs beside its stop. */
.flow-diagram-mobile .flow-group-caption { margin: 0; text-align: left; }
/* The stops -- one per stage caption, all three identical. `left: -26px`
   puts an 8px dot's centre on the rail (34 - 26 + 4 = 12); the box-sizing
   reset at the top of this file is what makes the border part of that 8, so
   the border width doesn't shift it. */
.flow-mobile-caption { position: relative; }
.flow-mobile-caption::before {
  content: ''; position: absolute; left: -26px; top: 50%; margin-top: -4px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--surface2); border: 1px solid var(--text3);
}
.flow-mobile-col { display: flex; flex-direction: column; gap: 8px; }
/* `.flow-node`'s `flex: 0 1 200px` was sized for a wrapping row. In this
   column container that basis would apply to the *height*, so it is reset,
   and the box takes the full width instead. */
.flow-mobile-col .flow-node { flex: 0 0 auto; width: 100%; }
/* A sub-header inside the column (a captioned group in engageGroups; none
   today) gets a little air above so the groups read as two. */
.flow-mobile-col .flow-mobile-subcaption { margin-top: 10px; }

/* Desktop: the React Flow canvas. Fixed height, since React Flow needs a
   real measured container to fit/center its content into -- `fitView`
   then does the "not too tall, actually centered" work our own bezier
   math kept getting wrong, by scaling the whole graph to fit whatever
   height we give it here. */
.flow-canvas {
  /* Close to the diagram's own aspect ratio so `fitView`'s padding lands
     even on all sides -- taller than that leaves dead space above and
     below once the width-bound fit stops scaling any further. Also sets
     the effective zoom `fitView` lands on: a bigger canvas for the same
     underlying node layout means less shrinking, i.e. bigger boxes.
     The layout in FlowChart.tsx is roughly 1280x794 (3:2-ish) now that the
     engage column carries six boxes; at the old 620px this fit to about
     1000px wide and left ~150px dead on each side of a full-width section,
     with the boxes shrunk to match. Re-measure this if either outer column
     gains or loses a box. Tightening the engage groups (see TIGHT in
     FlowChart.tsx) shortened the layout again, to roughly 1280x600. */
  height: 640px; border-radius: var(--r2); overflow: hidden;
  /* EqualiseColumns re-stacks the columns from their measured heights one
     frame after mount, so the first paint is a placeholder with the wrong
     spacing. Held back until it has re-fit rather than letting the boxes
     jump into place. */
  opacity: 0; transition: opacity 160ms ease;
}
.flow-canvas.ready { opacity: 1; }
.flow-canvas .react-flow__node {
  /* Our own `.flow-node`/`.flow-group-caption` already carry the visual
     styling; strip React Flow's default node chrome so it isn't doubled.
     (Width comes from each node's own inline `style.width` in
     FlowChart.tsx, which wins over any width set here regardless.) */
  background: none; border: none; padding: 0; border-radius: 0;
  font: inherit; text-align: inherit; box-shadow: none;
  /* React Flow inline-sets `pointer-events: none` on any node once it's
     neither draggable, connectable, nor selectable (all three are off for
     this static diagram) -- it assumes a fully inert node has nothing
     inside worth clicking, which isn't true here: the box is a `<Link>`.
     Only `!important` beats an inline style, and `pointer-events` is an
     inherited property, so this one override reaches the link inside
     without a second rule. */
  pointer-events: auto !important;
}
.flow-canvas .react-flow__handle {
  /* Real anchor points for our fixed edges, not user-facing connection
     dots -- this is a static diagram (`nodesConnectable={false}`). */
  opacity: 0; pointer-events: none; width: 1px; height: 1px; border: none;
}
.flow-canvas .react-flow__attribution { display: none; }
.flow-canvas .react-flow__edge-path { stroke-linecap: round; }
/* Bigger than the mobile stack's boxes -- scoped here rather than changed
   on `.flow-node` itself, since mobile's sizing is unrelated and already
   right. */
.flow-canvas .flow-node { padding: 16px 22px; font-size: 19px; gap: 12px; }
.flow-canvas .flow-node svg { width: 26px; height: 26px; }
/* The tap affordance is a phone thing: here a box says it's a link by
   lighting up under the cursor, and a chevron on top of that would be the
   same claim made twice. Rendered and hidden rather than conditionally
   rendered, so `FlowNode` stays one component across both layouts. */
.flow-canvas .flow-node-chevron { display: none; }
.flow-canvas .flow-group-caption { font-size: 15px; }

/* ── Privacy page ───────────────────────────────────── */
/* `width: 100%` is load-bearing, not decoration. `.public-main` is a flex
   column with `align-items: center`, and a centred flex child shrink-wraps
   to its content unless given a width -- so with only the max-width, each
   page under this class came out as wide as whatever happened to be in it,
   capped at 760. Pages carrying a paragraph or two hit the cap and looked
   right; the integrations page, once its intro paragraph went, stopped
   short of it and rendered narrower than uses and pricing beside it. Filling
   to the cap makes every page the same column by construction. */
.privacy { width: 100%; max-width: 760px; }
.privacy .detail-card p {
  color: var(--text2); font-size: 14px; line-height: 1.7;
}
.privacy .detail-card p + p { margin-top: 10px; }
.privacy .detail-card ul {
  color: var(--text2); font-size: 14px; line-height: 1.7;
  margin: 10px 0; padding-left: 0; list-style: none;
}
.privacy .detail-card ul li {
  position: relative; padding-left: 16px; margin-bottom: 8px;
}
.privacy .detail-card ul li:last-child { margin-bottom: 0; }
.privacy .detail-card ul li::before {
  content: '–'; position: absolute; left: 0; color: var(--text3);
}
.privacy-back {
  display: inline-flex; align-items: center; gap: 4px;
  font-family: 'DM Mono', monospace; font-size: 12px; letter-spacing: 0.04em;
  color: var(--text3); background: none; border: none; cursor: pointer;
  padding: 4px 0; margin-top: 12px;
}
.privacy-back:hover { color: var(--text2); }
.privacy-back svg { display: block; }
/* The compliance section is deliberately visually separate from the cards
   above it -- those are the actual voice of the page, this is boilerplate
   we're required to host per-integration and don't want diluting it. */
.privacy-compliance-head {
  font-family: 'DM Mono', monospace; font-size: 10px; color: var(--text3);
  letter-spacing: 0.08em; text-transform: uppercase; text-align: center;
  border-top: 0.5px solid var(--border); padding-top: 20px; margin: 24px 0 10px;
}
.privacy-compliance-sub {
  font-size: 10px; font-family: 'DM Mono', monospace; color: var(--text3);
  letter-spacing: 0.08em; margin-bottom: 8px; text-transform: uppercase;
}

/* ── Admin dashboard ────────────────────────────────── */
.admin-search {
  width: 100%; height: 32px; border-radius: var(--r); border: 0.5px solid var(--border2);
  background: var(--surface2); color: var(--text); padding: 0 10px;
  font-family: 'DM Mono', monospace; font-size: 12px; outline: none; margin-bottom: 10px;
}
.admin-search::placeholder { color: var(--text3); }

.admin-table-wrap { overflow-x: auto; }
.admin-table { width: 100%; border-collapse: collapse; font-size: 12px; white-space: nowrap; }
.admin-table th {
  text-align: left; font-family: 'DM Mono', monospace; font-size: 10px;
  letter-spacing: 0.06em; color: var(--text3); text-transform: uppercase;
  padding: 6px 10px; border-bottom: 0.5px solid var(--border2);
}
.admin-table td {
  padding: 8px 10px; border-bottom: 0.5px solid var(--border); color: var(--text2);
}
.admin-table tr:last-child td { border-bottom: none; }
.admin-detail-cell { white-space: normal; color: var(--text3); max-width: 260px; }

.admin-badge {
  display: inline-block; font-family: 'DM Mono', monospace; font-size: 10px;
  padding: 2px 7px; border-radius: 8px; letter-spacing: 0.03em; margin-left: 6px;
}
.admin-badge-ok { background: rgba(var(--done-rgb),0.1); color: var(--done); }
.admin-badge-fail { background: rgba(var(--danger-rgb),0.1); color: var(--danger); }
.admin-badge-staff { background: rgba(var(--accent-rgb),0.12); color: var(--accent); }

/* The audit table's `kind` column. Deliberately the flattest badge of the set:
   connect / disconnect / statement is a categorisation, not a verdict, and the
   ok/fail hues above would read as one. */
.admin-badge-kind { background: var(--surface2); color: var(--text2); }

/* A statement as it was actually run. Monospace because it is code, wrapping
   because a query that scrolls off the right edge of the row is the half nobody
   reads, and capped because one pathological migration should not push the rest
   of the table off the screen. .admin-table sets nowrap, hence the override. */
.admin-audit-message {
  white-space: pre-wrap;
  word-break: break-word;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  color: var(--text2);
  max-width: 480px;
  max-height: 7.5em;
  overflow-y: auto;
}

/* Under the audit table: what the view is reading and what it is not. It says
   out loud that these files are still editable by anyone with root. The privacy
   policy used to say the same and no longer describes the log beyond one
   sentence, so this note is now the only place that states it. */
.admin-audit-note {
  margin-top: 10px;
  font-size: 11px;
  color: var(--text3);
  line-height: 1.5;
}

.admin-grant-row { display: flex; gap: 6px; align-items: center; }
.admin-grant-input {
  width: 72px; height: 28px; border-radius: var(--r); border: 0.5px solid var(--border2);
  background: var(--surface2); color: var(--text); padding: 0 8px;
  font-family: 'DM Mono', monospace; font-size: 12px; outline: none;
}

.admin-section-toggle {
  display: flex; align-items: center; gap: 6px; width: 100%; padding: 0;
}
.admin-section-label { margin-bottom: 0; }
.admin-section-body { margin-top: 14px; }

.admin-pager {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  margin-top: 10px;
}
.admin-pager-label { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--text3); }

.admin-canary-meta { margin-bottom: 10px; }
.admin-canary-textarea { min-height: 100px; margin-bottom: 10px; }

@media (min-width: 640px) {
  /* .landing sits inside PublicLayout's .page grid area now, same as
     .privacy — but keeps its own narrower 480px cap, so it needs the same
     re-centering .privacy gets implicitly from not setting max-width itself. */
  .landing {
    margin: 0 auto;
  }
}

/* ── Feedback ─────────────────────────────────────────── */
.feedback-options { margin-top: 16px; }
.feedback-category { display: flex; gap: 6px; flex-wrap: wrap; }
.feedback-anon {
  display: flex; align-items: center; gap: 8px; margin-top: 14px;
  font-size: 12px; color: var(--text2); cursor: pointer;
}
.feedback-anon input { accent-color: var(--accent); cursor: pointer; }
/* `normal`, not `pre-wrap`: the body renders as markdown now, and remark-breaks
   already turns its newlines into <br> — pre-wrap on top of that double-spaces
   every line. Still an explicit override, since .admin-table sets nowrap. */
.admin-feedback-body { max-width: 380px; white-space: normal; word-break: break-word; }
.admin-feedback-anon { color: var(--text3); font-style: italic; }

/* ── Markdown editor + memo body rendering ──────────── */
.md-editor { display: flex; flex-direction: column; }
.md-toolbar {
  display: flex; align-items: center; gap: 2px;
  margin-bottom: 8px; min-height: 24px;
}
.md-tool {
  height: 24px; min-width: 26px; padding: 0 6px;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; border-radius: 6px; cursor: pointer;
  font-family: 'DM Mono', monospace; font-size: 11px; line-height: 1;
  color: var(--text3); transition: background 0.15s, color 0.15s;
}
.md-tool:hover:not(:disabled) { background: var(--surface3); color: var(--text); }
.md-tool:disabled { opacity: 0.4; cursor: default; }
.md-tool-bold { font-weight: 700; }
.md-tool-italic { font-style: italic; }
/* Pushed to the far end: it acts on the whole editor, not on a selection. */
.md-tool-preview { margin-left: auto; letter-spacing: 0.06em; }
.md-tool-preview.active { background: var(--surface3); color: var(--text); }
/* Stands in for the textarea, so it inherits that call site's own chrome. The
   height it holds is measured off the textarea it replaced and set inline:
   `rows` and the call site's font metrics are what size that box, and neither
   is knowable from here. This floor applies only if that measurement came
   back empty (no layout yet). */
.md-preview { min-height: 48px; cursor: default; }

/* Memo bodies. Same shapes as .chat-msg-md, sized for body text rather than
   for the tighter chat bubbles. */
.memo-md > :first-child { margin-top: 0; }
.memo-md > :last-child { margin-bottom: 0; }
.memo-md p { margin: 0 0 0.6em; }
.memo-md ul, .memo-md ol { margin: 0.4em 0 0.6em 1.3em; padding: 0; }
.memo-md li { margin-bottom: 0.25em; }
.memo-md h1, .memo-md h2, .memo-md h3, .memo-md h4, .memo-md h5, .memo-md h6 {
  font-size: 15px; font-weight: 600; color: var(--text); margin: 0.8em 0 0.35em;
}
.memo-md code {
  font-family: 'DM Mono', monospace; font-size: 12.5px;
  background: var(--surface3); border-radius: 4px; padding: 1px 5px;
}
.memo-md pre {
  background: var(--surface3); border-radius: var(--r); padding: 10px 12px;
  overflow-x: auto; margin: 0.5em 0; scrollbar-width: none;
}
.memo-md pre::-webkit-scrollbar { display: none; }
.memo-md pre code { background: none; padding: 0; }
.memo-md blockquote {
  border-left: 2px solid var(--border2); margin: 0.5em 0; padding-left: 12px;
  color: var(--text3);
}
.memo-md a { color: var(--accent); text-decoration: underline; }
.memo-md strong { color: var(--text); font-weight: 600; }
.memo-md hr { border: none; border-top: 0.5px solid var(--border); margin: 0.8em 0; }
.memo-md table { border-collapse: collapse; margin: 0.5em 0; font-size: 13px; }
.memo-md th, .memo-md td { border: 0.5px solid var(--border); padding: 5px 9px; text-align: left; }
.memo-md .katex { color: var(--text); font-size: 1em; }

/* ── The plan card, at the top of Settings ──────────────────────────────────
   Was its own page and its own nav item. A whole destination for one word and
   one button, that a free user had no reason to open and a premium user had
   none to open twice. */
.plan-name { font-size: 1.35rem; font-weight: 600; }
.plan-note { margin-top: 10px; margin-bottom: 0; }
.plan-actions { margin-top: 14px; flex-wrap: wrap; }

/* The table scrolls inside its own box rather than widening the card: thirteen
   rows of prose in the first column will not fit a phone otherwise, and a
   settings page that scrolls sideways is worse than a table that does. */
/* Small, because the header row brings 8px of its own cell padding on top of
   this and the two were reading as one large gap. Constant across both states:
   this wrapper is what holds the offer whether or not the table is open. */
.plan-table-wrap { overflow-x: auto; margin-top: 4px; }
/* Heavier than .help-text and lighter than .plan-name: it is the offer, not a
   second heading competing with the plan you are on. Sits directly on the
   table, which brings 8px of its own header padding — so the visible gap is
   about ten pixels, not the twenty two margins would have made. */
.plan-upsell {
  margin: 12px 0 0; font-size: 13px; color: var(--text2);
}
/* Premium that is running out. The task amber rather than --danger: nothing has
   gone wrong, and a red banner over "your trial ends in three weeks" reads as a
   failure the user has to fix rather than as the heads-up it is. Sized and
   bordered like a badge, not a toast, because it is a standing property of the
   account rather than something that just happened. */
.plan-badge {
  display: inline-block; margin: 8px 0 0; padding: 4px 9px;
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.04em;
  border-radius: var(--r); border: 0.5px solid transparent;
}
/* Running out. The task amber rather than --danger: nothing has gone wrong, and
   red over "your trial ends in three weeks" reads as a failure to fix. */
.plan-badge-expiring {
  color: var(--task); background: var(--task-bg); border-color: var(--task-border);
}
/* Renewing. Same badge, same slot, opposite news. */
.plan-badge-renewing {
  color: var(--finding); background: var(--finding-bg); border-color: var(--finding-border);
}
/* The disclosure lives inside that sentence, underlined in the accent so the
   two words read as the thing to press.

   `text-decoration` rather than a `border-bottom`: a border sits at the bottom
   of the button's *box*, which carries the whole inherited line-height, so the
   line floats several pixels clear of the letters. An underline is drawn from
   the text's own baseline, and the offset then places it exactly. It also
   breaks correctly if the phrase ever wraps. */
.plan-upsell-trigger {
  background: none; border: none; padding: 0; cursor: pointer;
  font: inherit; color: var(--accent);
  text-decoration: underline dotted;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.plan-upsell-trigger:hover { color: var(--text); }
.plan-upsell-trigger:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Full width. Narrowing it was an attempt to shorten the run between a label
   and its mark, and it bought a readable row at the cost of a table that
   stopped two thirds of the way across a card and looked truncated. The banded
   premium column below solves the same problem better: it gives the eye a rail
   to follow all the way across, so the distance stops mattering. */
.plan-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.plan-table th, .plan-table td { padding: 8px 10px; text-align: left; }
/* The first column keeps its 10px, so the whole table sits slightly right of
   the sentence introducing it and reads as a block nested under that line
   rather than as a continuation of it. (It was briefly flush, back when the
   offer *was* the header cell and the two had to share an edge.) */
/* The rule under the header is structural, not decoration. A comparison table's
   top-left cell is empty by convention, so without a line the header is two
   small words floating in the upper right with nothing holding the left half of
   the row — which is most of why the top of this card looked unfinished. The
   border spans the empty cell too, which is what closes it. */
.plan-table thead th {
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.08em;
  text-transform: lowercase; color: var(--text3); font-weight: 400;
  padding-bottom: 7px; border-bottom: 1px solid var(--border);
}
.plan-table thead th:not(:first-child),
.plan-table td { text-align: center; width: 76px; }
.plan-table tbody th {
  font-weight: 400; color: var(--text2); text-align: left;
  /* The row labels are the content; let them take the rest. */
  width: auto;
}
.plan-table tbody tr + tr th,
.plan-table tbody tr + tr td { border-top: 1px solid var(--border); }
/* The band starts at the header, so its top corners round there rather than on
   the first body row. */
.plan-table thead th.plan-col-premium { border-bottom-color: var(--border2); }

/* The premium column is banded, which is the fix for the two things wrong with
   an unshaded one: the eye loses the row on the way across, and an empty cell
   in the free column reads as missing data rather than as "not included" —
   absence needs something present beside it to be absence *from*. It also puts
   the visual weight on the column the table exists to argue for. */
.plan-col-premium { background: rgba(var(--accent-rgb), 0.05); }
.plan-table thead th.plan-col-premium {
  color: var(--text2);
  border-radius: 4px 4px 0 0;
}
.plan-table tbody tr:last-child .plan-col-premium { border-radius: 0 0 4px 4px; }
/* Traceable one row at a time, for the rows the band alone does not carry. */
.plan-table tbody tr:hover th,
.plan-table tbody tr:hover td { background: rgba(var(--accent-rgb), 0.04); }
.plan-table tbody tr:hover .plan-col-premium {
  background: rgba(var(--accent-rgb), 0.09);
}

.plan-yes { color: var(--finding); font-size: 14px; line-height: 1; }
/* A word where a tick would say less — "30 days" against "1 year" carries the
   whole retention row, which a check in both columns could not. */
.plan-value {
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.06em;
  color: var(--text2); text-transform: lowercase;
}

/* Two lines: the price, and the number that justifies it. Equal widths so the
   pair reads as a choice between two things rather than as a button and an
   afterthought.
   `height: auto` is the load-bearing line — .btn sets a fixed 32px, which two
   lines of text do not fit inside, so the second one was being pushed against
   the bottom edge however much padding was added under it. */
.plan-price {
  display: inline-flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2px;
  height: auto; min-width: 124px;
  padding: 9px 16px; line-height: 1.25;
}
.plan-price-sub { font-size: 9px; opacity: 0.6; letter-spacing: 0.04em; }

/* The admin panel's four-state control. A select rather than a row of buttons:
   the states are mutually exclusive and one of them is always current, which is
   what a select says and four buttons do not. */
.admin-plan-select {
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.06em;
  padding: 3px 6px; border-radius: 4px;
  background: var(--card); color: var(--text2);
  border: 0.5px solid var(--border);
}
.admin-plan-select:disabled { opacity: 0.5; }

/* ── The learn page a free account gets ──────────────────────────────────
   Deliberately not the real page dimmed. Nothing here is operable, so there is
   no disabled state to style — just the same two cards saying what they would
   do, and one way out. */
/* The example questions carry most of the explaining, so they get the weight of
   quoted speech rather than of a bulleted feature list: no markers, and the
   italic that says these are words you would type. */

/* The three kinds, on the locked page. Shares the shape of the example list
   above it and not the italics: those are questions someone might ask, quoted,
   and these are the three things on offer. The name of each carries the insight
   green, which is the colour its button will be once the account can press it. */

/* The learn page's allowance line: what a free account is being given, and what
   is left of it. Under the filter bar and above both cards, because that is
   what it qualifies -- the filters say which memos, this says how many more
   times you may ask about them. Gone entirely on premium.

   A line of `.help-text` rather than a card of its own, and no colour at all.
   It was boxed and tinted first, which made a one-sentence note look like a
   third card between two real ones; this is the treatment the same sentence
   already gets under `ask again` on a saved record. Not green, which is the
   finding colour and sits on the badge of everything this page makes and on the
   three buttons below it. Not amber either, which this app keeps for something
   needing attention: running out of free tries is not a fault, and colouring it
   like one turns an offer into a telling-off. */
.learn-allowance strong { font-weight: 500; color: var(--text2); }

/* On the learn page the sentence is a banner: it is the first thing on the page
   and it governs everything under it, where on the other two screens it is a
   footnote to one control. Neutral, and no colour at all -- not the finding
   green, which is on the badge of everything this page makes and on the three
   buttons below, and not amber either, which this app keeps for something
   needing attention. Running out of free tries is not a fault, and colouring it
   like one turns an offer into a telling-off. */
.learn-allowance {
  padding: 9px 12px; margin-bottom: 12px; border-radius: var(--r);
  background: var(--surface2); border: 0.5px solid var(--border);
}
.learn-allowance .premium-note { margin: 0; }

/* The same boxed treatment as .learn-allowance, for the same reason: a free
   account's question memo, said up front rather than left for the action menu
   and a 402 to explain. Its own class rather than a share, since the two boxes
   differ in exactly the properties `.learn-allowance strong` above exists to
   set and this one has no use for. */
.answer-upsell {
  padding: 9px 12px; margin-bottom: 12px; border-radius: var(--r);
  background: var(--surface2); border: 0.5px solid var(--border);
}
.answer-upsell .premium-note { margin: 0; }

/* The one sentence every screen uses to say a thing is premium: the learn page,
   the foot of a saved finding, the settings toggles that are stored but not
   honoured. No margin of its own -- each caller sits it where that screen
   needs it -- and no colour, because it is a fact about the account rather
   than a warning about anything. */
.premium-note { margin: 8px 0 0; }
/* Flush left, not indented to the label like `.setting-help` and the save line
   beside it. Those two are continuations of the sentence the label starts, so
   hanging them off it is right; this one is a fact about the account that
   happens to be printed under a switch, and indenting it left a ragged edge
   down a card whose every other line starts at the same x. More space above it
   than below, since below is the next switch's own 4px and a note nearer the
   row under it than the row it describes reads as a heading for the wrong
   one. */

/* The integrations cap notice reuses the plan card's amber badge, since it is
   the same register — a standing fact about the account, not an error — but
   needs its own spacing in a card that opens with a paragraph. */
.integration-limit { margin: 0 0 12px; }
