:root {
  color-scheme: light;
  --font-sans: 'IBM Plex Sans', ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, "SFMono-Regular", Consolas, "Courier New", monospace;

  --ink: #101826;
  --muted: #5b6b82;
  --line: #d8dfe9;
  --surface: #ffffff;
  --soft: #eef1f7;

  /* the sidebar is a fixed dark instrument bezel in BOTH themes -- a
     deliberate "equipment chassis stays dark regardless of ambient light"
     choice, not a light/dark miss (unlike the badge/flash colors below,
     which genuinely need to track the theme and now do). */
  --nav: #0b1220;
  --nav-line: rgba(255, 255, 255, .1);
  --nav-ink: #e8edf6;
  --nav-muted: #93a1bd;
  --nav-soft: rgba(255, 255, 255, .08);
  --nav-soft-hover: rgba(255, 255, 255, .15);

  /* role colors -- each one means exactly one thing, everywhere it
     appears, and every value below is tuned to hold real contrast
     against --surface in its own theme. No gradients, no purple. */
  --accent: #2455c9;   /* info / neutral action -- links, secondary actions */
  --accent2: #173f9e;  /* info, hover/emphasis shade */
  --live: #12793f;     /* live / confirmed / good -- the "* live" signal */
  --pending: #9a5b00;  /* pending / needs attention */
  --danger: #c62a1f;   /* error / destructive only, never decorative */

  /* "fill" variants -- for the few components that paint a SOLID block of
     role color with fixed white text/icon on top (primary/danger/confirm
     buttons, the checked checkbox, the chat avatar, the brand mark, text
     selection). --accent/--live/--danger above intentionally brighten in
     dark mode so they stay legible as TEXT on --surface -- reused as a
     fill with white text, that same brightening drops contrast against
     white to ~2:1 in dark mode. These stay constant across both themes
     (already >5:1 against white) so a filled control never goes dark-mode
     illegible. Same hue/role as their non-fill counterpart, just tuned
     for the opposite pairing. */
  --accent-fill: #2455c9;
  --accent-fill-hover: #1c46ab;
  --live-fill: #12793f;
  --danger-fill: #c62a1f;

  --radius-xs: 4px;
  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-lg: 10px;

  /* one spacing scale, used app-wide for padding/margin/gap on chrome and
     layout (not for the sub-4px micro-nudges inside compact chips/badges,
     which have their own documented, internally-consistent values right
     next to where they're used). 4px base at the default 16px root, so it
     composes cleanly whether a rule is written in px or rem. */
  --space-1: .25rem;  /* 4px  -- tightest: icon-to-label, dot-to-text */
  --space-2: .5rem;   /* 8px  -- tight grouping within one control/row */
  --space-3: .75rem;  /* 12px -- default control padding, row gaps */
  --space-4: 1rem;    /* 16px -- default block spacing */
  --space-5: 1.25rem; /* 20px */
  --space-6: 1.5rem;  /* 24px -- panel padding, page-header rhythm */
  --space-7: 2rem;    /* 32px -- section separation */
  --space-8: 2.5rem;  /* 40px -- page-level padding */
  --space-9: 3rem;    /* 48px */
  --space-10: 4rem;   /* 64px -- page bottom padding */

  --shadow: 0 1px 2px rgba(16, 24, 38, .06), 0 1px 1px rgba(16, 24, 38, .04);
  --shadow-raised: 0 4px 14px rgba(16, 24, 38, .1), 0 1px 2px rgba(16, 24, 38, .06);
}
/* dark mode -- follows the OS preference by default; the sidebar toggle
   (see base.html) stamps data-theme on <html> to override it either way,
   persisted in localStorage. Every role/status color below now tracks the
   theme (badges, flashes, confirm/danger buttons included) -- there is no
   longer a hardcoded-pastel exception. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --ink: #e7ecf5;
    --muted: #94a3bd;
    --line: #262f42;
    --surface: #141b28;
    --soft: #0b1119;

    --accent: #7db4ff;
    --accent2: #a9cbff;
    --live: #3ddc84;
    --pending: #f4b64d;
    --danger: #ff6f64;

    --shadow: 0 1px 2px rgba(0, 0, 0, .5), 0 1px 1px rgba(0, 0, 0, .3);
    --shadow-raised: 0 6px 18px rgba(0, 0, 0, .55), 0 1px 2px rgba(0, 0, 0, .4);
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --ink: #e7ecf5;
  --muted: #94a3bd;
  --line: #262f42;
  --surface: #141b28;
  --soft: #0b1119;

  --accent: #7db4ff;
  --accent2: #a9cbff;
  --live: #3ddc84;
  --pending: #f4b64d;
  --danger: #ff6f64;

  --shadow: 0 1px 2px rgba(0, 0, 0, .5), 0 1px 1px rgba(0, 0, 0, .3);
  --shadow-raised: 0 6px 18px rgba(0, 0, 0, .55), 0 1px 2px rgba(0, 0, 0, .4);
}
* { box-sizing: border-box; }
/* the browser's own UA default for [hidden] (display:none) is an author
   stylesheet away from being overridden by ANY class rule that sets its
   own `display` unconditionally (e.g. .bucket-start-popover { display:
   flex }) -- author CSS always wins over the UA stylesheet regardless of
   selector specificity, so a bare `hidden` attribute was silently losing
   to those rules and leaving popovers permanently open. This one rule
   re-asserts hidden for everything, past and future. */
[hidden] { display: none !important; }
body {
  margin: 0;
  font-family: var(--font-sans);
  color: var(--ink);
  background: var(--soft);
  line-height: 1.55;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  /* app-wide text-overflow hardening: a long unbroken token anywhere in
     user-supplied content (a session/bucket/list name, a pasted URL, an
     email address, a transcript line) can otherwise force a panel or flex
     row wider than its container instead of wrapping. This is inherited by
     everything unless a more specific rule says otherwise (none do), so it
     covers every template in one place rather than repeating it per
     component. It also lets flex/grid children shrink correctly without
     separate min-width:0 rules, since a breakable string's min-content
     width collapses to a single character instead of the whole token. */
  overflow-wrap: anywhere;
}

/* browser surfaces carry the design too -- selection, focus rings, and
   scrollbars all ship with unauthored browser defaults otherwise. */
::selection { background: var(--accent-fill); color: #fff; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-xs); }
* { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; border: 2px solid var(--soft); }
::-webkit-scrollbar-thumb:hover { background: var(--muted); }

/* the one authored motion idea (recording/live/pending dots breathing) --
   respected everywhere it's used, turned off for anyone who asked. */
@media (prefers-reduced-motion: reduce) {
  .record-toggle-btn.recording, .badge-open::before,
  .badge-reminder::before, .console-status.live::before, .chat-dots span {
    animation: none !important;
  }
}

/* shell */
/* 264px, not 240 -- at 240 a real bucket name plus its visibility
   checkbox and count badge had nowhere left to go but mid-word ellipsis
   ("Zack's b...", "Knox S...", "865Da..."), on every row that wasn't
   already short. 24px back is enough room for an actual name to read
   before truncating, without giving up the rail's "narrow instrument
   column" proportions the way a wide sidebar would. */
.app-shell { min-height: 100vh; display: grid; grid-template-columns: 264px 1fr; }
.sidebar {
  position: sticky; top: 0; height: 100vh;
  padding-top: var(--space-6); padding-bottom: var(--space-6);
  padding-left: max(var(--space-5), env(safe-area-inset-left));
  padding-right: var(--space-5);
  display: flex; flex-direction: column;
  background: var(--nav); color: var(--nav-ink);
  border-right: 1px solid var(--nav-line);
}
/* The sidebar is a fixed 100vh column, and nothing was allowed to
   scroll inside it -- so on a short window everything past the fold was
   simply clipped. Measured at 700px tall: content ran to 1116px, putting
   the whole footer (account, theme, Log out) 416px out of reach with no
   way to get to it but resizing the window.

   The nav takes the slack and scrolls; the brand above and the footer
   below stay put, so the account and Log out are always reachable no
   matter how many buckets are in the tree.

   min-height:0 is the load-bearing part, not overflow-y. A flex item
   defaults to min-height:auto -- "never shrink below my content" -- so
   without it the nav just grows to its full height again and pushes the
   footer back off the bottom, scrollbar or not. Same trap as the grid
   item that let the bucket board push the page sideways.

   Descendant selectors, not child ones: .mobile-nav-content is
   display:contents, so nav is a flex child of the sidebar for LAYOUT but
   still a DOM child of that wrapper -- and the child combinator matches
   the DOM tree, not the layout tree. `.sidebar > nav` silently matches
   nothing. */
.sidebar nav { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.sidebar .sidebar-footer { flex-shrink: 0; }
/* Thin and dim: this scrollbar sits against the dark nav and shouldn't
   read as a second border down the middle of the app. */
.sidebar nav { scrollbar-width: thin; scrollbar-color: var(--nav-soft-hover) transparent; }
.sidebar nav::-webkit-scrollbar { width: 6px; }
.sidebar nav::-webkit-scrollbar-thumb { background: var(--nav-soft-hover); border-radius: 999px; }
.sidebar nav::-webkit-scrollbar-track { background: transparent; }

.brand { display: flex; align-items: center; gap: var(--space-2); color: var(--nav-ink); text-decoration: none; font-size: 1.22rem; font-weight: 700; letter-spacing: -.01em; }
.brand-mark {
  width: 34px; height: 34px; border-radius: var(--radius-sm); display: inline-grid; place-items: center;
  background: var(--accent-fill); color: #fff; font-weight: 700; border: 1px solid rgba(255, 255, 255, .14);
}
.brand-subtitle { margin: var(--space-2) 0 var(--space-5); color: var(--nav-muted); font-size: .82rem; }
.sidebar-collapse-btn {
  /* desktop-only -- the existing mobile-menu-button already covers
     narrow viewports with its own overlay-nav pattern, a rail-collapse
     doesn't make sense stacked on top of that */
  display: none; border: 1px solid var(--nav-line); background: var(--nav-soft); color: var(--nav-ink);
  width: 26px; height: 26px; border-radius: var(--radius-sm); cursor: pointer; align-items: center; justify-content: center;
}
@media (min-width: 901px) {
  .sidebar-collapse-btn { display: inline-flex; }
  /* .mobile-nav-header is display:contents by default (its children --
     just the brand link, normally -- flow straight into the sidebar's
     own vertical stack). Now that the collapse button lives in this
     same header and needs to sit next to the brand in one row at
     desktop width too (not just the existing mobile row layout below),
     it needs to actually be a flex box here, not pass through. */
  .mobile-nav-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-bottom: var(--space-1); }
}
.sidebar-collapse-btn:hover { background: var(--nav-soft-hover); color: #fff; }
.sidebar-collapse-btn svg { transition: transform .15s ease; }

/* context selector -- "Personal + <org(s)>". A static label at 0-1 org
   (nothing to actually pick), a real checkbox filter at 2+ (see the
   context-org-toggle script in base.html) -- matches DESIGN's own call
   that this only becomes meaningfully interactive with 2+ orgs. */
.context-selector { margin: 0 0 var(--space-4); }
.context-selector-label {
  padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); background: var(--nav-soft); color: var(--nav-ink);
  font-weight: 600; font-size: .82rem;
}
.context-selector-picker > summary {
  padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); background: var(--nav-soft); color: var(--nav-ink);
  font-weight: 600; font-size: .82rem; cursor: pointer; list-style: none;
}
.context-selector-picker > summary::-webkit-details-marker { display: none; }
.context-selector-options { padding: var(--space-2) var(--space-3) 0; display: grid; gap: var(--space-1); }
.context-selector-options .checkbox-label { color: var(--nav-muted); font-weight: 500; font-size: .82rem; }
.context-selector-options input[type="checkbox"] { width: auto; }

/* align-content: start is load-bearing, not decorative -- a grid
   container's default (align-content: normal) computes to `stretch`,
   which spreads every row apart to fill any leftover height in the
   scrollable nav rather than packing them at the top. Collapsing the
   System group (and whichever others a person closes) makes leftover
   height the common case, not a short-window edge case -- without this,
   a mostly-collapsed sidebar reads as broken, huge gaps between rows
   that have nothing to do with each other. */
.sidebar nav { display: grid; gap: var(--space-1); align-content: start; }
.nav-section-header {
  margin: var(--space-4) var(--space-3) var(--space-1); color: var(--nav-muted); font-size: .7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;
}
.nav-section-header:first-child { margin-top: 0; }
/* Compact on purpose. A nav rail is a dense index you scan, not a page
   you read -- padded out to a "roomy" 46px row, the same handful of
   links stopped fitting on one screen and the gaps between them read as
   arbitrary rather than deliberate. Back to a tight, even rhythm. */
.nav-link {
  padding: var(--space-2) var(--space-3); color: var(--nav-muted); text-decoration: none; border-radius: var(--radius-sm);
  font-weight: 600; font-size: .92rem; display: flex; align-items: center; transition: background-color .1s ease, color .1s ease;
}
.nav-link:hover { background: var(--nav-soft); color: #fff; }
/* A solid fill, not a soft tint plus a colored edge -- the previous
   inset 2px border read as tentative next to a real "this is where you
   are" signal, and a >1px colored border on a list row is a flat no
   regardless (see craft-floor). This is the exact same solid-fill/white-
   text pairing already used for the brand mark and primary buttons
   (--accent-fill), so "you are here" now reads with the same confidence
   as every other filled control in the app, not a one-off treatment. */
.nav-link.active { background: var(--accent-fill); color: #fff; }
.nav-badge {
  margin-left: auto; background: var(--nav-soft-hover); color: #fff; font-size: .7rem; font-weight: 700;
  padding: 1px 7px; border-radius: var(--radius-xs); min-width: 18px; text-align: center;
  font-family: var(--font-mono);
}
.nav-link.active .nav-badge { background: rgba(255, 255, 255, .25); }
.nav-icon { display: inline-flex; margin-right: var(--space-2); flex-shrink: 0; }

/* "More" overflow menu (Desktop app/Archives/Trash/Console/Manage
   buckets/organizations/Admin) -- a real floating popover, not another
   inline accordion, so it never pushes Buckets/Workspace around above
   it. Anchored to .nav-more (position: relative) rather than measured in
   JS: the trigger is always the popover's only sibling, so "just below
   it" is a static position, not something that needs recalculating. */
.nav-more { position: relative; }
.nav-more-trigger { width: 100%; border: none; background: none; cursor: pointer; }
.nav-more-chevron { margin-left: auto; display: inline-flex; transition: transform .12s ease; }
.nav-more-trigger[aria-expanded="true"] .nav-more-chevron { transform: rotate(90deg); }
.nav-more-popover {
  position: absolute; top: calc(100% + var(--space-1)); left: 0; right: 0; z-index: 20;
  background: var(--nav-soft); backdrop-filter: blur(6px); border: 1px solid var(--nav-line);
  border-radius: var(--radius-md); padding: var(--space-1); box-shadow: var(--shadow-raised);
  display: grid; gap: 1px;
}
/* bucket tree -- Personal/Shared/Organizations are plain static headers,
   not a disclosure: every bucket is always visible, nothing to expand,
   nothing to persist. (This used to be a <details>-per-section tree with
   its own localStorage-backed open/closed state -- removed in favor of
   always-flat on the theory that a nav rail you have to click your way
   into isn't actually faster to scan than one short flat list.) */
.bucket-tree { display: grid; gap: 2px; }
.nav-subsection-header {
  padding: var(--space-2) var(--space-3) var(--space-1); margin-top: var(--space-2); color: var(--nav-muted);
  font-weight: 600; font-size: .82rem; display: flex; align-items: center;
}
.nav-subsection-header--org { font-size: .82rem; margin-top: var(--space-1); }
.bucket-group-items { padding-left: var(--space-4); display: grid; gap: 1px; }
.bucket-tree-empty { padding: var(--space-1) var(--space-3); margin: 0; font-size: .78rem; }
/* min-width: 0 on the row AND flex/min-width on the label span inside
   the link are both load-bearing: flex items default to min-width:auto
   (never shrink below content), so a long bucket name made the whole
   row overflow the 264px rail and pushed its own controls out past the
   sidebar's right edge -- adding the visibility checkbox's ~18px is
   what finally pushed real names over the line. Still load-bearing with
   the pin button gone: the count badge overflows the same way. */
.bucket-tree-row { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.bucket-tree-link .nav-link-label { flex: 1 1 auto; min-width: 0; }
.bucket-tree-link .nav-badge { flex-shrink: 0; }
/* decides whether this bucket shows up as a column on the dashboard
   board at all (see base.html's own script, localStorage-persisted,
   same shape as the org-context checkboxes just above it). Unchecked
   just means "not on the board right now," not archived/deleted --
   nothing destructive lives behind it. Sized down from a bare native
   checkbox's default ~13px box to something that sits quietly next to
   the icon/label instead of competing with them for the row's limited
   width -- same accent-color theming as before, just smaller. */
.bucket-visibility-checkbox { flex-shrink: 0; width: 14px; height: 14px; margin: 0; accent-color: var(--accent); }
.bucket-tree-link { flex: 1; min-width: 0; font-size: .88rem; }
.bucket-tree-link span:not(.nav-icon) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.sidebar-footer { margin-top: auto; color: var(--nav-muted); font-size: .78rem; display: grid; gap: var(--space-2); }
.sidebar-account { color: #fff; font-weight: 600; font-size: .85rem; }
.link-button { border: 0; padding: 0; background: none; color: #fff; text-decoration: underline; cursor: pointer; font: inherit; }
.theme-toggle {
  border: 1px solid var(--nav-line); padding: var(--space-2) var(--space-3); background: var(--nav-soft);
  color: var(--nav-ink); cursor: pointer; font: inherit; font-size: .78rem; font-weight: 600; border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; gap: var(--space-2); justify-content: flex-start;
}
.theme-toggle:hover { background: var(--nav-soft-hover); color: #fff; }
.mobile-nav-header { display: contents; }
.mobile-nav-content { display: contents; }
.mobile-menu-button { display: none; }

/* collapsed sidebar (desktop rail mode) -- toggled via #sidebar-collapse-btn,
   persisted in localStorage, data-sidebar="collapsed" stamped on <html>
   before paint (see base.html <head> script) so there's no expanded-then-
   collapsed flash on load. The bucket tree and section headers hide
   entirely rather than trying to render a nested tree in an icon rail --
   "All sessions" still reaches every bucket's content when collapsed,
   expanding the rail again is one click for the full tree. */
:root[data-sidebar="collapsed"] .app-shell { grid-template-columns: 60px 1fr; }
:root[data-sidebar="collapsed"] .sidebar { padding-left: var(--space-3); padding-right: var(--space-3); align-items: center; }
:root[data-sidebar="collapsed"] .mobile-nav-header { flex-direction: column; justify-content: center; gap: var(--space-2); }
:root[data-sidebar="collapsed"] .brand { justify-content: center; }
:root[data-sidebar="collapsed"] .brand-text,
:root[data-sidebar="collapsed"] .brand-subtitle,
:root[data-sidebar="collapsed"] .nav-link-label,
:root[data-sidebar="collapsed"] .nav-badge,
:root[data-sidebar="collapsed"] .nav-section-header,
:root[data-sidebar="collapsed"] .context-selector,
:root[data-sidebar="collapsed"] .bucket-tree,
:root[data-sidebar="collapsed"] .sidebar-footer-text,
:root[data-sidebar="collapsed"] .sidebar-account { display: none; }
:root[data-sidebar="collapsed"] .nav-link,
:root[data-sidebar="collapsed"] .theme-toggle { justify-content: center; padding: var(--space-2); width: 100%; }
:root[data-sidebar="collapsed"] .nav-icon { margin-right: 0; }
:root[data-sidebar="collapsed"] .sidebar-collapse-btn svg { transform: rotate(180deg); }
:root[data-sidebar="collapsed"] .sidebar-footer { align-items: center; }

.main-content {
  /* was width:min(1220px,100%) + margin:0 auto -- on any viewport wider
     than sidebar+1220px that CENTERS the content column in the leftover
     space instead of sitting next to the sidebar, which is exactly the
     "crazy amount of blank space" between them on a normal-to-wide
     desktop window. Left-aligned against the sidebar now; still capped
     so text-heavy pages don't stretch to an unreadable line length on an
     ultra-wide display, just capped much wider so the kanban board
     actually gets to use the room a wide window has for it. */
  width: 100%; max-width: 1760px; margin: 0;
  /* A grid item defaults to min-width:auto, i.e. "never shrink below my
     content" -- so with enough bucket columns open this stretched to fit
     them (measured 2200px inside a 2000px window) and pushed the whole
     PAGE into horizontal scroll, instead of letting .bucket-board's own
     overflow-x scroll them. Zero lets the item stay at its track width
     and the board scroll inside it, which is what it was always meant
     to do. */
  min-width: 0;
  padding-top: var(--space-8); padding-left: var(--space-8);
  padding-right: max(var(--space-8), env(safe-area-inset-right));
  padding-bottom: max(var(--space-10), env(safe-area-inset-bottom));
}
.page-header { display: flex; justify-content: space-between; align-items: end; gap: var(--space-5); margin-bottom: var(--space-6); flex-wrap: wrap; }
.page-header h1 { font-size: 2.3rem; margin: var(--space-1) 0; }
.page-header p { margin: 0; color: var(--muted); }
h1 { font-size: 2.3rem; margin: var(--space-1) 0; font-weight: 700; letter-spacing: -.01em; }
h2 { font-size: 1.05rem; margin: var(--space-7) 0 var(--space-3); color: var(--ink); font-weight: 700; letter-spacing: -.005em; }
.section-hint { margin: calc(-1 * var(--space-2)) 0 var(--space-5); color: var(--muted); font-size: .86rem; }
/* the failure state for the status paragraphs the share/capture scripts
   write into -- they're .section-hint the rest of the time, so without
   this "Could not create a share link" rendered in exactly the same muted
   grey as "Link copied". Red here is a real error, per DESIGN.md. (The JS
   used to toggle .mic-silent, which only ever matched compounded with
   .mic-meter-label -- a different component -- so it did nothing.) */
.section-hint.is-error { color: var(--danger); font-weight: 600; }

/* panels -- flatter surfaces, hairline borders, restrained shadows. This
   should read like a panel in an instrument cluster, not a floating card. */
.panel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-md); padding: var(--space-6); box-shadow: var(--shadow); margin-bottom: var(--space-5); }
.panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-3); gap: var(--space-3); }
.panel h2 { font-size: 1.05rem; margin: 0; }

/* session_live.html's growing transcript view */
.live-transcript { display: flex; flex-direction: column; gap: var(--space-3); max-height: 40vh; overflow-y: auto; }
.live-transcript-line { margin: 0; font-size: .92rem; }
.live-transcript-line .meta { display: inline; margin-right: var(--space-2); font-family: var(--font-mono); }

/* rows / records / cards */
.record-list { display: flex; flex-direction: column; }
.record {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-5);
  padding: var(--space-4) var(--space-1); border-bottom: 1px solid var(--line); border-radius: var(--radius-xs);
}
.record:last-child { border-bottom: 0; }
.record.vertical { display: block; padding: var(--space-5) var(--space-1); }
.record-main { display: flex; gap: var(--space-3); align-items: flex-start; min-width: 0; flex: 1; }
.record h3 { margin: 0; font-size: 1rem; }
.record p { margin: var(--space-1) 0 0; color: var(--muted); font-size: .88rem; }
.record.completed h3, .completed h3 { text-decoration: line-through; }
.completed { opacity: .58; }
.meta { font-size: .78rem; color: var(--muted); margin-top: var(--space-2); display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; }
.meta a { color: var(--muted); }
.empty { color: var(--muted); text-align: center; padding: var(--space-6); }

/* dashboard kanban board -- one column per bucket, its own horizontal
   scroll container (never the page body's, see body's overflow-x:hidden)
   since a wide board is expected once you have more than 2-3 buckets.
   Each column scrolls its own body vertically too (bucket-column-body) so
   one very active bucket doesn't stretch the whole page -- same
   "wide/tall content gets its own scroll container" pattern as everywhere
   else in this file. */
.bucket-board { display: flex; align-items: flex-start; gap: var(--space-4); overflow-x: auto; padding-bottom: var(--space-2); margin-bottom: var(--space-1); }
/* The board is the one page that isn't text: .main-content's 1760px cap
   exists so prose doesn't run to an unreadable line length on an
   ultra-wide display, which is the right call everywhere else and the
   wrong one here -- it was leaving the board short of the window while
   columns still had room to grow. Released just for the board. */
.main-content:has(.bucket-board) { max-width: none; }
/* The board page dropped its "Sessions" heading and subtitle, and the
   spacing above was sized to sit under them. Without a title the same
   gaps just push the board down the screen for nothing. */
.main-content:has(.bucket-board) { padding-top: var(--space-5); }
.main-content:has(.bucket-board) .page-header { margin-bottom: var(--space-3); }
.main-content:has(.bucket-board) .ask-all-panel { margin-bottom: var(--space-3); }
/* Columns share whatever width the board actually has instead of
   sitting at a fixed 340px with dead space beside them: two buckets on a
   wide desktop each take a real share, six stay at 340px and the board
   scrolls horizontally as before. The three parts each do one job --
   grow:1 claims the slack, min-width pins the floor (without it, shrink
   would squeeze columns narrower instead of letting the board scroll,
   losing the overflow-x behaviour), and max-width is the "up to a
   point": past roughly this width a card's preview text runs longer
   than comfortable reading, so the column stops growing and the board
   just has margin. */
.bucket-column {
  flex: 1 1 340px; min-width: 340px; max-width: 900px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-md);
  box-shadow: var(--shadow); overflow: hidden; display: flex; flex-direction: column; max-height: calc(100vh - 220px);
}
/* channel/feed-style header -- the bucket color is a channel-indicator
   accent (a top stripe + a dot next to the name), never the whole header
   surface. */
.bucket-column-header {
  position: relative; background: var(--surface); color: var(--ink); border-bottom: 1px solid var(--line);
  padding: var(--space-3) var(--space-4); display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  flex: 0 0 auto; cursor: grab;
}
.bucket-column-header::before {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: var(--bucket-color, var(--accent));
}
.bucket-column-header:active { cursor: grabbing; }
.bucket-column-header h2 { margin: 0; font-size: 1rem; font-weight: 700; display: flex; align-items: center; gap: .55em; min-width: 0; overflow-wrap: anywhere; }
.bucket-color-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--bucket-color, var(--accent)); flex-shrink: 0; }
/* on mobile (see the max-width:900px block below) a bucket's own
   sessions are collapsed by default -- this is what actually opens
   them, so the bucket name itself needs to read as tappable there.
   Plain inherited color/no-underline at every other width, where the
   sessions are already sitting right below it. */
.bucket-column-title-link { color: inherit; text-decoration: none; display: flex; align-items: center; gap: .55em; min-width: 0; }

/* reminders.html's bucket -> session grouping */
.reminders-bucket-group { margin-bottom: var(--space-6); }
.reminders-bucket-heading { display: flex; align-items: center; gap: .55em; font-size: 1.15rem; margin: 0 0 var(--space-3); }
.reminders-session-group { margin-bottom: var(--space-3); }
.reminders-session-heading { font-weight: 600; margin-bottom: var(--space-2); display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.reminders-session-heading a { color: inherit; }
.reminders-session-clear-form { flex-shrink: 0; }
.bucket-column-header-actions { display: flex; align-items: center; gap: var(--space-2); flex-shrink: 0; }
/* hidden everywhere except the mobile collapsed-bucket state (the
   :has() rule in the max-width:900px block flips it on) */
.bucket-column-preview { display: none; padding: var(--space-2) var(--space-4) var(--space-3); }
.bucket-column-count {
  background: var(--soft); color: var(--muted); border: 1px solid var(--line); font-size: .74rem; font-weight: 700;
  padding: 3px 9px; border-radius: var(--radius-xs); min-width: 20px; text-align: center; font-family: var(--font-mono);
}
.bucket-start-btn, .bucket-ask-btn {
  width: 30px; height: 30px; border-radius: var(--radius-xs); border: 1px solid var(--line);
  background: var(--surface); color: var(--muted); line-height: 1;
  cursor: pointer; display: grid; place-items: center; padding: 0;
}
.bucket-start-btn:hover, .bucket-ask-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--soft); }
/* Ask carries the accent at rest, not just on hover. Shape alone wasn't
   separating these two: side by side in identical boxes at 13px, a
   sparkle and a plus were the same mark to a glance. Colour does the
   work that size can't at this scale, and it matches the sparkle used
   for Ask everywhere else in the app. */
.bucket-ask-btn { color: var(--accent); }
@media (pointer: coarse) {
  .bucket-start-btn, .bucket-ask-btn, .checkbox, .chat-copy-btn {
    min-width: 44px; min-height: 44px;
  }
}
.bucket-start-popover {
  position: relative; z-index: 5; background: var(--surface); border: 1px solid var(--line);
  box-shadow: var(--shadow-raised); border-radius: 0 0 var(--radius-sm) var(--radius-sm); padding: var(--space-3); display: flex;
  flex-direction: column; gap: var(--space-2); margin: -1px var(--space-2) 0; border-top: none;
}
/* Deliberately narrow: this styles the popover's four LAUNCHER buttons
   (Record / System audio / Manual entry / PC capture, none of which carry
   .button) and nothing else. It used to read `button.button, button:not
   (.button)` -- i.e. every button in here -- and at (0,2,1) that beat both
   .session-type-btn and .button.primary: the shared session-type picker
   rendered as filled bordered rows with no accent outline (nothing like
   its own header version, which is the point of sharing the partial), and
   the "Create session" submit lost its primary fill to --soft chrome.
   :not(.session-type-btn) keeps the picker out; dropping the .button half
   keeps the submit out. All four launchers still match, including
   .bucket-start-pc, which is nested inside its own form. */
.bucket-start-popover button:not(.button):not(.session-type-btn) {
  width: 100%; text-align: left; padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); border: 1px solid var(--line);
  background: var(--soft); color: var(--ink); font: inherit; font-weight: 600; font-size: .86rem; cursor: pointer;
  display: inline-flex; align-items: center; gap: .5em;
}
.bucket-start-popover button:not(.button):not(.session-type-btn):hover { background: var(--surface); border-color: var(--accent); }
/* was a single row -- flex:1 + min-width:0 on the input is correct on
   paper, but the popover itself is only ~250px wide, and "Create
   session"'s min-content width (the button can't shrink below its own
   text) ate almost all of that row, squeezing the name input down to a
   near-zero-width square. Stacking the input on its own full-width row
   fixes this for real instead of fighting the button for space. */
.bucket-manual-form { display: flex; flex-direction: column; gap: var(--space-2); }
.bucket-manual-form input[type="text"] { width: 100%; min-height: 2.6em; }
.bucket-manual-form button[type="submit"] { align-self: flex-end; }
.bucket-column-body { padding: var(--space-4); overflow-y: auto; background: var(--soft); flex: 1 1 auto; transition: background-color .12s ease, box-shadow .12s ease; }
.bucket-column-body .empty { padding: var(--space-5) var(--space-1); }
.bucket-column-body.drop-target { background: var(--surface); box-shadow: inset 0 0 0 2px var(--accent); }
.bucket-column.dragging { opacity: .4; }
a.session-card.panel.dragging { opacity: .4; }
.color-swatch-input {
  width: 52px; height: 40px; padding: 2px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); cursor: pointer;
}

/* per-column "Ask" popover -- grounded to just that bucket, same visual
   language as the start-session popover it sits next to */
.bucket-ask-popover {
  position: relative; z-index: 5; background: var(--surface); border: 1px solid var(--line);
  box-shadow: var(--shadow-raised); border-radius: 0 0 var(--radius-sm) var(--radius-sm); padding: var(--space-3); margin: -1px var(--space-2) 0; border-top: none;
}
/* same fix as .bucket-manual-form above -- stack instead of squeeze */
.bucket-ask-form { display: flex; flex-direction: column; gap: var(--space-2); }
.bucket-ask-form input[type="text"] { width: 100%; min-height: 2.6em; }
.bucket-ask-form button[type="submit"] { align-self: flex-end; }
.bucket-ask-answer { margin-top: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--line); line-height: 1.6; font-size: .88rem; color: var(--ink); }
.bucket-ask-answer.chat-thinking { color: var(--muted); font-style: italic; }
.bucket-ask-answer p { margin: 0 0 .7rem; }
.bucket-ask-answer p:last-child { margin-bottom: 0; }

/* "Ask all buckets" row above the board -- deliberately NOT a .panel
   (no padded/bordered card chrome) so it reads as a lean, single-line
   search field rather than its own feature block competing with the
   board underneath it. Stays one line at every width via the icon-only
   submit button -- overrides .ai-ask-form's own mobile column-stack
   fallback, which existed for a full-width text button this one never
   needs to make room for. */
.ask-all-panel { margin: 0 0 var(--space-5); }
/* align-items:center because .ai-ask-form leaves it at the default
   stretch: the input stretches to ~51px but .icon-button has a definite
   height (2.2rem), so stretch doesn't apply to it and it hung from the top
   of the row with a visible notch beneath it. Centred keeps the submit's
   deliberately lower visual weight (see .icon-button) without the notch. */
.ask-all-panel .ai-ask-form { flex-direction: row; align-items: center; }
/* In the header slot the bar takes the width the hidden title left
   behind, matching the session page's ask rather than sitting as a short
   field with space beside it. */
.page-header:has(.ask-all-panel) { align-items: flex-start; }
.page-header:has(.ask-all-panel) > div:first-child { flex: 1 1 380px; min-width: 0; }
.page-header .ask-all-panel { margin: 0; }
/* phone-only expander for the ask bar -- on desktop the bar shows
   inline and this button never renders visibly */
#ask-all-toggle-btn { display: none; }

/* Recording-type picker (see _partials/session_type_picker.html). One
   tap between "Start recording" and actually recording, so it has to
   read instantly: each option is a label plus the one-line consequence
   of choosing it, stacked, with Auto marked as the expected answer
   rather than merely listed first. */
/* Out of flow on purpose. As an ordinary third flex child of .page-header
   it reflowed the whole header: closed it was a zero-size item that still
   took the space-between main-end slot, so "Start recording" sat mid-header
   instead of flush right; open it was a ~340x365px item that either broke
   to a second header line and packed flush LEFT (a lone item under
   space-between), ~790px from the button that opened it, or stayed in row
   and bottom-aligned the h1 to a 365px-tall line. It already carries
   --shadow-raised, i.e. it was always drawn to float. */
.session-type-panel { margin: 0 0 var(--space-5); }
.session-type-picker {
  display: grid; gap: var(--space-1); padding: var(--space-3);
  border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface);
  box-shadow: var(--shadow-raised); max-width: 340px;
}
.bucket-start-popover .session-type-picker { border: 0; box-shadow: none; padding: 0; background: none; }
.session-type-prompt { margin: 0 0 var(--space-1); }
.session-type-btn {
  /* flex column with an explicit cross-axis start, not grid: as grid
     items the label/help spans were being centred (measured text left
     edges of 176/186/196/214/182px for the five options -- varying with
     text length, i.e. centred), which `text-align: left` on the button
     cannot fix because it aligns text INSIDE each span, not the spans
     themselves. align-items:flex-start pins them to the left edge. */
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 1px; text-align: left; width: 100%;
  border: 1px solid transparent; background: none; border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3); cursor: pointer; font: inherit;
}
.session-type-btn:hover { background: var(--soft); border-color: var(--line); }
.session-type-label { font-size: .9rem; font-weight: 700; color: var(--ink); }
.session-type-help { font-size: .76rem; color: var(--muted); }
.session-type-auto { border-color: var(--accent); }
/* Separated from the recording types above it: those all answer "what am
   I about to record?", this one answers "I'm not recording". A rule
   rather than extra spacing, so it reads as a different kind of choice
   instead of just a further-away one. */
.session-type-manual { margin-top: var(--space-2); border-top: 1px solid var(--line); border-radius: 0; padding-top: var(--space-3); }
.session-type-manual:hover { border-color: var(--line); border-top-color: var(--line); }
.session-type-auto .session-type-label { color: var(--accent); }

/* mid-session switcher on the live screen -- inline label + select, not
   the full picker: by this point you've already started, so it's a
   correction, not a decision */
.session-type-inline {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  margin-top: var(--space-3); font-size: .8rem; font-weight: 600; color: var(--muted);
}
/* width:auto is the load-bearing part -- the global `input, textarea,
   select { width: 100% }` was never overridden here, and a flex item with
   flex-basis:auto takes its base size from `width`, so the select claimed
   100% of the row, could never share a line with its own label (making the
   flex/gap/align-items above dead), and wrapped to line 2 at full panel
   width for a control whose longest option is ~50 characters. min-width:0
   lets it shrink on a phone; the cap keeps it sane on a wide window. */
.session-type-inline select { font-size: .82rem; padding: var(--space-1) var(--space-2); width: auto; min-width: 0; max-width: min(100%, 24rem); }

/* PC-capture status banner (dashboard, only while a capture is
   desired-active) -- one row: state badge, optional error, session
   link, stop. */
.pc-capture-banner { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; padding: var(--space-3) var(--space-5); }
.pc-capture-banner .pc-capture-stop-form { margin-left: auto; }
.pc-capture-error { margin: 0; color: var(--danger); }

/* session list (dashboard) -- each session is its own card, not a row
   sharing one panel, for a clearer break between sessions */
.session-list { display: flex; flex-direction: column; gap: var(--space-4); }
.session-card-wrap { position: relative; }
a.session-card.panel {
  display: block; text-decoration: none; color: inherit; margin-bottom: 0;
  transition: box-shadow .15s ease, transform .15s ease, border-color .15s ease;
}
a.session-card.panel:hover { box-shadow: var(--shadow-raised); transform: translateY(-1px); border-color: var(--accent); }
/* Sibling of the <a>, not nested inside it -- a <form>/<button> inside
   an <a> is invalid HTML and unreliable across browsers for exactly
   the click-hijack reason you'd want to avoid here (the whole card is
   one big link). Positioned to overlap the card's corner instead. */
/* Always visible, not hover-gated -- hover has no persistent equivalent
   on touch devices, and this app is used heavily on mobile (no way to
   "hover" a card to reveal a hidden delete button there at all). */
.session-card-delete-form { position: absolute; top: var(--space-3); right: var(--space-3); z-index: 2; }
/* Same outlined mark as the key-point delete rather than a filled
   .button.danger. As a solid red block it was the highest-contrast thing
   on a card -- louder than the session's own title, on every card at
   once -- for an action you rarely take. Red is kept, as the stroke. */
.session-card-wrap button.session-card-delete-btn {
  display: grid; place-items: center; width: 22px; height: 22px; padding: 0;
  border: 1px solid var(--line); border-radius: var(--radius-xs);
  background: var(--surface); color: var(--danger); line-height: 1;
  cursor: pointer; flex-shrink: 0; opacity: .55;
  transition: opacity .12s ease, border-color .12s ease;
}
/* Reveals on hover the way the per-point controls do, so a board of
   cards isn't a grid of delete buttons at rest. Kept fully visible on
   touch, where there is no hover to reveal it with. */
.session-card-wrap:hover button.session-card-delete-btn,
.session-card-wrap button.session-card-delete-btn:focus-visible { opacity: 1; }
.session-card-wrap button.session-card-delete-btn:hover {
  opacity: 1; border-color: currentColor; background: var(--surface); color: var(--danger);
}
@media (hover: none) {
  .session-card-wrap button.session-card-delete-btn { opacity: 1; }
}
/* The delete button above is absolutely positioned but nothing reserved
   room for it, and the h3 (no rule of its own until now) ran the card's
   full content width with the "live" badge rendered inside it -- so on a
   phone the button painted straight over the title and the badge. 36px
   clears it at both the 24px desktop and 16px mobile .panel padding.
   Still 36px with the smaller 22px mark: the button sits inset by
   --space-3 from the card's edge, so the clearance it needs is its own
   width plus that inset, not its width alone. */
.session-card h3 { padding-right: 36px; }
.session-card-inner { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-5); flex-wrap: wrap; }
.session-updated { font-size: .74rem; color: var(--muted); margin: var(--space-1) 0 0; text-transform: uppercase; letter-spacing: .04em; font-weight: 700; font-family: var(--font-mono); }
/* clamped to a few lines so a long transcript-derived summary can't make
   one card dominate the whole screen -- the point of a card on this
   board is "enough to recognize it and decide whether to open it," the
   full text is one tap away on session_detail.html either way. */
.session-card-summary { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; }

/* status system -- a colored dot + a thin rule, never a soft pastel pill.
   One component, four roles, used identically everywhere something is
   live, pending, informational, or in error. The dot-bearing chips
   (.badge, .console-status) share one padding anatomy (3px 9px 3px 8px --
   the dot's own gap eats a touch of the left side); the plain count chips
   (.bucket-column-count, .list-count, .topic) share the plain 3px 9px --
   so the same "chip" reads as one component everywhere it shows up. */
@keyframes status-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* nowrap + a width cap: body's inherited overflow-wrap:anywhere let a long
   composed label (summary.html's "Moved by X, seconded by Y") wrap inside
   the chip, and align-items:center then put the 6px dot in the gutter
   BETWEEN the two baselines -- a multi-line uppercase paragraph, which is
   the opposite of the dot-plus-thin-rule status language. Overlong labels
   now clip; no text-overflow, because ellipsis can't reach the anonymous
   text item of a flex container without a wrapper element in the markup. */
/* Badges must WRAP, never nowrap. .badge-motion carries real sentences
   ("Moved by Councilmember X, seconded by Y"), and `white-space: nowrap`
   plus `max-width: 100%` does not constrain the badge's intrinsic
   contribution to its container -- one long motion label was measured
   dragging the entire page 282px wider than a 375px viewport, which
   `body { overflow-x: hidden }` then makes permanently unreachable
   rather than merely ugly. Truncating instead isn't right either: the
   mover/seconder names are the content.
   align-items:flex-start (not center) so the status dot lands on the
   FIRST line of a wrapped label rather than floating beside its vertical
   middle; the margin-top is the optical centering on that first line. */
.badge {
  display: inline-flex; align-items: flex-start; gap: .42em; padding: 3px 9px 3px 8px;
  border: 1px solid var(--line); border-radius: var(--radius-xs); background: var(--surface);
  font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
  max-width: 100%;
}
.badge::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor;
  flex-shrink: 0; margin-top: .32em;
}
.badge-open { color: var(--live); border-color: var(--live); }
.badge-open::before { animation: status-pulse 1.8s ease-in-out infinite; }
.badge-contact { color: var(--accent); border-color: var(--accent); }
.badge-reminder { color: var(--pending); border-color: var(--pending); }
.badge-reminder::before { animation: status-pulse 2.2s ease-in-out infinite; }
.badge-vote-passed { color: var(--live); border-color: var(--live); }
.badge-vote-failed { color: var(--danger); border-color: var(--danger); }
.badge-vote-neutral { color: var(--muted); border-color: var(--line); }
/* a motion/second is procedural, not pass/fail -- always the same
   neutral tone regardless of outcome, distinct from the vote badges */
.badge-motion { color: var(--muted); border-color: var(--line); }
/* next to a session's own title -- shown once it's been manually named,
   so it's obvious at a glance the AI won't silently rename it later */
.badge-locked { color: var(--muted); border-color: var(--line); font-weight: 600; }

.topics { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
.topic { font-size: .72rem; color: var(--muted); background: var(--soft); border: 1px solid var(--line); padding: 3px 9px; border-radius: var(--radius-xs); }
/* dashboard session cards only ever render the first two topics plus
   this count of the rest (see dashboard.html) -- a card is a preview,
   not the full topic list, which is one tap away on the real session */
.topic-more { font-weight: 700; }

/* Key points read as: topic heading -> its own bullets, one group after
   another. The spacing rhythm carries that grouping (generous above a
   heading, tight from heading to its bullets and between them), so the
   structure is legible without boxing every group in its own container.
   list-style is off on purpose -- the marker below is authored, see
   .key-points li::before. */
.key-points { margin: var(--space-2) 0 0; padding-left: 0; list-style: none; }
.key-points li {
  margin: var(--space-2) 0; color: var(--ink); font-size: .92rem; line-height: 1.55;
  display: flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap;
}
/* Load-bearing, not cosmetic: with no flex sizing of its own the text span
   defaults to flex: 0 1 auto, whose flex base size is the MAX-content width
   of the whole unwrapped sentence -- and flex line-breaking is decided
   against that hypothetical size, never against what the span would shrink
   to. So any point longer than the row's remaining space broke onto flex
   line 2 and stranded the ::before marker alone on line 1 (plus the 12px
   row-gap), killing the hanging indent. Not a narrow-screen-only problem:
   at desktop width it hit any point past ~120 characters. A zero flex base
   keeps the text on the marker's line at any length and lets it wrap
   inside itself instead. flex-wrap above stays as the safety net for the
   edit controls at very narrow widths. */
.key-point-text { flex: 1 1 0; min-width: 0; }
/* The bullet marker is drawn, not native: display:flex on the li (needed
   for the inline edit controls) replaces display:list-item, which
   silently kills ::marker entirely -- every key point rendered as an
   unanchored indented line, which is exactly what it looked like. Drawn
   as a small dot rather than restored as a disc so it speaks the same
   status-dot/LED language DESIGN.md establishes everywhere else.
   translateY is an optical correction against the text baseline, not a
   layout offset. */
.key-points li::before {
  content: ''; flex: 0 0 auto; align-self: flex-start;
  /* the box is one full line tall and the dot is painted at its center,
     so the marker optically centers on the first line at ANY font size
     -- the kiosk view scales this type up and a hand-tuned pixel offset
     would only be right at one of them */
  width: 5px; height: 1.55em;
  background: radial-gradient(circle at 50% 50%, var(--accent) 2.5px, transparent 2.6px);
  opacity: .5;
}

/* Numbered variant, for a group whose points are a sequence (see
   summary.html's list_tag). The number is drawn from a CSS counter for
   the same reason the dot above is drawn at all: display:flex on the li
   suppresses ::marker, so a real <ol>'s own numbering never renders.
   Tabular figures and a fixed min-width keep 9. and 10. aligned to the
   same left edge instead of the text shifting right at double digits.
   MUST stay after the dot rule above: .key-points-ordered and
   .key-points are both a single class, so the two ::before rules tie on
   specificity and source order alone decides which wins. */
.key-points-ordered { counter-reset: keypoint; }
.key-points-ordered li { counter-increment: keypoint; }
.key-points-ordered li::before {
  content: counter(keypoint) ".";
  background: none; width: auto; min-width: 1.4em; height: auto;
  color: var(--accent); opacity: 1; font-variant-numeric: tabular-nums;
  font-weight: 700; font-size: .85em; text-align: right;
  /* baseline, not the dot's flex-start: a numeral is type and has to sit
     on the same baseline as the sentence it labels */
  align-self: baseline;
}

/* "on the fly" key-points correction (see static/key-points-edit.js) --
   the toggle bar above the list, and every per-point/per-group edit
   affordance, which the toggle bar's own text already explains stay
   hidden until "Edit" is clicked. */
.key-points-edit-bar { display: flex; align-items: center; gap: var(--space-3); margin: var(--space-3) 0 0; flex-wrap: wrap; }
.key-points-edit-hint { margin: 0; }
.key-point-row-actions, .key-point-group-delete-btn, .key-point-edit-row { display: none; }
#live-summary-panel.key-points-editing .key-point-row-actions,
#live-summary-panel.key-points-editing .key-point-group-delete-btn { display: inline-grid; }
.key-point-row-actions { align-items: center; gap: var(--space-1); }
/* grid/place-items rather than the old bare inline-block: these hold a
   real drawn icon now (see summary.html), and an inline-block would sit
   it on the text baseline with a descender gap instead of centered */
.key-point-correct-btn, .key-point-delete-btn, .key-point-save-btn, .key-point-cancel-btn, .key-point-group-delete-btn {
  border: 1px solid var(--line); background: var(--surface); color: var(--muted); border-radius: var(--radius-xs);
  width: 22px; height: 22px; padding: 0; line-height: 1; cursor: pointer; flex-shrink: 0;
  place-items: center;
}
/* `display` is set per-button below, NOT in the shared rule above. The
   four row buttons are shown/hidden by their CONTAINER
   (.key-point-row-actions / .key-point-edit-row), so they can carry
   display:grid unconditionally. .key-point-group-delete-btn is the odd
   one out -- it's hidden and shown DIRECTLY -- so giving it a display
   here would tie on specificity with the `display:none` rule above and
   win on source order, leaving the delete x permanently visible AND on
   its own line outside edit mode. That is exactly what happened. */
.key-point-correct-btn, .key-point-delete-btn, .key-point-save-btn, .key-point-cancel-btn {
  display: grid;
}
.key-point-correct-btn:hover, .key-point-delete-btn:hover, .key-point-save-btn:hover,
.key-point-cancel-btn:hover, .key-point-group-delete-btn:hover { border-color: currentColor; }
.key-point-delete-btn, .key-point-cancel-btn, .key-point-group-delete-btn { color: var(--danger); }
.key-point-save-btn { color: var(--live); }
.key-point-group-delete-btn { margin-left: var(--space-2); vertical-align: middle; }
/* Pinned to the heading's own first line instead of left in inline flow.
   The button is an atomic inline box ~42px wide once the coarse-pointer
   34px floor and its margin are counted, and its only break opportunity is
   the single space before it in the markup -- so any topic label ending
   within ~42px of the right edge (about 31-36 characters at phone width,
   i.e. ordinary LLM topic length) pushed the whole button onto a line of
   its own at the paragraph's left edge. padding-right reserves the room
   rather than layering over the text. Taking it out of flow also stops its
   34px min-height inflating a ~25px heading line box. Both halves are
   scoped to .key-points-editing so the non-edit heading -- where the
   button is display:none anyway -- keeps exactly the box it has today. */
#live-summary-panel.key-points-editing .key-point-group-topic { position: relative; padding-right: 42px; }
#live-summary-panel.key-points-editing .key-point-group-delete-btn { position: absolute; top: 0; right: 0; margin-left: 0; }
.key-point-editing-row .key-point-text { display: none; }
/* has to match the ID: the rule above that reveals the per-point
   pencil/delete pair while editing is (1,2,0), so the plain
   .key-point-editing-row (0,2,0) hide this used to share with
   .key-point-text never applied -- the pair stayed on screen beside the
   input and overflowed the row hard enough to draw Cancel over Correct. */
#live-summary-panel.key-points-editing .key-point-editing-row .key-point-row-actions { display: none; }
.key-point-editing-row .key-point-edit-row { display: flex; align-items: center; gap: var(--space-2); flex: 1; min-width: 200px; }
/* min-width:0 (not 160px) so the row can still shrink at 320px; the tighter
   padding/line-height brings the field down from ~49px to ~37px, near the
   34px the coarse-pointer block gives the Save/Cancel buttons beside it,
   which were otherwise less than half its height. */
.key-point-edit-input { flex: 1; min-width: 0; padding: var(--space-2) var(--space-3); line-height: 1.3; }
/* Save/Cancel act on the input immediately beside them, so they match
   its height instead of sitting as 22px marks against a ~36px field.
   align-self:stretch rather than a hard-coded height, so they track the
   input at whatever height its own padding/font produce -- including the
   larger coarse-pointer sizing, where a fixed height would go back to
   disagreeing. */
.key-point-editing-row .key-point-save-btn,
.key-point-editing-row .key-point-cancel-btn { height: auto; align-self: stretch; width: 30px; }

/* Heading for one key-points group. It used to render at .84rem against
   .92rem bullets -- a heading literally smaller than the content it
   heads, which is why the breakdown read as one flat wall of text.
   Now a real step UP from the bullets, with a hairline rule beneath it
   so each group separates structurally (the channel/feed-header
   language DESIGN.md asks for) rather than relying on weight alone.
   Generous space above, tight space below: the heading belongs to the
   bullets under it, not to the group before it. */
.key-point-group-topic {
  margin: var(--space-6) 0 var(--space-3); font-size: 1rem; font-weight: 700; color: var(--ink);
  letter-spacing: -.005em; padding-bottom: var(--space-2); border-bottom: 1px solid var(--line);
}
/* the first group sits right under the "Key points (N)" heading, which
   already provides the separation the rule above is for */
.key-point-group-topic:first-of-type { margin-top: var(--space-4); }

/* nested subtopic, one level down: clearly subordinate (smaller than the
   1rem parent, muted, no rule of its own) and indented together with its
   own bullets so the nesting reads as one block, not a heading that
   happens to sit further right than the text it owns */
/* :not(.key-point-depth-1) rather than .key-point-depth-2 so the styling
   DEGRADES at depth 3+ instead of resetting: the macro in summary.html
   recurses with no cap and the outline editor accepts `#### `, and with
   nothing matching depth 3 a grandchild heading fell all the way back to
   the depth-1 look -- bigger, darker, ruled, and further LEFT than the
   parent that owns it, with un-indented bullets under it. */
.key-point-group-topic:not(.key-point-depth-1) {
  margin: var(--space-4) 0 var(--space-2) var(--space-4); font-size: .88rem; font-weight: 600;
  color: var(--muted); border-bottom: 0; padding-bottom: 0; letter-spacing: 0;
}
.key-point-group-topic:not(.key-point-depth-1) + .key-points { margin-left: var(--space-4); }
/* one more indent step. Both selectors have to stay compounded with
   .key-point-group-topic: a bare .key-point-depth-3 is (0,1,0) and would
   lose its margin-left to the (0,2,0) `margin` shorthand just above. */
.key-point-group-topic.key-point-depth-3 { margin-left: var(--space-6); }
.key-point-group-topic.key-point-depth-3 + .key-points { margin-left: var(--space-6); }

/* literal agenda item number/letter prefixed onto a key-point group's own
   heading (e.g. "Item 5: Zoning Variance Request") -- muted so it reads as
   a reference label, not competing with the topic text's own weight */
.key-point-item-number { color: var(--muted); font-weight: 600; }

.identify-forms { display: flex; flex-wrap: wrap; gap: var(--space-3); }
.identify-forms form { flex: 1; min-width: 200px; }
.identify-all-row { padding-bottom: var(--space-5); margin-bottom: var(--space-1); border-bottom: 1px solid var(--line); }
.identify-all-row .section-hint { margin: 0 0 var(--space-3); }

/* forms */
label { display: grid; gap: var(--space-2); font-size: .82rem; font-weight: 600; color: var(--ink); }
input, textarea, select {
  width: 100%; border: 1px solid var(--line); border-radius: var(--radius-sm); padding: var(--space-3);
  font: inherit; color: var(--ink); background: var(--surface);
}
textarea { resize: vertical; }
input:focus, textarea:focus, select:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
::placeholder { color: var(--muted); opacity: .85; }
.form-row { display: flex; gap: var(--space-2); flex-wrap: wrap; }
/* a <select> sizes itself to its longest option text by default, which can
   force a flex row (settings' timezone picker, a chunk's "move to session"
   dropdown, session_detail's merge/move dropdowns) wider than its column
   on a phone -- letting it shrink and grow like the text inputs next to it
   fixes the row instead of relying on the wrap above as a fallback. */
.form-row select, .rename-form select, .row-actions select { flex: 1; min-width: 0; }
/* batch action above the individual pending questions -- separated by a
   rule so it reads as acting on the whole group below it rather than as
   the first question's own controls */
.answer-all-row {
  align-items: center; padding-bottom: var(--space-4); margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--line);
}
.answer-all-label { font-size: .82rem; font-weight: 600; color: var(--muted); flex-shrink: 0; }
.answer-all-row input[type="text"] { flex: 1; min-width: 120px; }

.button, button {
  display: inline-flex; align-items: center; justify-content: center; gap: .5em; border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4); font-weight: 700; text-decoration: none; cursor: pointer; font: inherit;
  background: var(--surface); color: var(--ink); border: 1px solid var(--line);
  transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
.button svg, button svg { flex-shrink: 0; }
/* one dominant, sharp, confident color per action -- no gradients, no glow */
.button.primary, button.primary, button[type="submit"] { background: var(--accent-fill); color: #fff; border-color: var(--accent-fill); }
.button.primary:hover, button.primary:hover, button[type="submit"]:hover { background: var(--accent-fill-hover); border-color: var(--accent-fill-hover); }
.button.secondary, button.secondary { background: var(--surface); color: var(--accent); border: 1px solid var(--line); }
.button.secondary:hover, button.secondary:hover { border-color: var(--accent); }
.button.danger, button.danger { background: var(--danger-fill); color: #fff; border-color: var(--danger-fill); }
.button.confirm, button.confirm { background: var(--live-fill); color: #fff; border-color: var(--live-fill); }
.button.small, button.small { padding: var(--space-2) var(--space-3); font-size: .77rem; }
.button.wide, .wide { width: 100%; }
.button:disabled, button:disabled { opacity: .55; cursor: default; }
/* icon-only variant -- Quick reminder/Share-a-link/Ask-all's submit are
   deliberately smaller and lower-visual-weight than a primary text
   button like "Start recording", so the one action that actually
   matters most reads as the obvious default rather than competing
   evenly with two or three secondary ones next to it. */
/* rem, not em: the header's icon buttons also carry .small, whose
   font-size:.77rem (0,2,0) beats this rule's own (0,1,0) and made 2.2em
   resolve to 27px instead of the 35px it was drawn for -- next to a 51px
   "Start recording", and against the ask-all submit, which has no .small
   and stayed 35px. The repeated padding matches .button.small's
   specificity so the ring around the 15px glyph stays the intended 8px
   rather than being widened to 12px. Coarse-pointer 44px floor unchanged. */
.icon-button { padding: var(--space-2); width: 2.2rem; height: 2.2rem; }
.button.small.icon-button { padding: var(--space-2); }
.icon-button svg { margin: 0; }

/* button[type="submit"] (above) has higher specificity than a bare class,
   so it was silently winning over .checkbox and painting every toggle
   button solid teal instead of an empty box -- repeating the class here
   (button.checkbox) matches that specificity so source order decides it
   correctly instead. */
.checkbox, button.checkbox {
  width: 28px; height: 28px; border: 2px solid var(--line); border-radius: var(--radius-xs); background: var(--surface);
  color: var(--live); font-weight: 700; cursor: pointer; flex-shrink: 0; display: grid; place-items: center; padding: 0;
}
.checkbox.checked, button.checkbox.checked { border-color: var(--live-fill); background: var(--live-fill); color: #fff; }

.row-actions, .actions { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; }
.actions { margin-left: auto; }

/* shared "label + one action" row -- admin.html and bucket_access.html
   each hand-copied this exact shape as an inline style 4 times over; one
   real class here instead, and hardened to wrap onto two lines rather than
   squeezing the action button off-screen next to a long name or email. */
.list-row {
  display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap;
  gap: var(--space-2); padding: var(--space-3) 0; border-bottom: 1px solid var(--line);
}
.list-row:last-child { border-bottom: 0; }
/* Two side-by-side actions on a .list-row (scheduled captures' own
   Pause/Delete pair) -- .list-row's own flex-wrap already keeps them
   from crowding a long label on narrow screens, this just keeps the
   two buttons themselves from stacking on top of each other. */
.row-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
/* A run of checkboxes read as one control (weekday picker, week-of-month
   picker) rather than a vertical list -- wraps rather than overflowing
   on a narrow phone screen recording a scheduled capture was set up for. */
.checkbox-row { display: flex; flex-wrap: wrap; gap: var(--space-3); }
/* Once/Recurring radio pair -- same row-of-choices shape as
   .checkbox-row, kept separate since it's exactly two options and
   toggles which fields below it are visible. */
.capture-schedule-mode { display: flex; gap: var(--space-4); margin-bottom: var(--space-2); }
.capture-schedule-mode label { display: flex; align-items: center; gap: var(--space-2); font-weight: 600; font-size: .88rem; }

.flash {
  display: flex; align-items: flex-start; gap: .55em; padding: var(--space-3) var(--space-4); border: 1px solid var(--line);
  border-radius: var(--radius-sm); margin-bottom: var(--space-4); font-weight: 600; overflow-wrap: anywhere; background: var(--surface);
}
.flash::before { content: ''; width: 7px; height: 7px; margin-top: .45em; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.flash.success { color: var(--live); border-color: var(--live); }
.flash.error { color: var(--danger); border-color: var(--danger); }
.flash-link-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-top: var(--space-2);
}
.flash-link-url {
  flex: 1 1 220px; min-width: 0; overflow-wrap: anywhere; background: var(--soft); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius-xs); padding: var(--space-2) var(--space-3); font-family: var(--font-mono);
  font-size: .82rem; font-weight: 500;
}
.flash-copy-btn { flex: 0 0 auto; }

/* login -- flat charcoal-navy background, same instrument-bezel color as
   the sidebar, not a gradient; the card sits on --surface so it holds up
   in dark mode instead of being a hardcoded white rectangle. */
.login-main {
  min-height: 100vh; display: grid; place-items: center; background: var(--nav);
  padding-top: max(var(--space-7), env(safe-area-inset-top));
  padding-right: max(var(--space-7), env(safe-area-inset-right));
  padding-bottom: max(var(--space-7), env(safe-area-inset-bottom));
  padding-left: max(var(--space-7), env(safe-area-inset-left));
}
/* session_live_view.html's fullscreen "news feed" pop-out -- no sidebar,
   no centered-card constraint (unlike .login-main, which is built to
   center exactly one small card, wrong for a feed that should use the
   whole window) -- just fills the viewport and lets its own content
   set the actual reading width. */
.kiosk-main {
  min-height: 100vh; background: var(--soft);
  /* base.html sets viewport-fit=cover, so env() insets are real on a
     notched device -- and unlike .login-main and .main-content, which both
     pad against them, this <main> had none at all and ran the feed under
     the cutout (a landscape iPhone's side inset is ~47-59px against
     .kiosk-feed's fixed 16-24px padding). Insets are 0 everywhere else, so
     this is a no-op off a notched device. */
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  padding-bottom: env(safe-area-inset-bottom);
}

/* session_live_view.html's own content -- wide reading column (not the
   narrow login-card width), bigger type since this is meant to be
   glanced at from across a room/second monitor rather than read closely
   at a desk the way session_detail.html's own copy of these same
   panels is. */
.kiosk-feed { max-width: 900px; margin: 0 auto; padding: var(--space-7) var(--space-6); }
.kiosk-feed-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-6); flex-wrap: wrap; }
.kiosk-feed-header h1 .badge { vertical-align: middle; margin-left: var(--space-2); }
.kiosk-feed-actions { display: flex; align-items: center; gap: var(--space-3); flex-shrink: 0; }

/* Key-point order switch (see static/view-order.js). One control for one
   two-state choice: the segmented pair it replaces made you read both
   options to work out which was on. The label names the CURRENT order,
   and the track shows the state independently of it -- so "which way is
   this set" survives losing color fidelity or reading it from across a
   room, which is the kiosk case these screens exist for. */
.view-order-switch {
  display: inline-flex; align-items: center; gap: var(--space-2);
  border: 1px solid var(--line); border-radius: 999px; background: var(--surface);
  color: var(--muted); cursor: pointer; font: inherit; font-size: .74rem;
  font-weight: 600; letter-spacing: .01em; white-space: nowrap; flex-shrink: 0;
  padding: var(--space-2) var(--space-3) var(--space-2) var(--space-2);
}
.view-order-switch:hover { background: var(--soft); color: var(--ink); }
.view-order-track {
  position: relative; width: 30px; height: 17px; border-radius: 999px;
  background: var(--line); flex-shrink: 0; transition: background-color .15s ease;
}
.view-order-thumb {
  position: absolute; top: 2px; left: 2px; width: 13px; height: 13px;
  border-radius: 50%; background: var(--surface); transition: transform .15s ease;
  box-shadow: 0 1px 2px rgba(16, 24, 38, .25);
}
/* --accent-fill, not --accent: this is a SOLID block of role color, which
   is what the fill tokens exist for (see :root) -- --accent brightens in
   dark mode to stay legible as TEXT and would wash out here. */
.view-order-switch.on { color: var(--ink); }
.view-order-switch.on .view-order-track { background: var(--accent-fill); }
.view-order-switch.on .view-order-thumb { transform: translateX(13px); }
@media (prefers-reduced-motion: reduce) {
  .view-order-track, .view-order-thumb { transition: none; }
}
.kiosk-feed-header h1 { margin: 0; font-size: 1.6rem; }
.kiosk-feed #live-summary-panel { font-size: 1.08rem; }
.kiosk-feed #live-summary-panel .session-summary { font-size: 1.15rem; line-height: 1.6; }
/* scales the same hierarchy up for across-the-room reading -- the topic
   heading has to stay a step ABOVE its own bullets here too (it used to
   land at 1rem against 1.02rem bullets, inverting the hierarchy exactly
   where the type is largest and the inversion is most obvious) */
.kiosk-feed #live-summary-panel .key-point-group-topic { font-size: 1.15rem; }
/* :not(depth-1) rather than .key-point-depth-2 so depth 3+ steps down here
   too instead of falling back to the depth-1 size; 1.06rem, not 1rem,
   because the bullets under it are 1.02rem -- a subheading smaller than its
   own content is the exact inversion the note above says was fixed. */
.kiosk-feed #live-summary-panel .key-point-group-topic:not(.key-point-depth-1) { font-size: 1.06rem; }
.kiosk-feed #live-summary-panel .key-points li { font-size: 1.02rem; line-height: 1.6; }
/* the panel headings ("Summary", "Key points (N)", "Transcript") and the
   transcript itself were the only things this kiosk block never scaled --
   they kept their desk-view 1.05rem/.92rem, so the outermost heading on
   the page was SMALLER than the body copy under it and the half of the
   screen that actually updates live was the smallest text on a view whose
   whole purpose is being read from across a room. .kiosk-feed .panel (not
   #live-summary-panel) so the transcript and speaker-clip panels, which
   are siblings inside the same feed, are covered in the same pass. */
.kiosk-feed .panel h2 { font-size: 1.3rem; }
.kiosk-feed #live-transcript-panel .live-transcript-line { font-size: 1.02rem; line-height: 1.6; }
@media (max-width: 640px) {
  .kiosk-feed { padding: var(--space-5) var(--space-4); }
  .kiosk-feed-header h1 { font-size: 1.3rem; }
}
.login-card { width: min(360px, 100%); background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg); padding: var(--space-8); box-shadow: var(--shadow-raised); text-align: center; }
.login-brand { display: flex; justify-content: center; margin-bottom: var(--space-2); }
.login-card h1 { margin: 0 0 var(--space-2); }
.login-card > p { color: var(--muted); margin: 0; }

/* public share link page (public_share.html) -- same centered-card look
   as the login page, just wider to fit the record widget/dropzone */
.share-card { width: min(480px, 100%); text-align: center; }
.share-record-row { justify-content: center; margin-top: var(--space-6); }
.share-divider { color: var(--muted); font-size: .8rem; margin: var(--space-5) 0; text-transform: uppercase; letter-spacing: .06em; }
.share-card .dropzone { text-align: center; }
.stacked-form { display: grid; gap: var(--space-4); margin-top: var(--space-6); }
.stacked-form input { text-align: center; }
.error { color: var(--danger); margin-top: var(--space-4); font-weight: 600; }
.sso-btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-3); }
.login-pin-toggle { margin-top: var(--space-6); font-size: .82rem; color: var(--muted); }
.login-pin-toggle summary { cursor: pointer; }
.login-pin-toggle .stacked-form { margin-top: var(--space-4); }

/* quote / transcript / console -- the dark instrument-console register;
   this is the app's reference implementation, extended everywhere else */
.quote { color: var(--muted); font-style: italic; font-size: .9rem; margin: .5rem 0; }
.transcript, .console-view {
  white-space: pre-wrap; background: #0a0d13; border: 1px solid #202838; color: #d7dde8;
  border-radius: var(--radius-sm); padding: var(--space-4); font-size: .87rem; font-family: var(--font-mono);
}
.console-view { height: 60vh; overflow-y: auto; }
/* empty state -- otherwise a session with nothing captured yet (or a
   wired-capture that's never connected) is just a blank dark rectangle
   with no explanation. Disappears the moment the first line lands since
   :empty stops matching. Rendered in the UI sans face, not mono -- this is
   chrome copy, not console data. */
.console-view:empty::before {
  content: 'Waiting for activity from the capture pipeline\2026';
  color: var(--muted); font-style: italic; font-family: var(--font-sans); font-size: .88rem;
}
.console-line { padding: .1rem 0; }
.console-ts { color: #7f8aa3; margin-right: var(--space-3); }
.console-status {
  font-size: .72rem; font-weight: 700; color: var(--muted); border: 1px solid var(--line);
  border-radius: var(--radius-xs); padding: 3px 9px 3px 8px; vertical-align: middle; margin-left: var(--space-3);
  text-transform: none; font-family: var(--font-mono); display: inline-flex; align-items: center; gap: .4em;
}
.console-status::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex-shrink: 0; opacity: .5; }
.console-status.live { color: var(--live); border-color: var(--live); background: transparent; }
.console-status.live::before { opacity: 1; animation: status-pulse 1.8s ease-in-out infinite; }

/* lists page */
.form-panel > summary, .compact-add-panel > summary { cursor: pointer; font-weight: 700; color: var(--accent); list-style: none; }
.form-panel > summary::-webkit-details-marker, .compact-add-panel > summary::-webkit-details-marker { display: none; }
.form-panel { margin-bottom: var(--space-5); }
.form-panel form { margin-top: var(--space-4); }
.compact-add-panel { margin-top: var(--space-4); border-top: 1px solid var(--line); padding-top: var(--space-3); }
.compact-add-panel form { margin-top: var(--space-3); }
.list-card-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-5); }
.list-card { margin: 0; display: flex; flex-direction: column; }
.list-card-header { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); }
.list-card-header h2 { margin: 0; font-size: 1.08rem; }
.list-count { font-size: .7rem; font-weight: 700; color: var(--accent); background: var(--soft); border: 1px solid var(--line); padding: 3px 9px; border-radius: var(--radius-xs); font-family: var(--font-mono); }
.list-card-header form { margin-left: auto; }
.list-item-done h3, .list-item-done .record p:first-child { text-decoration: line-through; color: var(--muted); }
.list-item-edit { flex: 1; min-width: 0; }
.list-item-edit > summary { cursor: pointer; list-style: none; display: flex; align-items: center; min-height: 30px; }
/* reminders.html's title + who/when meta both need to live inside
   summary (content outside summary is natively hidden by <details>
   until expanded, so meta can't just sit alongside summary as a
   sibling) -- but wrapping them in one block-level flex item, instead
   of leaving them as two separate flex children of summary's own row,
   is what actually matters here. Without this wrapper, .meta's own
   un-shrinking preferred width (flex: 0 1 auto, sized to its "Zack and
   project team" / "before renewal" content) competed directly against
   h3 for the same flex row and could squeeze the title down to a
   handful of pixels -- confirmed live, wrapped to one character per
   line. Stacking them inside one shared flex item instead lets h3 and
   .meta size normally in ordinary block flow (title, then meta below
   it, each wrapping on its own), while that one item is the only thing
   contending with summary's own "‹ edit" affordance for space. */
.list-item-edit-heading { flex: 1; min-width: 0; }
.list-item-edit > summary::-webkit-details-marker { display: none; }
.list-item-edit > summary::after { content: '\2039 edit'; margin-left: auto; padding-left: var(--space-3); color: var(--muted); font-size: .74rem; font-weight: 700; flex-shrink: 0; }
.list-item-edit > summary h3 { margin: 0; overflow-wrap: anywhere; }
.list-item-edit[open] > summary::after { content: 'close \203a'; }
.list-item-edit form { margin-top: var(--space-2); }
.form-grid-edit { display: flex; flex-direction: column; gap: var(--space-2); max-width: 480px; }
.form-grid-edit .button { align-self: flex-start; }

/* share-link creation form (session_detail.html / dashboard.html) */
.share-link-toggle { margin: var(--space-4) 0 var(--space-7); }
.share-link-panel { margin-top: var(--space-3); }
.expiry-fieldset { border: 0; padding: 0; margin: var(--space-1) 0; display: flex; flex-direction: column; gap: var(--space-2); }
.radio-label, .checkbox-label { display: flex; align-items: center; gap: var(--space-2); font-weight: 600; font-size: .88rem; color: var(--ink); }
.radio-label input[type="radio"], .checkbox-label input[type="checkbox"] { width: auto; }
.inline-number { width: 64px; display: inline-block; padding: var(--space-1) var(--space-2); }
.share-link-list { margin-top: var(--space-4); border-top: 1px solid var(--line); padding-top: var(--space-2); }
/* The 32px of section spacing belongs to the row, not to the button. The
   `-link` name is stale -- this is a submit button inside an
   align-items:center .record-row now, and flexbox centres an item's MARGIN
   box, so a 37px button with 32px under it made a 69px-tall line and sat
   ~16px above the checkbox label beside it. :not(.share-record-row) leaves
   public_share_pdf.html's card-bottom copy of this form alone; only
   session_detail's in-page row needs separation beneath it. */
.pdf-download-link { margin: 0; }
.pdf-download-form:not(.share-record-row) { margin-bottom: var(--space-7); }

/* summary panel's own edit toggle -- separate from .list-item-edit above
   since that one assumes a single-line flex summary (a contact/reminder's
   name); this one's summary is a whole multi-paragraph block */
.summary-edit-toggle { margin-top: var(--space-4); }
.summary-edit-toggle > summary {
  cursor: pointer; list-style: none; display: inline-block;
  color: var(--accent); font-weight: 700; font-size: .82rem;
}
.summary-edit-toggle > summary::-webkit-details-marker { display: none; }
.summary-edit-toggle form { margin-top: var(--space-3); }

/* whole Chunks section collapsed by default too, on top of each chunk
   already being collapsed -- a session with a dozen+ chunks was still a
   wall of collapsed rows just to scroll past */
/* The section's separation belongs to the <details>, not to the <h2>
   inside its <summary>. summary is display:flex, so the h2 is a flex item
   and its margin never collapses -- the 32px lived INSIDE the summary box,
   on top of the preceding paragraph's own 16px, giving a 48px void above
   "Key points (N)". It also broke the row two other ways: align-items
   centres MARGIN boxes, so the h2's 58px margin box set the line height
   and the "collapse ›" affordance floated 16px above the heading text; and
   because summary is the <details> activation target, the empty 32px band
   was a live tap target that collapsed the whole section. On the
   <details> the same 32px collapses normally against the block above --
   all five call sites are preceded by a zero-bottom-margin block, so every
   one renders at the same distance it does today. */
.chunks-section-toggle { margin-top: var(--space-7); }
.chunks-section-toggle > summary { cursor: pointer; list-style: none; display: flex; align-items: center; }
.chunks-section-toggle > summary h2 { margin: 0; }
.chunks-section-toggle > summary::-webkit-details-marker { display: none; }
.chunks-section-toggle > summary::after { content: '\2039 expand'; margin-left: auto; color: var(--muted); font-size: .74rem; font-weight: 700; }
.chunks-section-toggle[open] > summary::after { content: 'collapse \203a'; }

/* chunks -- collapsed <details> by default so a long session's transcript
   list doesn't dominate the page; expand one to read/edit/move/delete it */
.chunk-disclosure > summary { cursor: pointer; list-style: none; display: flex; align-items: center; }
.chunk-disclosure > summary::-webkit-details-marker { display: none; }
.chunk-disclosure > summary::after { content: '\2039 expand'; margin-left: auto; color: var(--muted); font-size: .74rem; font-weight: 700; }
.chunk-disclosure[open] > summary::after { content: 'collapse \203a'; }
.chunk-disclosure > summary .meta { font-family: var(--font-mono); }
.chunk-body { margin-top: var(--space-3); }
.chunk-edit-form { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-3); }
.chunk-edit-form textarea { font-family: var(--font-mono); font-size: .85rem; }
.chunk-edit-form .button { align-self: flex-start; }

/* chat / ask */
.ai-ask-form { display: flex; gap: var(--space-3); margin-bottom: var(--space-2); }
.ai-ask-form input { flex: 1; }
.chat-log { display: flex; flex-direction: column; gap: var(--space-6); margin-bottom: var(--space-6); }
.chat-exchange { display: flex; flex-direction: column; gap: var(--space-4); }
.chat-question {
  align-self: flex-end; background: var(--soft); border: 1px solid var(--line); color: var(--ink);
  padding: var(--space-3) var(--space-4); border-radius: var(--radius-md); max-width: 80%; margin: 0; font-weight: 600;
}

/* assistant answer -- avatar + rich content */
.chat-answer-row { display: grid; grid-template-columns: 32px minmax(0, 1fr); gap: var(--space-3); align-items: start; }
.chat-avatar {
  width: 32px; height: 32px; border-radius: var(--radius-sm); display: grid; place-items: center;
  background: var(--accent-fill); color: #fff; font-size: .78rem; font-weight: 700; border: 1px solid rgba(255, 255, 255, .14);
}
.chat-answer-wrap { min-width: 0; }
.chat-answer { line-height: 1.7; font-size: .96rem; color: var(--ink); overflow-wrap: anywhere; max-width: 75ch; }
.chat-answer.chat-thinking { color: var(--muted); font-style: italic; }
.chat-answer p { margin: 0 0 var(--space-4); }
.chat-answer p:last-child { margin-bottom: 0; }
.chat-answer h2, .chat-answer h3, .chat-answer h4, .chat-answer h5, .chat-answer h6 {
  margin: var(--space-4) 0 var(--space-2); line-height: 1.3; font-weight: 700; color: var(--ink);
}
.chat-answer h2:first-child, .chat-answer h3:first-child, .chat-answer h4:first-child { margin-top: 0; }
.chat-answer ul, .chat-answer ol { margin: 0 0 var(--space-4); padding-left: 1.3em; }
.chat-answer li { margin: var(--space-1) 0; }
.chat-answer strong { font-weight: 700; }
.chat-answer code {
  background: var(--soft); border: 1px solid var(--line); border-radius: var(--radius-xs); padding: .15em .4em; font-size: .9em;
  font-family: var(--font-mono);
}
.chat-answer pre.code-block {
  background: #0a0d13; color: #d7dde8; border-radius: var(--radius-sm); padding: var(--space-4);
  overflow-x: auto; margin: var(--space-1) 0 var(--space-4); font-size: .82rem; line-height: 1.55; font-family: var(--font-mono);
}
.chat-answer pre.code-block code { background: transparent; border: 0; padding: 0; font-size: inherit; color: inherit; }
.chat-link { color: var(--accent); font-weight: 700; text-decoration: underline; }
.response-link { color: var(--accent); text-decoration: none; border-bottom: 1px solid var(--accent); }
.response-link:hover { border-bottom-color: currentColor; }

.chat-actions { display: flex; gap: var(--space-2); margin-top: var(--space-2); opacity: .5; transition: opacity .15s ease; }
.chat-answer-row:hover .chat-actions { opacity: 1; }
.chat-copy-btn {
  background: transparent; border: 0; color: var(--muted); padding: var(--space-2);
  border-radius: var(--radius-xs); font-size: .8rem; cursor: pointer; display: inline-flex; align-items: center;
}
.chat-copy-btn:hover { background: var(--soft); color: var(--accent); }
.chat-copy-btn svg { display: block; }

.chat-dots span { animation: chat-blink 1.4s infinite both; }
.chat-dots span:nth-child(2) { animation-delay: .2s; }
.chat-dots span:nth-child(3) { animation-delay: .4s; }
@keyframes chat-blink { 0%, 80%, 100% { opacity: .2; } 40% { opacity: 1; } }

/* session detail */
.back { color: var(--accent); text-decoration: none; font-size: .85rem; font-weight: 700; display: inline-flex; align-items: center; gap: .4em; }
.back:hover { text-decoration: underline; }
.session-detail-header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; }
/* The session page carries a lot of chrome above its own content --
   header, rename/merge/move, the action row, share-a-link, PDF, Ask --
   and measured 518px of it, putting the summary at y=962 on an 800px
   desktop viewport: the point of the page started below the fold. These
   trim the gaps between those blocks rather than removing any of them;
   nothing here is hidden, it's just no longer spaced like a series of
   unrelated page sections. */
.main-content:has(.session-detail-header) h1 { font-size: 1.7rem; }
.main-content:has(.session-detail-header) .chunks-section-toggle { margin-top: var(--space-4); }
.main-content:has(.session-detail-header) .record-panel { margin-bottom: var(--space-4); }
.session-detail-header { margin-bottom: var(--space-4); }
.session-detail-header h1 { margin-bottom: 0; display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.session-detail-header .session-date { margin: var(--space-1) 0 0; color: var(--muted); }
.session-detail-header-actions { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
/* the session-level structural actions (rename / merge / move-to-bucket)
   -- grouped under one heading rhythm with a small caption per row so
   each control's purpose reads before you touch it, instead of three
   identical unlabeled rows stacked with no way to tell them apart. */
.session-actions { display: flex; flex-direction: column; gap: var(--space-4); margin: var(--space-4) 0 var(--space-7); }
.session-action-label { display: block; font-size: .78rem; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; margin-bottom: var(--space-2); }
.rename-form { display: flex; gap: var(--space-2); margin: 0; max-width: 420px; flex-wrap: wrap; }
.rename-form input { flex: 1; }
/* preserves the blank-line breaks between the transcript summary and any
   appended "from an attached document/email/screenshot" blurbs */
.session-summary { white-space: pre-wrap; max-width: 75ch; }

/* browser mic recording (session_detail.html + dashboard.html, static/recorder.js) */
/* :not(:empty) guard, same as #capture-status further down: these panels
   hold three always-empty status placeholders, and an empty <p> with no
   border or padding is self-collapsing -- its bottom margin collapsed
   straight through as a permanent ghost gap, then became a real ~21px line
   box the moment it filled and shoved everything below it down. */
.record-panel .section-hint { margin: 0; }
.record-panel .section-hint:not(:empty) { margin: 0 0 var(--space-4); }
.record-row { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.new-record-widget { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.mono-readout { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* mic input level meter (session_live.html + quick_capture.html) --
   "is it actually hearing me?" reassurance while recording. Width
   transition is intentionally very short (not the usual .15-.2s this
   app uses elsewhere) -- a meter needs to feel instantaneous, a
   noticeably-lagged one reads as broken rather than smooth. */
/* Capture-source chooser -- desktop app only, and only while it's being
   asked. Its own row rather than inline with Record, because it appears
   in response to pressing Record and should read as the next step, not
   as another control competing with it. */
.capture-source-choice {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--space-2); margin-top: var(--space-3);
}
.capture-source-caption { font-size: .78rem; color: var(--muted); font-weight: 600; flex-shrink: 0; }
@media (max-width: 650px) {
  .capture-source-choice .button { flex: 1 1 auto; }
}
.mic-meter-row { display: flex; align-items: center; gap: var(--space-3); margin-top: var(--space-3); }
.mic-meter { flex: 1; max-width: 220px; height: 10px; border-radius: 999px; background: var(--soft); border: 1px solid var(--line); overflow: hidden; }
.mic-meter-fill { height: 100%; width: 0%; background: var(--live-fill); transition: width .05s linear; }
.mic-meter-label { font-size: .78rem; color: var(--muted); white-space: nowrap; }
.mic-meter-label.mic-silent { color: var(--danger-fill); font-weight: 600; }

/* The open-mic safeguard notice: "this is about to stop" before the
   wall-clock cap, and "this stopped, here's why" after either
   safeguard fires. --pending, not --danger, on purpose -- nothing has
   gone wrong, you're just being told before it happens instead of
   after. Wraps rather than nowrapping: the button has to stay
   reachable on a phone, and the sentence is longer than the row. */
.extend-banner {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-3); padding: var(--space-2) var(--space-3);
  border: 1px solid var(--pending); border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--pending) 8%, transparent);
  color: var(--pending); font-size: .85rem; font-weight: 600;
}
.extend-banner .button { flex: 0 0 auto; }

/* Per-session list block. The head row keeps the list's name and its
   "open the full list" escape hatch on one line, wrapping instead of
   squashing the button on a phone. */
.session-list-panel { margin-bottom: var(--space-3); }
.session-list-panel:last-child { margin-bottom: 0; }
.session-list-head {
  display: flex; align-items: center; flex-wrap: wrap;
  justify-content: space-between; gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.session-list-head h3 { margin: 0; }

/* The "N days left" figure on a trash row, once it's close enough to
   matter. --danger only in the last few days: before that it's neutral
   information, not a warning. */
.trash-expiring { color: var(--danger-fill); font-weight: 600; }

/* On-the-record facts. The kind tag is a label, not a status badge --
   it says what sort of thing this is, so it stays quiet and lets the
   fact itself carry the weight. */
.fact-row .record-main { align-items: flex-start; }
.fact-text { margin: 0; }
.fact-kind {
  display: inline-block; margin-right: var(--space-2); padding: 1px 7px;
  border: 1px solid var(--line); border-radius: 999px; background: var(--soft);
  font-size: .68rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted); vertical-align: 1px;
}
.fact-kind-decision { color: var(--accent); border-color: var(--accent); }
.fact-kind-figure { color: var(--live); border-color: var(--live); }
/* --pending, not --danger: the fact isn't wrong, we just heard it badly.
   Red would read as "this is an error" rather than "check this one". */
.fact-unsure {
  margin-left: var(--space-2); padding: 1px 7px; border-radius: 999px;
  border: 1px solid var(--pending); color: var(--pending);
  font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
}

/* Session-level "we couldn't hear this well" notice. Sits with the
   session's title because it qualifies everything below it, and there is
   no point letting someone read a page of confident notes before finding
   out the audio was mostly unintelligible. */
/* A session card being hovered as a merge target. Deliberately distinct
   from .drop-target (the column's "move to this bucket" state) -- the
   two drops are inches apart and mean very different things, one
   reversible and one not. */
a.session-card.panel.merge-target {
  position: relative;
  outline: 2px solid var(--accent); outline-offset: -1px;
  background: color-mix(in srgb, var(--accent) 10%, var(--surface));
}
a.session-card.panel.merge-target::after {
  content: 'Merge into this'; position: absolute; inset-inline: 0; bottom: 0;
  padding: 3px 0; text-align: center; font-size: .7rem; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase;
  background: var(--accent-fill); color: #fff;
}

/* "Something is recording" -- on every page except the one where it's
   obvious. Fixed to the viewport rather than placed in the layout: it
   has to be true wherever you've scrolled to, and it is the one thing on
   a page that should survive being ignored. */
.recording-indicator {
  position: fixed; right: var(--space-4); bottom: var(--space-4); z-index: 60;
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: 8px 14px; border-radius: 999px; text-decoration: none;
  background: var(--danger); color: #fff;
  font-size: .78rem; font-weight: 650;
  box-shadow: 0 6px 20px rgba(0, 0, 0, .35);
  max-width: min(340px, calc(100vw - 2 * var(--space-4)));
}
.recording-indicator:hover { filter: brightness(1.08); }
.recording-indicator-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recording-indicator-dot {
  width: 9px; height: 9px; border-radius: 50%; background: #fff; flex-shrink: 0;
  animation: recording-indicator-pulse 1.6s ease-in-out infinite;
}
@keyframes recording-indicator-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) { .recording-indicator-dot { animation: none; } }

/* Brief vs full notes.

   Both are in the DOM and CSS picks; see summary.html for why the state
   lives on <html> rather than in the panel. The headline list is hidden
   in full view rather than removed, so switching back and forth costs
   nothing and never re-renders. */
.headline-points { margin: 0 0 var(--space-3); padding-left: 1.15rem; }
.headline-points li { margin-bottom: var(--space-2); line-height: 1.55; }
:root[data-notes="brief"] .notes-full-only { display: none; }
:root:not([data-notes="brief"]) .headline-points { display: none; }
/* Decisions ride with the summary, and only there -- in the full notes
   the same motion and vote badges already sit on their own groups, and
   showing both would be the same fact twice on one screen. */
:root:not([data-notes="brief"]) .decision-strip { display: none; }
.decision-strip {
  margin: 0 0 var(--space-3); padding-top: var(--space-2);
  border-top: 1px solid var(--line);
}
.decision-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
  margin: 0 0 var(--space-2); font-size: .84rem;
}
.decision-subject { font-weight: 650; color: var(--ink); }

.panel-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
/* Deliberately the same affordance as .chunks-section-toggle's summary:
   muted, small, bold, an arrow pointing the way it goes. It sits beside
   those toggles on the same page and there is no reason for it to look
   like a different kind of control. */
.notes-toggle-btn {
  border: 0; background: transparent; color: var(--muted);
  font: inherit; font-size: .74rem; font-weight: 700; cursor: pointer;
  padding: 2px 0;
}
.notes-toggle-btn:hover { color: var(--ink); }
/* The label is the ACTION, not the current state -- again matching
   expand/collapse, where the word tells you what clicking does. */
:root:not([data-notes="brief"]) .notes-toggle-btn::after { content: 'summary ›'; }
:root[data-notes="brief"] .notes-toggle-btn::after { content: '‹ full notes'; }

/* The live screen's catch-up box.

   Deliberately the loudest thing on the page while a session is running:
   it's what you look at after glancing away, so it has to be findable
   without searching. The accent border and the pulse do that work; the
   points inside stay ordinary body text, because the box is a place to
   read from, not a thing to admire. */
.current-topic {
  margin: 0 0 var(--space-4); padding: var(--space-3) var(--space-4);
  border: 1px solid var(--accent); border-left-width: 4px;
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--accent) 7%, var(--surface));
}
.current-topic-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2); margin-bottom: var(--space-2);
}
.current-topic-label {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-size: .7rem; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--accent);
}
.current-topic-time { font-size: .72rem; color: var(--muted); }
/* Marks the box as live rather than as a stored note. Stops moving for
   anyone who has asked their system not to animate things. */
.current-topic-pulse {
  width: 7px; height: 7px; border-radius: 50%; background: var(--live);
  animation: current-topic-pulse 2s ease-in-out infinite;
}
@keyframes current-topic-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .35; }
}
@media (prefers-reduced-motion: reduce) {
  .current-topic-pulse { animation: none; }
}
.current-topic-subject {
  margin: 0 0 var(--space-2); font-size: 1.05rem; font-weight: 650;
  line-height: 1.3; color: var(--ink);
}
.current-topic-points { margin: 0; padding-left: 1.1rem; }
.current-topic-points li { margin-bottom: 2px; line-height: 1.5; }
.current-topic-more {
  margin: var(--space-2) 0 0; font-size: .74rem; color: var(--muted);
}

/* A footnote, not an alert. It's on every session by design, so it has
   to sit quietly under the eye rather than compete with the notes it's
   describing -- the amber warning box this replaced was styled for
   something exceptional and appeared every time, which is the fastest
   way to teach someone to skip a line. */
.ai-notice {
  display: flex; align-items: flex-start; gap: var(--space-2);
  margin: 0 0 var(--space-3);
  color: var(--muted); font-size: .76rem; line-height: 1.5;
}

/* Sits under the Ask input as a quiet qualifier on the question, not as
   a form field in its own right -- it modifies what gets searched. */
/* Off-screen but still announced -- for a heading a page needs
   semantically but shouldn't spend vertical space on. Not display:none
   or visibility:hidden, which remove it from the accessibility tree
   too. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}
.ask-archives-label {
  display: inline-flex; align-items: center; gap: var(--space-2);
  margin-top: var(--space-2); font-size: .82rem; color: var(--muted);
}
/* In the dashboard's ask row it's a sibling of the input and button, so
   it needs neither the top margin nor to be pushed to a line of its own. */
.ask-all-panel .ask-archives-label { margin-top: 0; flex: 0 0 auto; white-space: nowrap; }

/* Agenda attachment, meeting sessions only. Textarea and controls stack
   so the paste box gets the full width -- an agenda is a document, not a
   field, and a one-line input would make pasting one feel wrong. */
.agenda-panel { display: block; }
.agenda-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2) var(--space-3); }
.agenda-head .section-hint { margin: 0; }
.agenda-form { display: flex; flex-direction: column; gap: var(--space-2); margin-top: var(--space-2); }
.agenda-form textarea {
  width: 100%; min-height: 62px; resize: vertical; font-family: inherit; font-size: .88rem;
}
.agenda-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.agenda-actions input[type="file"] { flex: 1 1 200px; min-width: 0; font-size: .8rem; color: var(--muted); }
.record-toggle-btn.recording {
  background: var(--danger-fill); color: #fff; border-color: var(--danger-fill);
  animation: record-pulse 1.4s ease-in-out infinite;
}
@keyframes record-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .65; } }

/* captures (screenshots + documents + emails) */
.dropzone {
  border: 2px dashed var(--line); border-radius: var(--radius-md); padding: var(--space-6) var(--space-5); text-align: center;
  margin-bottom: var(--space-5); transition: border-color .15s, background .15s;
}
.dropzone p { margin: 0 0 var(--space-2); }
.dropzone .capture-warning { font-size: .76rem; }
.dropzone.drag-active { border-color: var(--accent); background: var(--soft); }
.capture-dropzone-actions { display: flex; gap: var(--space-2); justify-content: center; flex-wrap: wrap; margin-top: var(--space-3); }
/* free-typed note / pasted link row, inside the same dropzone card --
   text-align:left against the dropzone's centered text, own divider so
   it doesn't read as glued to the drag-and-drop copy above it */
.capture-note-row {
  display: flex; gap: var(--space-2); align-items: flex-end; text-align: left;
  margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px dashed var(--line);
}
.capture-note-row textarea {
  flex: 1; min-width: 0; resize: vertical; font: inherit; padding: var(--space-2) var(--space-3);
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface); color: var(--ink);
}
.capture-note-row .button { flex-shrink: 0; }

/* Compact variant (public_share_view.html's contributor row). The full
   .dropzone is a big drop target because on public_share.html adding
   something IS the page; here the live feed is the content and
   contributing is occasional, so it collapses to a single row. Drag and
   drop plus Ctrl+V still work across it -- capture-dropzone.js binds
   those to this element, not to the explanatory copy that's gone. */
.dropzone-compact {
  padding: var(--space-3); margin-bottom: var(--space-4); border-width: 1px; text-align: left;
}
.dropzone-compact .capture-note-row {
  margin-top: 0; padding-top: 0; border-top: 0; align-items: center; gap: var(--space-2);
}
.dropzone-compact textarea {
  min-height: 40px; padding: var(--space-2) var(--space-3); line-height: 1.4;
}
.capture-pending-compact { margin-bottom: var(--space-4); }
.capture-pending-compact .capture-note-row {
  margin-top: 0; padding-top: 0; border-top: 0; align-items: center;
}
.capture-pending-compact .section-hint { margin: 0 0 var(--space-2); }
.capture-pending-compact input[type="text"] { flex: 1; min-width: 0; margin: 0; }
/* :not(.capture-pending-compact) because these two ID rules (0,1,x) were
   silently beating the class rules written three lines above them for the
   compact variant, which lives on the same element -- the note input kept
   its 8px/12px block margins inside an align-items:center row, sitting off
   centre from the Upload/Cancel buttons and inflating the row ~20px. The
   ID selectors are the thing that's wrong here, so scope them to the full
   dropzone they were actually written for rather than adding !important. */
#capture-pending:not(.capture-pending-compact) { margin-bottom: var(--space-5); }
#capture-pending:not(.capture-pending-compact) input[type="text"] { width: 100%; margin: var(--space-2) 0 var(--space-3); }
#capture-status:not(:empty) { margin-bottom: var(--space-4); }
.screenshot-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--space-4); }
.screenshot-card { border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden; background: var(--surface); }
.screenshot-thumb { width: 100%; height: 150px; object-fit: cover; display: block; background: var(--soft); }
.screenshot-file-link {
  display: flex; align-items: center; gap: var(--space-2); height: 150px; padding: 0 var(--space-4); background: var(--soft);
  color: var(--accent); font-weight: 700; font-size: .85rem; overflow-wrap: anywhere;
}
.screenshot-body { padding: var(--space-3); }
.screenshot-body p { margin: var(--space-2) 0; font-size: .85rem; color: var(--muted); overflow-wrap: anywhere; }
.screenshot-body form { margin-top: var(--space-2); }

/* settings */
.settings-panel { max-width: 620px; }
.danger-zone { border-color: var(--danger); }

@media (max-width: 900px) {
  /* Explicit rows matter, not just the single column: .app-shell has
     min-height: 100vh, and once the page's content is SHORTER than the
     viewport (exactly what the collapsed-buckets mobile board achieves)
     the grid's default stretch hands the leftover height to both auto
     rows -- which inflated the nav bar's row to ~172px of dark
     background for 68px of actual content. auto 1fr pins the nav row
     to its content and gives every spare pixel to the page instead. */
  .app-shell { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
  /* shrunk a lot from the desktop rail's own padding -- a persistent
     top bar on every page should cost as little vertical space as
     possible, not carry the same generous padding a full-height
     sidebar can afford to. */
  .sidebar { position: relative; height: auto; padding-top: var(--space-3); padding-bottom: var(--space-3); }
  .mobile-nav-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); }
  .brand { font-size: 1.05rem; }
  .brand-mark { width: 26px; height: 26px; font-size: .85rem; }
  /* the tagline cost a full row in the persistent collapsed header for
     copy that's purely decorative -- worth showing once the drawer is
     actually open (there's room then), not worth its own row every
     single page load. */
  .brand-subtitle { display: none; margin: var(--space-1) 0 0; }
  .sidebar.mobile-menu-open .brand-subtitle { display: block; margin-bottom: var(--space-4); }
  .mobile-nav-content { display: none; }
  /* Capped height + its own scroll, not "however tall the full nav
     tree happens to be" -- letting a long bucket list push the entire
     page hundreds of pixels down reads as a full-screen takeover even
     though it's structurally a push, not an overlay. Bounding it keeps
     a peek of the actual page visible below and makes the open drawer
     feel like a compact accordion instead. */
  .sidebar.mobile-menu-open .mobile-nav-content {
    display: block; max-height: calc(100vh - 120px); overflow-y: auto; -webkit-overflow-scrolling: touch;
  }
  /* The wrapper above is the scroll container on a phone, so the nav
     must not be one as well. It happens not to scroll today -- in block
     layout it just grows to its content -- but leaving overflow-y:auto
     on it means any future height constraint silently produces two
     nested scroll regions, which is a problem this app has already had
     once with the bucket board. Reset rather than relied upon. */
  .sidebar nav { overflow-y: visible; min-height: auto; }
  /* order:-1 puts the menu button first in the header row, ahead of the
     brand -- reordering the markup instead would put it before the
     brand link for a screen reader too, and "Menu" isn't what should be
     announced first on the page. The header is display:contents outside
     this breakpoint, so this can't affect desktop. */
  .mobile-menu-button {
    order: -1;
    display: inline-flex; align-items: center; gap: var(--space-2); border: 1px solid var(--nav-line);
    border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3); background: var(--nav-soft); color: #fff;
    font: inherit; font-size: .82rem; font-weight: 700; cursor: pointer;
  }
  .sidebar.mobile-menu-open .sidebar-footer { margin-top: var(--space-5); }
  .main-content {
    padding-top: var(--space-4); padding-left: var(--space-5);
    padding-right: max(var(--space-5), env(safe-area-inset-right));
    padding-bottom: max(var(--space-6), env(safe-area-inset-bottom));
  }
  .list-card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* the board's side-scrolling fixed-width columns (each with its own
     capped-height internal scroll) work fine with a mouse but cramp badly
     on a phone -- stack buckets full-width instead, one after another,
     and let the page itself scroll rather than each column separately.
     Column-header dragging to reorder is mouse-only (HTML5 drag-and-drop
     has no touch equivalent) so it just quietly stops doing anything on
     touch here; reordering from a mouse elsewhere still works. */
  .bucket-board { flex-direction: column; overflow-x: visible; }
  /* flex-basis reset matters: the base rule's flex: 0 0 340px meant
     "340px wide" on the desktop board's horizontal axis, but with the
     board flipped to column direction the same basis becomes "340px
     TALL" -- invisible while session cards filled the column anyway,
     but a giant empty panel once the collapsed-bucket rule below hides
     the body. auto lets a collapsed column shrink to its header. */
  .bucket-column { width: 100%; max-height: none; flex: 0 0 auto; min-width: 0; max-width: none; }
  /* The column keeps its own scrollport on desktop because that's what
     makes a kanban board work -- columns scroll independently, the board
     doesn't. Stacked on a phone that's just a second scroll region
     inside the page's: open a bucket and you get a short window you have
     to scroll within, while the page scrolls around it. One scroll
     region here; the page's. */
  .bucket-column-body { max-height: none; overflow-y: visible; }
  .bucket-column-header { flex-wrap: wrap; row-gap: var(--space-2); }

  /* Coming from mobile, the "All sessions" board -- every bucket you
     belong to, each one's sessions fully expanded -- is a lot of
     scrolling before you've actually picked what you came to look at.
     Collapse to just the bucket headers (tap a name to open that one
     bucket's sessions via the existing ?bucket= filter) whenever more
     than one column is actually on the board; a single already-
     filtered bucket (only one .bucket-column present) keeps showing
     its sessions directly, same as before. */
  .bucket-board:has(.bucket-column ~ .bucket-column) .bucket-column-body { display: none; }
  .bucket-board:has(.bucket-column ~ .bucket-column) .bucket-column-title-link::after {
    content: '›'; color: var(--muted); font-weight: 400; font-size: 1.1em;
  }
  /* The open state is a board filtered to ONE column, so the collapsed
     rule above (which needs a sibling column) can't reach it -- it needs
     its own marker, pointing back the way you came. Without this there
     was no on-screen way out of a bucket at all on a phone. */
  .bucket-column-title-link.is-open::after {
    content: '‹ all'; color: var(--muted); font-weight: 600;
    font-size: .72rem; letter-spacing: .04em; text-transform: uppercase;
    margin-left: var(--space-2);
  }
  /* the collapsed bucket's own glanceable preview -- 3 most recent
     session titles (see dashboard.html), shown ONLY in this
     multi-bucket collapsed state; the expanded single-bucket view and
     desktop both show the real session cards instead */
  .bucket-board:has(.bucket-column ~ .bucket-column) .bucket-column-preview { display: block; }
  /* align-items:center, NOT baseline: .bucket-preview-title sets
     overflow:hidden for its ellipsis, which makes it a scroll container,
     and a scroll container has no usable first baseline -- flexbox
     synthesises one from its bottom border edge, so the timestamp aligned
     to the title's bottom and sat ~4px low. With one line of text on each
     side, centre is what baseline was reaching for anyway.
     min-height because this row and the bucket title link are the only two
     routes into a bucket's sessions in this collapsed state, and neither
     was ever picked up by the coarse-pointer blocks (they came out ~29px
     and ~27px next to 44px siblings). */
  .bucket-preview-row {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
    padding: var(--space-1) 0; text-decoration: none; min-width: 0; min-height: 44px;
  }
  .bucket-column-title-link { min-height: 44px; flex: 1 1 auto; }
  .bucket-preview-title {
    color: var(--ink); font-size: .85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
  }
  /* the "live" badge is its own flex item now rather than trailing content
     inside the truncating title span, where text-overflow simply clipped
     it out of existence (ellipsis substitutes at the clip edge, it doesn't
     preserve what follows) -- on the one screen whose whole job is showing
     what's active right now */
  .bucket-preview-row .badge { flex-shrink: 0; }
  /* margin-left:auto now that the badge makes this a THREE-item row:
     auto margins absorb the free space before justify-content runs, so
     the name and its badge stay packed together on the left and the
     timestamp stays hard right -- space-between alone would have stranded
     the badge in the middle of the row whenever the name was short. */
  .bucket-preview-time { color: var(--muted); font-size: .72rem; font-family: var(--font-mono); flex-shrink: 0; margin-left: auto; }

  /* Ask bar collapses into the sparkle icon button in the actions row
     (see #ask-all-toggle-btn in dashboard.html) -- a full-width search
     field costs a whole row of a phone screen for a feature that's
     reached-for, not constant. .open restores the exact same inline
     bar, nothing about the form itself changes. */
  #ask-all-toggle-btn { display: inline-flex; }
  .ask-all-panel { display: none; }
  .ask-all-panel.open { display: block; }
}
@media (max-width: 650px) {
  .list-card-grid { grid-template-columns: 1fr; }
  .page-header, .record, .session-detail-header { align-items: stretch; flex-direction: column; }
  /* The two rules that let the ask bar share the header row on desktop
     have to be undone here, and at matching :has() specificity or the
     plain .page-header rule above loses to them. flex-basis is the real
     hazard: 380px means "380px wide" on the desktop header's row axis
     but "380px TALL" once this block turns it into a column -- measured
     the board starting 551px down a 375px screen, with the ask input
     squeezed to 89px because align-items no longer stretched it. */
  .page-header:has(.ask-all-panel) { align-items: stretch; }
  .page-header:has(.ask-all-panel) > div:first-child { flex: 0 0 auto; }
  /* On a phone the ask row can't hold input + button + the archives
     chip: the chip is nowrap by design (its "(3)" count shouldn't break
     off its label) and won a fight it shouldn't have been in, squeezing
     the input to 90px. Wrap instead, chip on its own line -- there's a
     whole row's worth of width for it once it stops competing. */
  .ask-all-panel .ai-ask-form { flex-wrap: wrap; }
  .ask-all-panel #ask-all-input { flex: 1 1 180px; }
  .ask-all-panel .ask-archives-label { flex: 1 1 100%; }
  /* the desktop-scale 2.3rem page title plus its stacked subtitle and
     margins cost ~160px of a phone screen before any actual content --
     on mobile a page title is a label, not a billboard */
  .page-header { gap: var(--space-3); margin-bottom: var(--space-4); }
  h1, .page-header h1 { font-size: 1.45rem; margin: 0; }
  .page-header p { font-size: .86rem; }
  /* decorative-only subtitles ("Every session you've captured...")
     don't earn a row on a phone -- informative ones (the admin
     viewing_user banner with its back link) keep rendering, they
     don't carry this class */
  .page-subtitle-decorative { display: none; }
  .main-content {
    padding-top: var(--space-4); padding-left: var(--space-4);
    padding-right: max(var(--space-4), env(safe-area-inset-right));
    padding-bottom: max(var(--space-5), env(safe-area-inset-bottom));
  }
  .panel { padding: var(--space-4); }
  /* the header stacks to a full-width column here, and under a coarse
     pointer the primary button plus three 44px icon buttons plus 3x12px of
     gap overflow a phone row -- orphaning the last icon (#ask-all-toggle-
     btn, the ONLY route to the ask bar on mobile) onto a line of its own.
     8px gaps buy back the 12px that clears it at 375px. */
  .new-record-widget { gap: var(--space-2); }
  .ai-ask-form { flex-direction: column; }
  /* stacked column => cross axis is horizontal => the default stretch made
     the blockified button full width, so a secondary "Correct" outweighed
     every real primary action on the panel. Matches what .form-grid-edit
     and .chunk-edit-form already do for their own stacked buttons. */
  .ai-ask-form > .button { align-self: flex-start; }
  /* except the ask-all bar, which is pinned to a row at every width -- an
     align-self on the item would otherwise beat that row's own
     align-items:center and top-align its icon submit again */
  .ask-all-panel .ai-ask-form > .button { align-self: center; }
  .chat-question { max-width: 100%; }
  .chat-answer-row { grid-template-columns: 26px minmax(0, 1fr); gap: var(--space-2); }
  .chat-avatar { width: 26px; height: 26px; font-size: .68rem; }
  .bucket-start-popover, .bucket-ask-popover { margin: -1px 0 0; border-radius: 0; }
  /* .bucket-manual-form/.bucket-ask-form flex-direction:column used to be
     set here only -- both are column at every width now (see their base
     rules), so nothing mobile-specific is needed for them any more. */
  /* the login/share card's generous desktop padding (--space-7 around the
     card, --space-8 inside it) leaves very little room for the card's own
     content on a small phone (an iPhone SE at 375px, or narrower) --
     tighten both so the form doesn't feel squeezed against its own edges */
  .login-main { padding: var(--space-4); }
  .login-card { padding: var(--space-6); }
}

/* additional touch-target sizing -- kept in its own block at the very end
   of the file (rather than folded into the earlier `pointer: coarse` rule
   above) since several of these selectors already carry an explicit
   min-height/padding declaration earlier in this file with the exact same
   specificity; a media-query wrapper doesn't add specificity, so putting
   the override earlier in source order would have silently lost the
   cascade tie to that later, unconditional declaration. Living last
   guarantees it wins on a touch device without needing to raise
   specificity artificially. */
@media (pointer: coarse) {
  .mobile-menu-button, .theme-toggle { min-width: 44px; min-height: 44px; }
  /* small/icon-only buttons and disclosure summaries are tapped often
     (Save/Delete rows, expand-a-chunk/list-item/contact) -- comfortable
     at their compact size under a mouse pointer, cramped under a
     fingertip */
  .button.small, button.small { min-height: 44px; }
  /* icon-only buttons carry no text to grow the hit area, so they land
     well under a fingertip's minimum at their drawn size -- the
     contributor row's attach/send pair and the ask-all submit are all
     primary actions on touch */
  .icon-button { min-width: 44px; min-height: 44px; }
  /* ...which leaves the note field beside them at its drawn 40.4px in a
     row that should read as one flush instrument strip. Grow the field to
     the touch target rather than switching the row to stretch --
     .icon-button has a definite height, so stretch wouldn't apply to it
     and under a mouse it would top-align instead. */
  .dropzone-compact textarea { min-height: 44px; }
  /* the order toggle is the one control on an otherwise read-only
     screen, so it has to be comfortably tappable there of all places */
  .view-order-switch { min-height: 44px; padding-left: var(--space-3); padding-right: var(--space-4); }
  /* the per-point correct/delete controls are small marks by design
     (they sit inline inside a sentence), so grow the tap area without
     growing the drawn button */
  .key-point-correct-btn, .key-point-delete-btn, .key-point-save-btn,
  .key-point-cancel-btn, .key-point-group-delete-btn {
    min-width: 34px; min-height: 34px;
  }
  .nav-link { padding-top: var(--space-3); padding-bottom: var(--space-3); }
  .list-item-edit > summary, .chunk-disclosure > summary, .chunks-section-toggle > summary {
    min-height: 44px;
  }
}
