/* ===================================================================
   wired -- Limestone Technology Group design language.

   Source of truth: Limestone_Technology_Group_Company_Style_Guide.pdf
   (Sept 2026 v1.0), as transcribed for Limestone Records. Sibling apps:
   Records (post-guide, cool neutrals -- this one), Administrative and
   Dashboard (pre-guide, warm canvas -- deliberately NOT matched here).

   Two rules the guide is emphatic about, and that this file obeys:
     1. Orange is SIGNAL. ~5% of a composition, never decorative, and it
        never carries white text (navy on orange is 5.80:1; white is 2.2:1).
     2. Status is never conveyed by colour alone -- every status colour is
        paired with a text label, and every badge keeps its dot AND its word.

   ORDERING WARNING: the cascade in this file is load-bearing in several
   places (see the `pointer: coarse` block near the end, `.mobile-nav-header`,
   `button.checkbox`, `.key-points-ordered li::before`). Do not reorder rules
   and do not append anything after the final `pointer: coarse` block.
   =================================================================== */
:root {
  color-scheme: light;
  --font-sans: 'Sora', Aptos, 'Segoe UI', Arial, sans-serif;
  /* The trailing generic is load-bearing: without it, a platform that has
     none of the named faces (most Android and Linux) falls back to the
     default PROPORTIONAL family rather than a monospace one, silently
     un-monospacing every transcript, the console, and all tabular readouts. */
  --font-mono: ui-monospace, "Cascadia Mono", "Cascadia Code", Consolas, "SFMono-Regular", monospace;

  --ink: #183149;      /* body text */
  --muted: #51677a;    /* "Slate" -- secondary TEXT. 5.33:1 on white. */
  --steel: #7d8b94;    /* borders, decorative icons, disabled. 3.50:1 --
                          passes non-text contrast, NEVER use for body text. */
  --line: #dce5ed;
  --surface: #ffffff;
  --top: #ffffff;      /* topbar -- same as --surface in light (Stone & Sky
                          has no distinct "top" layer; dark mode does) */
  --soft: #f3f7fa;     /* "Cloud" -- the page ground behind --surface panels */

  /* The sidebar is fixed brand navy in BOTH themes -- a deliberate "the
     chassis doesn't change colour when you turn the lights on" choice, and
     the same call Administrative makes. Only the CONTENT area flips.
     Everything under this comment is therefore theme-independent. */
  --nav: #102b46;           /* Midnight Navy -- the brand's primary */
  --nav-raised: #1a3d60;    /* hover surface ON the navy */
  --nav-raised-2: #234d75;  /* second step: badges, hover on rail buttons */
  --nav-selected: #254D6C;  /* the current destination (guide v2 p14) -- never the hover fill */
  --nav-line: rgba(255, 255, 255, .12);
  --nav-ink: #f8fafb;
  --nav-muted: #8fb9d8;     /* soft blue -- support type on navy, 6.96:1 */

  /* SIGNAL. Budgeted: the active nav marker and the account avatar. That is
     the whole list. Adding a third use dilutes the two that matter -- if
     something needs emphasis, it wants weight or --primary, not orange. */
  --orange: #ff8200;

  /* 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.

     Each role is a TRIPLET: the foreground (>=4.5:1 on its own -bg), the
     tinted background, and the dot (>=3:1 on white). The dot is NOT the
     foreground -- a dot only has to clear the 3:1 non-text bar, so it can
     stay saturated where the text has to go dark to stay readable. */
  --info: #23577a;     /* info / neutral action -- links, secondary actions */
  --live: #1b6b43;     /* live / confirmed / good -- the "* live" signal */
  --pending: #7a4400;  /* pending / needs attention */
  --danger: #a12a2a;   /* error / destructive only, never decorative */
  --neutral: #536674;  /* no status / draft / dismissed */

  --info-bg: #e7f1f8;     --info-dot: #3d7ba8;
  --live-bg: #e3f2ea;     --live-dot: #2e8b57;
  --pending-bg: #fff3e0;  --pending-dot: #c46a00;
  --danger-bg: #fceaea;   --danger-dot: #c93b3b;
  --neutral-bg: #edf3f7;  --neutral-dot: #7d8b94;

  /* "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). --info/--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. */
  /* --primary is the DOMINANT action fill (primary buttons, text selection,
     the checked checkbox, the toggle track), and it is brand navy -- NOT
     --info. These used to be one token doing two unrelated jobs. A link and
     a "do the thing" button are different roles; keep them apart.

     Note each fill is the role's FOREGROUND value, never its dot: white on
     --live-dot (#2e8b57) is 3.4:1 and fails, white on --live-fill is 6.1:1. */
  --primary: #102b46;
  --primary-hover: #0b1f33;
  --primary-ink: #ffffff;   /* text/icon ON --primary. 14.4:1 here. */
  --live-fill: #1b6b43;
  --pending-fill: #7a4400;
  --danger-fill: #a12a2a;

  /* Height of the sticky topbar, referenced by anything that has to
     subtract the chrome above it (the board's capped columns). */
  --topbar-h: 53px;


  /* ---- type scale --------------------------------------------------
     The guide (p7) defines SIX steps, and its digital sizes map onto a 16px
     root exactly:
       DISPLAY 48/56 · TITLE 32/38 · SECTION 24/30 · SUBSECTION 18/24 ·
       BODY/UI 16/26 · LABEL 11/16
     Display is unused here -- wired has no hero surface -- so it isn't
     declared; a token with no consumer is just another thing to keep true.

     --text-ui and --text-ui-sm are EXTENSIONS BELOW the guide's single
     BODY/UI step, which is 16/26 for both body copy and interface text. That
     one step is right for documents, proposals and marketing pages; it is too
     loose for an application's chrome, and the guide says so itself on p12:
     "Product and marketing interfaces use the same tokens but different
     density." These two rungs are that density, and nothing else.

     Before this pass there were 35 distinct font-size literals across 135
     declarations -- .84 beside .85 beside .86 beside .87, with nothing to say
     which was right. 52 of them, the largest single band, sat between .6875
     and .875: row meta, counts, the nav rail. That band is what --text-ui-sm
     names. Do not add a third rung without the same kind of reason. */
  /* Headings follow Style Guide v2.0 (p11): page title 28/36, section
     20/28, subsection 17/24. v1's 32/24/18 steps are gone. */
  --text-title: 1.75rem;       --text-title-lh: 1.2857;  /* page h1: 28/36 */
  --text-section: 1.25rem;     --text-section-lh: 1.4;   /* 20/28 */
  --text-subsection: 1.0625rem; --text-subsection-lh: 1.4118; /* 17/24 */
  --text-body: 1rem;           --text-body-lh: 1.625;
  --text-ui: .875rem;          --text-ui-lh: 1.4286;
  --text-ui-sm: .8125rem;      --text-ui-sm-lh: 1.3846;
  --text-label: .6875rem;      --text-label-lh: 1.4545;

  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-lg: 12px;  /* guide: cards are 12 */
  --radius-xl: 16px;

  /* 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 */

  /* guide: low contrast, large blur, navy-tinted -- shadow suggests depth,
     it never draws an edge. Edges are --line's job. */
  --shadow: 0 1px 2px 0 rgb(16 43 70 / .04);
  --shadow-raised: 0 8px 24px -8px rgb(16 43 70 / .12);
  --shadow-overlay: 0 24px 64px -16px rgb(16 43 70 / .24);
}
/* 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;
    /* Dark is the same room with the lights off, not a different product.
       The chrome tokens (--nav*, --orange) deliberately do NOT appear here:
       the sidebar is brand navy in both themes.

       Elevation ladder, darkest to lightest:
         --soft #0a1b2c  page ground
         --nav  #102b46  the fixed chrome
         --surface       panels, one step ABOVE the chrome
       --surface must never equal --nav. They meet on two screens -- the
       login card sits directly on a --nav ground, and every panel butts up
       against the sidebar -- and if the two match, the card and the seam
       simply disappear, leaving a 1px border as the only separation. */
    --ink: #e8eff5;
    --muted: #b7c9d8;
    --steel: #6f8798;
    --line: #36526a;
    --surface: #1c3750;
    /* Topbar sits between --nav and --surface -- a third distinct layer
       ("Layered Midnight"), not just --surface reused. Using the mockup's
       own "surface" value (#162c42) directly here would have put it
       almost flush with --nav (#102b46), which is exactly the seam bug
       the comment above warns about -- #1c3750 (their "raised" value)
       keeps the documented safety margin while still deepening the tone. */
    --top: #13283d;
    --soft: #0c1725;

    /* Brightened so they stay legible as TEXT on --surface (each >=5.6:1);
       the -fill counterparts above stay dark because they carry white. */
    /* Navy is the brand's primary, but a navy fill on a navy panel is a
       1.30:1 rectangle -- the label stays readable while the BUTTON stops
       reading as a button, which fails SC 1.4.11 on the control's own
       boundary. So in dark the primary fill inverts to soft blue carrying
       navy text: 6.20:1 against the panel, 6.96:1 for its own label, and it
       reuses the exact pairing the brand mark already uses on the navy rail.
       This is the same fill-tokens-are-tuned-for-their-pairing rule the
       block above describes, applied to --primary. */
    --primary: #8fb9d8;
    --primary-hover: #a9c5dd;
    --primary-ink: #102b46;

    --info: #8fb9d8;
    --live: #5fc98d;
    --pending: #f0a955;
    --danger: #ff8a80;
    --neutral: #9db3c4;

    /* Mixed against the live --surface rather than hand-picked, so a future
       change to --surface carries the whole set with it. The dot equals the
       foreground here: on a dark tint there is no 3:1-vs-4.5:1 split to
       exploit, and a separate darker dot would only lose contrast. */
    --info-bg: color-mix(in srgb, var(--info) 16%, var(--surface));
    --live-bg: color-mix(in srgb, var(--live) 16%, var(--surface));
    --pending-bg: color-mix(in srgb, var(--pending) 16%, var(--surface));
    --danger-bg: color-mix(in srgb, var(--danger) 16%, var(--surface));
    --neutral-bg: color-mix(in srgb, var(--neutral) 14%, var(--surface));
    --info-dot: var(--info);
    --live-dot: var(--live);
    --pending-dot: var(--pending);
    --danger-dot: var(--danger);
    --neutral-dot: var(--neutral);

    /* A navy-tinted shadow is invisible on a near-black ground -- dark mode
       needs real black at real opacity or panels read as flat. */
    --shadow: 0 1px 2px rgba(0, 0, 0, .5);
    --shadow-raised: 0 8px 24px -8px rgba(0, 0, 0, .6);
    --shadow-overlay: 0 24px 64px -16px rgba(0, 0, 0, .7);
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  /* Dark is the same room with the lights off, not a different product.
     The chrome tokens (--nav*, --orange) deliberately do NOT appear here:
     the sidebar is brand navy in both themes.

     Elevation ladder, darkest to lightest:
       --soft #0a1b2c  page ground
       --nav  #102b46  the fixed chrome
       --surface       panels, one step ABOVE the chrome
     --surface must never equal --nav. They meet on two screens -- the
     login card sits directly on a --nav ground, and every panel butts up
     against the sidebar -- and if the two match, the card and the seam
     simply disappear, leaving a 1px border as the only separation. */
  --ink: #e8eff5;
  --muted: #b7c9d8;
  --steel: #6f8798;
  --line: #36526a;
  --surface: #1c3750;
  --top: #13283d;
  --soft: #0c1725;

  /* Brightened so they stay legible as TEXT on --surface (each >=5.6:1);
     the -fill counterparts above stay dark because they carry white. */
  /* Navy is the brand's primary, but a navy fill on a navy panel is a
     1.30:1 rectangle -- the label stays readable while the BUTTON stops
     reading as a button, which fails SC 1.4.11 on the control's own
     boundary. So in dark the primary fill inverts to soft blue carrying
     navy text: 6.20:1 against the panel, 6.96:1 for its own label, and it
     reuses the exact pairing the brand mark already uses on the navy rail.
     This is the same fill-tokens-are-tuned-for-their-pairing rule the
     block above describes, applied to --primary. */
  --primary: #8fb9d8;
  --primary-hover: #a9c5dd;
  --primary-ink: #102b46;

  --info: #8fb9d8;
  --live: #5fc98d;
  --pending: #f0a955;
  --danger: #ff8a80;
  --neutral: #9db3c4;

  /* Mixed against the live --surface rather than hand-picked, so a future
     change to --surface carries the whole set with it. The dot equals the
     foreground here: on a dark tint there is no 3:1-vs-4.5:1 split to
     exploit, and a separate darker dot would only lose contrast. */
  --info-bg: color-mix(in srgb, var(--info) 16%, var(--surface));
  --live-bg: color-mix(in srgb, var(--live) 16%, var(--surface));
  --pending-bg: color-mix(in srgb, var(--pending) 16%, var(--surface));
  --danger-bg: color-mix(in srgb, var(--danger) 16%, var(--surface));
  --neutral-bg: color-mix(in srgb, var(--neutral) 14%, var(--surface));
  --info-dot: var(--info);
  --live-dot: var(--live);
  --pending-dot: var(--pending);
  --danger-dot: var(--danger);
  --neutral-dot: var(--neutral);

  /* A navy-tinted shadow is invisible on a near-black ground -- dark mode
     needs real black at real opacity or panels read as flat. */
  --shadow: 0 1px 2px rgba(0, 0, 0, .5);
  --shadow-raised: 0 8px 24px -8px rgba(0, 0, 0, .6);
  --shadow-overlay: 0 24px 64px -16px rgba(0, 0, 0, .7);
}
* { 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: var(--text-body-lh);
  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(--primary); color: var(--primary-ink); }
:focus-visible { outline: 2px solid var(--info); outline-offset: 2px; border-radius: var(--radius-sm); }
/* --info on the navy rail is 2.21:1 -- it fails SC 1.4.11 (3:1 for non-text)
   outright, so keyboard focus is effectively invisible in the sidebar. White
   is 14.4:1 there. Put .on-navy on any permanently-dark surface.
   The offset goes NEGATIVE inside the rail: nav rows sit 1-2px apart, so an
   outward ring overlaps its neighbours instead of framing its own row. */
.on-navy :focus-visible { outline-color: #fff; outline-offset: -2px; }
* { 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(--steel); }

/* 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;
  /* Smooth, not a snap: animating the grid track itself (rather than
     toggling display/width) is what lets the collapse/expand rail
     glide, the same reasoning the mobile drawer's own transform
     transition already relies on (see .sidebar below, and the mobile
     media query's own note on why transform/grid over display). */
  transition: grid-template-columns .32s cubic-bezier(.4, 0, .2, 1);
}
.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;
  /* Midnight Navy, kept in family -- a straight-down depth gradient rather
     than a flat fill, lighter at the top where the brand mark sits, deeper
     toward the bottom. Fixed in both themes ("Layered Midnight"/"Stone &
     Sky" share one sidebar -- see the theme-tokens comment near :root). */
  background: linear-gradient(180deg, #1b3a58 0%, #14324e 44%, var(--nav) 77%, #0a1b2d 100%);
  color: var(--nav-ink);
  border-right: 1px solid var(--nav-line);
  transition: padding .32s cubic-bezier(.4, 0, .2, 1);
}
/* 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. */
/* overflow-x explicit, not left to default: `visible` computes to `auto`
   here anyway once overflow-y is set (a CSS quirk -- the pair can't have
   `visible` on one axis and a scrolling value on the other), so this rail
   silently had a horizontal scrollbar available the whole time. It only
   showed itself for the width-transition's own duration -- see the
   collapse/expand comment below -- but explicit `hidden` is correct
   regardless: the rail is never meant to scroll sideways. */
.sidebar nav { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; }
.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-raised-2) transparent; }
.sidebar nav::-webkit-scrollbar { width: 6px; }
.sidebar nav::-webkit-scrollbar-thumb { background: var(--nav-raised-2); 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: var(--text-subsection); font-weight: 700; letter-spacing: -.01em; }
/* The approved Limestone cornerstone mark, reversed for the navy rail -- an
   image file, not a drawn tile. The guide's first non-negotiable is "use an
   approved logo file; never rebuild or redraw the mark", and its logo-protection
   page adds "do not decorate: no shadows, glows, outlines, bevels, or boxes" --
   so no tile, no border, no radius behind it. The product is named in the text
   beside it, which is the same lockup Administrative and Records use. */
.brand-mark { width: 30px; height: 30px; flex: 0 0 auto; display: block; }
/* min-width:0 + nowrap/ellipsis on both lines, even though the rail is
   always wide enough at rest to show them in full: the collapse/expand
   toggle un-clips this instantly while the rail's own width is still
   mid-transition (see .sidebar-collapse-btn's comment), and for that
   moment the box genuinely is narrower than "Limestone"/"Technology
   Group". Without this they wrapped to two lines apiece for the
   transition's duration -- a visible glitch, not a real 264px overflow. */
.brand-text { display: grid; line-height: 1.15; min-width: 0; }
.brand-text b, .brand-text small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.brand-text b { font-weight: 700; font-size: var(--text-subsection); letter-spacing: -.01em; }
.brand-text small { font-size: var(--text-label); font-weight: 500; color: var(--nav-muted); letter-spacing: .01em; }
/* Which app this is, on its own line under the company -- the third line of
   a lockup that is otherwise identical across the Limestone apps. */
.brand-app {
  display: block; padding: var(--space-3) 0 var(--space-2);
  color: #fff; text-decoration: none;
  font-size: var(--text-subsection); font-weight: 700; letter-spacing: -.01em;
  /* Same reasoning as .brand-text/.nav-link-label above: safe at rest, but
     the collapse/expand toggle un-clips this instantly while the rail is
     still mid-transition, and a block element with no shrink rule wraps
     "wired" mid-word for that moment instead of just clipping. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.brand-app:hover { color: var(--nav-ink); }
.sidebar-collapse-btn {
  /* desktop-only -- the mobile drawer has its own close button (below),
     and a rail-collapse control doesn't mean anything on an overlay that
     is either fully open or fully off-screen. */
  display: none; border: 1px solid var(--nav-line); background: var(--nav-raised); color: var(--nav-ink);
  width: 26px; height: 26px; border-radius: var(--radius-sm); cursor: pointer; align-items: center; justify-content: center;
}
/* The drawer's own dismiss control. Hidden at desktop widths for the same
   reason .sidebar-collapse-btn is hidden below 901px in reverse -- each rail
   mode gets the one control that means something in it, not both stacked
   in the same corner. */
.sidebar-close-btn {
  display: none; border: 1px solid var(--nav-line); background: var(--nav-raised); color: var(--nav-ink);
  width: 26px; height: 26px; border-radius: var(--radius-sm); cursor: pointer; align-items: center; justify-content: center;
}
.sidebar-close-btn:hover { background: var(--nav-raised-2); color: #fff; }
@media (max-width: 900px) {
  .sidebar-close-btn { display: inline-flex; }
}
/* Hidden by default, UNCONDITIONALLY -- not inside the mobile media query,
   where it used to live. This is a real DOM element (a sibling of .sidebar
   inside .app-shell, which is display:grid), and a bare <div> with no
   display rule at all defaults to the UA stylesheet's display:block. At
   desktop widths, where none of this element's other rules match either,
   that made it a THIRD grid item alongside .sidebar and .content-column --
   .app-shell has no third column to put it in, so grid auto-placement
   pushed .content-column into the next cell down, off the bottom of the
   sidebar's own row, at the sidebar's column width. The whole desktop page
   past the rail went blank because of an empty div nobody could see. */
.mobile-nav-backdrop { display: none; }
/* Default: children (just the brand link, normally) flow straight into
   the sidebar's own vertical stack. Has to come BEFORE the desktop
   override below -- same selector, same specificity, so whichever is
   later in the file wins regardless of the @media condition; declaring
   this after the override silently cancelled it on every viewport. */
.mobile-nav-header { display: contents; }
@media (min-width: 901px) {
  .sidebar-collapse-btn { display: inline-flex; }
  /* 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-raised-2); color: #fff; }
.sidebar-collapse-btn svg { transition: transform .15s ease; }

/* 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: 2px; align-content: start; }
.nav-section-header {
  margin: var(--space-3) var(--space-3) var(--space-1); color: var(--nav-muted); font-size: var(--text-label); font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;
  /* Same collapse/expand mid-transition reasoning as .brand-app above --
     "MY WORK"/"SPACES"/"WORKSPACE" wrapped onto two lines for that moment
     without this. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nav-section-header:first-child { margin-top: 0; }
/* The mockup's hairlines: the two rules that fence the administrative tail
   (Manage / Settings) off from the primary groups above it. Drawn on the
   group that FOLLOWS the break so an empty group never leaves a stray rule. */
.nav-rule {
  height: 1px; background: var(--nav-line); margin: var(--space-3) var(--space-2);
  border: 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. Tightened again (8px -> 6px vertical)
   once a longer Workspace list plus several buckets made even the
   "tight, even rhythm" version above run past one screen. */
.nav-link {
  padding: .375rem var(--space-3); color: var(--nav-muted); text-decoration: none; border-radius: var(--radius-sm);
  font-weight: 600; font-size: var(--text-ui); display: flex; align-items: center; transition: background-color .1s ease, color .1s ease;
}
.nav-link:hover { background: var(--nav-raised); color: #fff; }
/* "You are here", in three agreeing signals: a raised surface, a white
   label, and an orange marker.

   This used to be a solid --primary block, on the argument that it matched
   the primary button. That argument no longer holds -- --primary is now brand
   navy, and a navy block on a navy rail is invisible. More importantly, the
   raised surface CANNOT carry the state on its own: --nav-selected on --nav
   is 1.62:1, a surface rather than a signal. The orange marker (3.58:1 on
   the selected fill) and the white label are what actually communicate.
   The fill is the guide's own current-destination value, deliberately NOT
   the hover fill (--nav-raised): "a hover fill alone is not a selected
   state", and sharing one colour made hovering a row look like choosing it.

   The marker is an inset box-shadow rather than a border-left on purpose: a
   real border participates in layout and would shift the icon 3px, knocking
   every active row out of alignment with its inactive siblings. (The old
   comment here said "a >1px colored edge on a list row is a flat no" -- that
   was about a border drawn AROUND a row, which is still true. A single-edge
   marker bar is a different thing and is what the house style uses.) */
.nav-link.active {
  background: var(--nav-selected); color: #fff;
  box-shadow: inset 3px 0 0 var(--orange);
}
.nav-link.active:hover { background: color-mix(in srgb, var(--nav-selected) 90%, #fff); }
/* flex/min-width:0/ellipsis, same reasoning as .brand-text just above: the
   rail is always wide enough at rest, but the collapse/expand toggle
   un-clips this instantly while the width is still mid-transition, and a
   plain <span> with no shrink rule wraps ("All sessions" onto two lines,
   its badge included) for that moment instead of just clipping. */
.nav-link-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-badge {
  margin-left: auto; background: var(--nav-raised-2); color: #fff; font-size: var(--text-label); font-weight: 700;
  padding: 1px 7px; border-radius: var(--radius-sm); min-width: 18px; text-align: center;
  font-family: var(--font-mono); flex-shrink: 0; white-space: nowrap;
}
.nav-link.active .nav-badge { background: rgba(255, 255, 255, .25); }
.nav-icon { display: inline-flex; margin-right: var(--space-2); flex-shrink: 0; }

/* Manage -- an inline disclosure, not a floating menu. It used to be a
   popover; that could not work, because .nav-more lived inside <nav>, which is
   overflow-y:auto, and an absolutely-positioned child cannot escape a scrolling
   ancestor. Anchored near the bottom of the rail it rendered clipped inside its
   own scrollport. Inline, it scrolls with everything else -- and it matches the
   Spaces groups, so the chevron means one thing throughout the rail. */
.nav-subgroup > summary.nav-subgroup-summary {
  cursor: pointer; list-style: none; width: 100%;
}
.nav-subgroup > summary.nav-subgroup-summary::-webkit-details-marker { display: none; }
.nav-subgroup > summary .nav-subsection-chevron { margin-left: auto; transition: transform .12s ease; }
.nav-subgroup[open] > summary .nav-subsection-chevron { transform: rotate(90deg); }
/* The summary carries .active to mean "the page you are on is inside me".
   That is only worth saying while the group is CLOSED -- once it is open the
   child row shows its own marker, and two orange bars stacked four rows apart
   read as two current pages. Open, the summary keeps a plain hover surface. */
.nav-subgroup[open] > summary.nav-link.active {
  background: none; color: var(--nav-muted); box-shadow: none;
}
.nav-subgroup[open] > summary.nav-link.active:hover { background: var(--nav-raised); color: #fff; }
/* The items sit one step in from their summary so the grouping reads without
   needing a rule or a box. */
.nav-subgroup .bucket-group-items { padding-left: var(--space-3); }
.nav-subgroup-label {
  padding: var(--space-2) var(--space-3) 2px; color: var(--nav-muted); font-family: var(--font-mono);
  font-size: var(--text-label); font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
}
.nav-subgroup-divider { height: 1px; background: var(--nav-line); margin: var(--space-1) var(--space-2); }
/* Kept: the account menu still uses these two. */
.nav-more-group-label {
  padding: var(--space-2) var(--space-3) 2px; color: var(--nav-muted); font-family: var(--font-mono);
  font-size: var(--text-label); font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
}
.nav-more-divider { height: 1px; background: var(--nav-line); margin: var(--space-1) var(--space-2); }
/* 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-1) var(--space-3) 2px; margin-top: var(--space-1); color: var(--nav-muted);
  font-weight: 600; font-size: var(--text-ui-sm); display: flex; align-items: center;
}
.nav-subsection-header--org { font-size: var(--text-ui-sm); 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: var(--text-ui-sm); }
/* Personal/Shared/Organizations, and each org within Organizations, as
   independent <details> -- collapsing the ones you're not using right
   now is what actually shortens the rail once there are several buckets
   across a few orgs, not just tighter padding on rows you still see.
   State persists per section (see base.html's own restore/toggle
   script). cursor/list-style/marker-hiding mirror .chunks-section-
   toggle's own summary treatment elsewhere. */
.bucket-tree-section > summary { cursor: pointer; list-style: none; }
.bucket-tree-section > summary::-webkit-details-marker { display: none; }
.nav-subsection-chevron {
  margin-left: auto; display: inline-flex; flex-shrink: 0; color: var(--nav-muted); transition: transform .12s ease;
}
.bucket-tree-section[open] > summary .nav-subsection-chevron { transform: rotate(90deg); }
/* 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 -- the count badge overflows the same way. */
.bucket-tree-row { display: flex; align-items: center; gap: var(--space-2); min-width: 0; cursor: grab; }
.bucket-tree-row:active { cursor: grabbing; }
.bucket-tree-row.dragging { opacity: .4; }
.bucket-tree-link .nav-link-label { flex: 1 1 auto; min-width: 0; }
.bucket-tree-link .nav-badge { flex-shrink: 0; }
.bucket-tree-link { flex: 1; min-width: 0; font-size: var(--text-ui); }
.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: var(--text-ui-sm);
  border-top: 1px solid var(--nav-line); padding-top: var(--space-2);
}
/* Colour is --info, not the #fff this used to carry: the one surface that
   needed white was the old sidebar footer's Log out, which is now the account
   chip. Every remaining link-button sits on a content surface.
   The `button.link-button` half of this lives further down, next to
   button.checkbox -- see the note there. */
.link-button { border: 0; padding: 0; background: none; color: var(--info); text-decoration: underline; cursor: pointer; font: inherit; }

/* ---- sidebar footer -----------------------------------------------
   Get Desktop App, the dark-theme switch and Settings read as one more
   group of nav rows rather than a different shape of control tacked onto
   the bottom of the rail -- then who you are, then signing out and the
   version.

   Everything here is visible. It replaces an account chip whose popover held
   the theme control and Log out: two clicks and a thing to find, for the two
   controls people reach for most. Nothing is absolutely positioned any more,
   which is also why this no longer has to explain itself as escaping the
   scrolling <nav> above it. */
.sidebar-footer-row {
  display: flex; align-items: center; gap: var(--space-2); width: 100%;
  min-height: 32px; border: 0; padding: var(--space-2);
  border-radius: var(--radius-sm); background: none;
  color: var(--nav-muted); text-decoration: none; font: inherit;
  font-size: var(--text-ui-sm); cursor: pointer; text-align: left;
}
.sidebar-footer-row:hover { color: #fff; background: var(--nav-raised); }
.sidebar-footer-row.active { color: #fff; background: var(--nav-selected); box-shadow: inset 3px 0 0 var(--orange); }
.sidebar-footer-row-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A switch, not the old two-button segmented control, now that Light and
   Dark share one row -- the row's own label says which state is which, so
   the switch only has to show on/off. */
.footer-toggle {
  flex-shrink: 0; width: 30px; height: 18px; border-radius: 999px;
  padding: 2px; box-sizing: border-box;
  background: var(--nav-raised); transition: background-color .15s ease;
}
.footer-toggle-knob {
  display: block; width: 14px; height: 14px; border-radius: 50%;
  background: var(--nav-muted); transition: transform .15s ease, background-color .15s ease;
}
/* Fill AND the knob's position AND its colour, never colour alone. */
.sidebar-footer-row.is-on .footer-toggle { background: var(--nav-raised-2); }
.sidebar-footer-row.is-on .footer-toggle-knob { background: #fff; transform: translateX(12px); }

/* Sign out reads as a small button, not bare text -- padding with no
   visible edge around it would just be extra whitespace, indistinguishable
   from margin. */
.sidebar-footer-link {
  /* inline-flex + min-height 24, not inline-block: SC 2.5.8's floor applies
     to a text link as much as to a button. The text itself is unchanged --
     only the hit area grows. */
  display: inline-flex; align-items: center; min-height: 24px;
  border: 0; padding: 0; background: none;
  color: var(--nav-muted); text-decoration: none; font: inherit;
  font-size: var(--text-ui-sm); cursor: pointer;
}
.sidebar-footer-link:hover { color: #fff; text-decoration: underline; }
.sidebar-signout-btn {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--nav-raised);
  text-decoration: none;
}
.sidebar-signout-btn:hover { background: var(--nav-raised-2); text-decoration: none; }

.sidebar-user { display: flex; align-items: center; gap: var(--space-2); min-width: 0; margin-top: var(--space-2); padding: 0 var(--space-2); }
.sidebar-user-avatar {
  flex: 0 0 auto; width: 28px; height: 28px; border-radius: 50%;
  display: grid; place-items: center;
  /* --orange on --nav, exactly as the chip this replaces had it: the brand
     orange is the only warm thing in the rail, which is what makes a circle
     with an initial in it read as a person rather than another nav item. */
  background: var(--orange); color: var(--nav);
  font-size: var(--text-label); font-weight: 700; letter-spacing: .02em;
}
.sidebar-user-text { min-width: 0; display: grid; }
.sidebar-user-text b {
  font-size: var(--text-ui-sm); font-weight: 600; color: #fff;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sidebar-user-text small {
  font-size: var(--text-label); color: var(--nav-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sidebar-user-meta {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2); margin-top: var(--space-2);
}
.sidebar-user-meta form { margin: 0; flex-shrink: 0; }
/* white-space:nowrap isn't the redundant-looking rule it seems: body sets
   overflow-wrap:anywhere (see the mobile-pass fixes further down), which
   without this split "v1.24.44" one character per line the instant the
   collapse/expand toggle's un-clip landed on a still-narrow rail. */
.sidebar-version {
  color: var(--nav-muted); font-size: var(--text-label); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- skip link -----------------------------------------------------
   Off-screen until focused, then a real target at the top-left. */
/* WCAG 2.2 SC 2.5.8 -- a 24x24 pointer target minimum, at EVERY pointer type.
   The coarse-pointer block near the end of this file raises the rail and the
   dense controls to 44px for touch; this is the floor that applies with a
   mouse too, and nothing was enforcing it.

   Scoped to standalone controls on a line of their own. SC 2.5.8's exception
   for links inside a sentence is real, and a blanket rule here would bloat
   every inline link in a transcript or an answer. This grows the hit area, not
   the drawn type. */
.breadcrumb a, .back {
  display: inline-flex; align-items: center; min-height: 24px;
}
/* Every <summary> in this app is a standalone disclosure control on its own
   line -- there are none sitting inline inside a sentence -- so the floor
   applies to all of them rather than to an enumerated list that quietly goes
   stale as panels are added. min-height only; `display` is deliberately left
   alone, since changing a summary's display has historically broken toggling
   in WebKit. */
summary { min-height: 24px; }

.skip-link {
  position: fixed; top: var(--space-2); left: var(--space-2); z-index: 100;
  transform: translateY(-200%); transition: transform .12s ease;
  background: var(--surface); color: var(--ink); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3);
  font-weight: 700; font-size: var(--text-ui); text-decoration: none; box-shadow: var(--shadow-raised);
}
.skip-link:focus-visible { transform: translateY(0); }
#main:focus { outline: none; }
.mobile-nav-content { display: contents; }
/* 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; }
/* Rail mode hides the LABELS, but `display: none` also strips them from the
   accessibility tree -- and every .nav-icon is aria-hidden, so a collapsed nav
   link had no accessible name at all: a screen reader announced "link" and
   stopped, and voice control had nothing to say. Clipping instead keeps the
   name for AT while removing it visually. Every rail control also carries a
   `title`, which is what a sighted mouse user needs anyway. */
:root[data-sidebar="collapsed"] .brand-text,
:root[data-sidebar="collapsed"] .nav-link-label,
:root[data-sidebar="collapsed"] .sidebar-user-text,
:root[data-sidebar="collapsed"] .sidebar-footer-row-label,
:root[data-sidebar="collapsed"] .brand-app {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* Genuinely redundant in rail mode -- a count with no label to count, a group
   heading with no group visible, a chevron with nothing to point at. */
:root[data-sidebar="collapsed"] .nav-badge,
:root[data-sidebar="collapsed"] .nav-section-header,
:root[data-sidebar="collapsed"] .bucket-tree,
:root[data-sidebar="collapsed"] .nav-more-chevron { display: none; }
/* .nav-rule deliberately stays visible in rail mode: once the section headers
   are hidden it is the only remaining grouping cue between the icon clusters,
   which is exactly how the reference rail reads. */
:root[data-sidebar="collapsed"] .nav-rule { margin-left: 0; margin-right: 0; }
:root[data-sidebar="collapsed"] .sidebar-user { justify-content: center; }
/* Rail mode keeps Get Desktop App, the theme switch and Settings as icon
   rows -- clipped labels, the same technique the nav links above them use --
   but drops the switch's own on/off pill (no room for it at 64px; the row
   still toggles on click) along with sign out and the version line. Neither
   of those survives a 64px rail legibly, and sign out is one click away once
   the rail is expanded. The avatar stays because it is the only thing left
   saying who is signed in. */
:root[data-sidebar="collapsed"] .footer-toggle,
:root[data-sidebar="collapsed"] .sidebar-user-meta { display: none; }
:root[data-sidebar="collapsed"] .nav-link,
:root[data-sidebar="collapsed"] .sidebar-footer-row,
: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; }

/* The topbar and the page share one column beside the rail, so the bar can be
   sticky WITHOUT being fixed -- it scrolls into place against the content it
   labels and needs no magic offset to sit beside the sidebar. */
.content-column { min-width: 0; display: flex; flex-direction: column; }
.topbar {
  position: sticky; top: 0; z-index: 15;
  display: flex; align-items: center; gap: var(--space-3);
  min-height: 52px; padding: 0 var(--space-8);
  padding-right: max(var(--space-8), env(safe-area-inset-right));
  background: var(--top); border-bottom: 1px solid var(--line);
}
.topbar-menu-button {
  display: none; flex: 0 0 auto; align-items: center; justify-content: center;
  width: 36px; height: 36px; margin-left: calc(-1 * var(--space-2));
  border: 0; background: none; color: var(--ink); cursor: pointer; border-radius: var(--radius-sm);
}
.topbar-menu-button:hover { background: var(--soft); }
.breadcrumb { min-width: 0; }
.breadcrumb ol {
  list-style: none; margin: 0; padding: 0;
  display: flex; align-items: center; gap: var(--space-2);
  font-size: var(--text-ui-sm); color: var(--muted);
}
/* The separator is generated content, never a text node -- a literal "/"
   between crumbs gets announced as "slash" on every page. */
.breadcrumb li + li::before {
  content: ''; display: inline-block; width: 5px; height: 5px;
  margin-right: var(--space-2); border-top: 1.5px solid var(--steel);
  border-right: 1.5px solid var(--steel); transform: rotate(45deg);
  vertical-align: middle;
}
.breadcrumb li:last-child { color: var(--ink); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.breadcrumb a { color: inherit; text-decoration: none; }
.breadcrumb a:hover { color: var(--ink); text-decoration: underline; }
.topbar-actions { margin-left: auto; display: flex; align-items: center; gap: var(--space-2); }

.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. */
  container-type: inline-size; container-name: page;
  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-6); 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: var(--text-title); margin: var(--space-1) 0; }
.page-header p { margin: 0; color: var(--muted); }
h1 { font-size: var(--text-title); line-height: var(--text-title-lh); margin: var(--space-1) 0; font-weight: 700; letter-spacing: -.015em; }
h2 { font-size: var(--text-subsection); line-height: var(--text-subsection-lh); margin: var(--space-7) 0 var(--space-3); color: var(--ink); font-weight: 700; letter-spacing: -.01em; }
.section-hint { margin: calc(-1 * var(--space-2)) 0 var(--space-5); color: var(--muted); font-size: var(--text-ui); }
/* 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-lg); 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: var(--text-subsection); 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: var(--text-ui); border-radius: var(--radius-sm); transition: background-color .3s ease; }
.live-transcript-line .meta { display: inline; margin-right: var(--space-2); font-family: var(--font-mono); }
/* A chunk holds MANY spoken turns, not one. They used to render as a
   single run-on paragraph -- three speakers on one 23px line -- because
   the chunk's text went out as one string and its newlines collapsed.
   Each turn is now its own <p> (see Chunk.utterances / parse_utterances),
   so a speaker change is a line break and a name, the way the PDF and
   Word exports have always rendered it.
   No per-turn timestamp yet, deliberately: the offsets a turn carries are
   relative to its own chunk's audio, not to the session, so printing them
   beside a wall-clock time would read as wrong. The chunk's clock time
   above the group is the honest one until those become session-relative. */
.live-transcript-line .utterance { margin: 0 0 var(--space-1); }
.live-transcript-line .utterance:last-child { margin-bottom: 0; }
.live-transcript-line .u-speaker {
  display: block; font-weight: 600; font-size: var(--text-ui-sm);
  color: var(--ink-soft, var(--muted)); letter-spacing: .01em;
  margin-top: var(--space-2);
}
.live-transcript-line .utterance:first-child .u-speaker { margin-top: 0; }
/* Whisper reported low confidence for these exact words (transcription.py
   marks them inline). Muted rather than flagged with an icon -- it is a
   hint about the audio, not an error the reader has to act on. */
.live-transcript-line .utterance.is-unclear .u-text { opacity: .72; font-style: italic; }
/* Position in the recording ("4:19"), shown only where it's actually known
   -- see Chunk.start_offset_seconds. Mono and tabular so a column of them
   lines up, and DESIGN.md scopes the mono face to exactly this kind of
   readout. This is the hook the media player will seek from (data-at). */
.live-transcript-line .u-at {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: var(--text-ui-sm); color: var(--muted); margin-right: var(--space-2);
}
/* A turn that knows where it sits in the recording can be clicked to jump
   the player there (see _partials/recording_player.html). Only those turns
   get the affordance -- a line with no known position would seek to the
   wrong place, so it stays ordinary text. */
.live-transcript-line .utterance[data-at] { cursor: pointer; border-radius: var(--radius-sm); }
.live-transcript-line .utterance[data-at]:hover { background: var(--surface-2, rgba(127, 127, 127, .10)); }
.live-transcript-line .utterance[data-at]:hover .u-at { color: var(--info); }

/* Audio-only playback is a transport bar and two lines of text; at the full
   width of the content column it read as a huge empty panel with a 40px
   control marooned in it. Capped rather than moved into the session rail:
   the rail is 340px, and the off-the-record warning that sits under the
   player needs more room than that to stay two lines.
   Video is deliberately NOT capped -- a 16:9 call recording earns the width,
   and is what the max-height below is for. */
.recording-panel[data-mode="audio"] { max-width: 34rem; }
/* .section-hint carries a negative top margin globally so it tucks under the
   label it explains. Here it follows a media control -- a block element with
   no margin of its own -- and that pull lands the hint 8px INSIDE the player,
   printed over the scrub bar at narrow widths. */
.recording-panel .section-hint { margin-top: var(--space-2); }

/* Email-this-report panel, opened from the header action row. Its own row
   under the export controls rather than inline with them: it appears on a
   click, and pushing the buttons sideways as it opens moves the target the
   user just aimed at. */
.session-email {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
  margin-top: var(--space-2); width: 100%;
}
.session-email input[type="email"] { min-width: 15rem; flex: 1 1 15rem; }
/* Both hints take the whole row, so the scope line and the result do not sit
   either side of the field competing for the same glance. */
.session-email .section-hint { flex-basis: 100%; margin: 0; }

/* A note the catch-up box is already showing, a few inches further up the
   same screen. Collapsed rather than removed: it is still the record, its
   edit and delete controls still address it, and it un-collapses on its own
   once the box moves on to a newer stretch of the meeting.

   Collapsed to a rule, not to nothing. A note that silently disappears from
   the notes while a meeting runs reads as content being lost -- the line says
   the record is intact and this part of it is just being shown above. Only
   ever applied while a session is live (see Session.focus_echo_keys). */
.key-point-editable.is-echoed { display: none; }
/* The count the row no longer contributes to the visible list. */
.key-points-echoed-note {
  margin: var(--space-2) 0 0; font-size: var(--text-ui-sm); color: var(--muted);
}

/* An optional tool on the live screen, collapsed. The same expand/collapse
   affordance .chunks-section-toggle uses, at the scale of a row inside a
   panel rather than a whole section. */
.live-tool { margin-top: var(--space-3); }
.live-tool > summary {
  cursor: pointer; list-style: none; display: flex; align-items: baseline;
  gap: var(--space-2); flex-wrap: wrap;
}
.live-tool > summary::-webkit-details-marker { display: none; }
.live-tool > summary::after {
  content: '‹ open'; margin-left: auto; color: var(--muted);
  font-size: var(--text-ui-sm); font-weight: 700;
}
.live-tool[open] > summary::after { content: 'close ›'; }
.live-tool-label {
  font-size: var(--text-label); line-height: var(--text-label-lh);
  letter-spacing: .06em; text-transform: uppercase; font-weight: 700; color: var(--muted);
}
.live-tool-hint { font-size: var(--text-ui-sm); color: var(--muted); }

/* The format list, behind its own disclosure inside the dropzone. */
.capture-formats > summary { cursor: pointer; display: inline-block; }
.capture-formats > summary::-webkit-details-marker { color: var(--muted); }

/* A summary panel with nothing in it yet. Present for the live poll to
   replace, but not pretending to be content. */
.panel.is-empty { padding-top: var(--space-4); padding-bottom: var(--space-4); }
.panel.is-empty .session-summary { margin: 0; color: var(--muted); }

/* Live view, on a live session's card. Sits left of the delete button, which
   owns the top-right corner; both are absolutely positioned because the card
   itself is one big <a> and a control cannot nest inside it. Always visible
   rather than revealed on hover like delete -- it is the thing you are looking
   for while a meeting runs, and there is no hover on the phone this gets used
   from. */
.session-card-live-btn {
  position: absolute; top: var(--space-3); right: calc(var(--space-3) + 32px); z-index: 2;
  min-height: 24px; padding: 2px var(--space-2);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--primary);
  font-size: var(--text-ui-sm); font-weight: 700; line-height: 1.4;
  cursor: pointer; white-space: nowrap;
}
.session-card-live-btn:hover { border-color: var(--primary); background: var(--soft); }

/* The add-an-alert row. Wraps rather than squeezing: the phrase field is the
   long one and the scope checkbox reads as a sentence, so at narrow widths
   they stack instead of both becoming unusable. */
.alert-form { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); }
.alert-form input[type="text"] { flex: 1 1 18rem; min-width: 0; }
.alert-form .section-hint { flex-basis: 100%; margin: 0; }

/* Destination links on the Settings screen. A row per page rather than a
   bare list: each one needs a sentence saying what is behind it, which is
   exactly what the "Manage" disclosure could not do and why nobody knew what
   was in it. */
.settings-links { display: flex; flex-direction: column; gap: var(--space-2); }
.theme-choice { border: 0; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
.theme-choice .radio-label { min-height: 32px; }
.settings-link {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--space-3); border: 1px solid var(--line); border-radius: var(--radius-md);
  text-decoration: none; color: inherit; background: var(--surface);
}
.settings-link:hover { border-color: var(--primary); background: var(--soft); }
.settings-link .section-hint { margin: 0; }
.settings-link-label { font-weight: 700; color: var(--primary); }

/* Live follow-along feed. Capped and scrollable -- this is a "keep up with
   what is being said" view, not the transcript, which lives on the session
   page and is the record. */
.live-text-feed {
  max-height: 11rem; overflow-y: auto; scrollbar-width: thin;
  display: flex; flex-direction: column; gap: var(--space-1);
}
/* Pushed to the far end of the panel header, away from the heading and the
   connection state, so it reads as an action on the panel rather than another
   piece of its status. */
.live-text-popout { margin-left: auto; }
.live-text-line { margin: 0; font-size: var(--text-ui); line-height: 1.5; color: var(--ink); }
/* An unfinalized guess, shown dimmer because it is about to be rewritten.
   Colour alone does not carry it -- a partial is also the only line without a
   settled one beneath it, and it resolves within a second or two. */
.live-text-line.is-partial { color: var(--muted); font-style: italic; }

/* A paused recording is still a live one -- amber (the "needs attention /
   ongoing" role this app already uses for an open exclusion) rather than the
   neutral secondary it would otherwise inherit, so a page left paused does not
   read as a page that finished. */
#pause-btn.is-paused {
  background: var(--pending-bg); color: var(--pending);
  border-color: var(--pending-dot); font-weight: 700;
}

/* Meeting recording playback. Sized to the panel rather than given a fixed
   height: a mixed video from a call is 16:9, and letting it letterbox
   itself beats guessing. */
.session-recording {
  width: 100%; max-height: 60vh; display: block;
  background: #000; border-radius: var(--radius-sm);
}

/* Embedded YouTube playback for a worker-driven capture whose source was a
   real youtube.com/watch (or youtu.be/embed/live/shorts) url -- see
   stream_capture.youtube_video_id and _partials/youtube_embed.html.
   aspect-ratio, not the recording's own max-height: a live/VOD YouTube
   embed has no natural bound the way a Recall call recording does (it is
   always exactly 16:9 here), and 60vh would crop it on a short viewport
   for no reason. */
.session-youtube-embed {
  width: 100%; aspect-ratio: 16 / 9; border: 0; display: block;
  border-radius: var(--radius-sm); background: #000;
}
/* Summary / Transcript tabs on the session page. Two readings of the same
   meeting, so they are alternatives rather than a sequence -- stacked, you
   scrolled past all of one to reach the other.
   Underline rather than a filled pill: these sit directly above panels
   that already carry a border and a surface, and a second filled shape
   would read as a third card rather than a control. */
.session-tabs {
  display: flex; gap: var(--space-4);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--space-5);
}
.session-tab {
  appearance: none; background: none; border: 0; cursor: pointer;
  font: inherit; font-weight: 600; font-size: var(--text-ui); color: var(--muted);
  padding: var(--space-3) 0; margin-bottom: -1px;
  border-bottom: 2px solid transparent; min-height: 40px;
  /* A flex item may shrink below its content width, and a label that
     narrow wraps one character per line into a very tall button. Two
     short words never need to wrap at all. */
  white-space: nowrap; flex: none;
}
.session-tab:hover { color: var(--ink); }
.session-tab.is-active { color: var(--ink); border-bottom-color: var(--info); }
.session-tab:focus-visible { outline: 2px solid var(--info); outline-offset: 2px; }
/* The transcript inside its tab is no longer a collapsible section, so it
   needs the height cap the <details> used to imply -- otherwise a long
   meeting turns the tab into an endless page. */
.session-tabpanel .live-transcript { max-height: 70vh; }

.transcript-download { margin: 0 0 var(--space-3); font-size: var(--text-ui); }

/* A calendar event wired has already booked a bot onto. --live, the same
   colour the app uses everywhere else for "this is on", rather than a new
   one for the same meaning. */
.calendar-booked { color: var(--live); font-weight: 600; }

/* The finished-session dashboard: what you read on the left, what you do
   on the right. minmax(0,1fr) rather than 1fr on the main column because
   the transcript and the raw-chunk editor both contain long unbroken
   strings, and a plain 1fr track refuses to shrink below its content,
   which would push the rail off the page.
   Deliberately not sticky: the rail holds forms (share links, rename,
   export), and a form that follows you down the page while you are
   reading the transcript is an irritation, not a convenience. */
.session-dash { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: var(--space-6); align-items: start; }
.session-dash-main, .session-dash-rail { min-width: 0; }
/* One column below this, MAIN FIRST -- on a phone the summary and action
   items are the whole point, and a stack that led with the share-link form
   would bury them under controls.

   A CONTAINER query, not a viewport one. How much room this grid actually has
   depends on the sidebar as much as on the window: the rail is 264px expanded,
   60px collapsed, and 0 once it stacks below 901px. A viewport breakpoint
   cannot see any of that, so the old `max-width: 1100px` was wrong in both
   directions -- it stacked a 900px window whose rail was collapsed and had 760px
   of room going spare, and it kept two columns on a 1101px window whose rail was
   open and only had 757px. 720px is the real threshold: 340 rail + 24 gap still
   leaves the main column ~356px. */
@container page (max-width: 720px) {
  .session-dash { grid-template-columns: 1fr; gap: 0; }
}

/* Action items (session_detail.html). A checklist you can work through:
   the tick is a real control, and the owner and deadline sit on the row
   rather than inside the collapsed edit form, because those are the two
   things that decide what to do next. */
.action-item .record-main { display: flex; align-items: flex-start; gap: var(--space-3); }
.action-item-body { flex: 1; min-width: 0; }
.action-item-tick { flex: none; }
.action-item-check {
  width: 22px; height: 22px; margin-top: 2px; padding: 0;
  border: 1.5px solid var(--line-strong, var(--line)); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--live); cursor: pointer;
  font-size: var(--text-ui-sm); line-height: 1; display: flex; align-items: center; justify-content: center;
}
.action-item-check:hover { border-color: var(--live); }
/* Done items stay visible rather than disappearing -- what was completed
   in a meeting is part of its record, and an export that silently dropped
   them would misrepresent it. Muted, and struck through, so the open work
   still reads first. */
.action-item.is-done .list-item-edit-heading h3 { text-decoration: line-through; opacity: .6; }
.action-item.is-done .action-item-check { border-color: var(--live); }
.action-item-who { font-weight: 600; color: var(--ink); }
.action-item-overdue { color: var(--danger); font-weight: 600; }

/* One-tap questions under the Ask box. Chips rather than buttons-proper:
   these are suggestions, and styling them as primary actions would have
   them compete with Ask itself, which is the real control. */

.session-chunk-audio { width: 100%; display: block; }
.recording-exclusion-note { margin-top: var(--space-3); }
.live-transcript-line .chunk-text { white-space: pre-wrap; }

/* Marks (static/marks.js, _partials/transcript.html) -- hover a
   transcript line for a small mark icon (bookmark it, or start/extend an
   off-the-record or redaction range); a bookmarked line gets a pin
   instead. Hidden until hover/focus rather than always-on so the
   transcript itself stays the visual focus, not a rail of icons. */
.mark-icon {
  display: inline-flex; align-items: center; justify-content: center; vertical-align: middle;
  width: 22px; height: 22px; margin-left: var(--space-2); padding: 0;
  border: none; border-radius: var(--radius-sm); background: none; color: var(--muted);
  cursor: pointer; opacity: 0; transition: opacity .12s ease;
}
.live-transcript-line:hover .mark-icon,
.live-transcript-line:focus-within .mark-icon,
.mark-icon:focus-visible { opacity: 1; }
.mark-icon:hover { background: var(--soft); color: var(--info); }
.mark-pin { margin-right: var(--space-2); color: var(--info); }

/* The create-mark popover: static/marks.js drops this after whichever
   line was clicked via insertAdjacentElement, so it reads as "attached to
   that line" rather than a modal floating over the whole page. */
.mark-popover {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2);
  margin: var(--space-2) 0; padding: var(--space-2) var(--space-3);
  background: var(--soft); border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.mark-popover-caption { font-size: var(--text-ui-sm); font-weight: 600; color: var(--muted); margin-right: auto; }
/* .link-button is white-on-solid for its usual home (the dark sidebar
   footer's Log out link) -- invisible against this popover's light
   --soft background, so Cancel here needs its own color. Same fix
   applies to .capture-source-choice's own Cancel (session_live.html),
   which sits on the same light --surface panel and had the identical
   invisible-white-text problem. */
.mark-popover .link-button, .capture-source-choice .link-button { color: var(--muted); text-decoration: underline; }
.mark-popover .link-button:hover, .capture-source-choice .link-button:hover { color: var(--ink); }

/* An off-the-record/redacted run, collapsed by default: a border-stripe
   + tinted background in the same role color the rest of the app already
   uses for "needs attention" (amber) vs "destructive/withheld" (red), so
   the meaning is legible before you even read the label. Real words stay
   inside for the owner to expand -- everyone/everything else only ever
   sees the placeholder text in the <summary>, never this markup (see
   _partials/transcript_share.html, which renders the placeholder as
   plain text with no <details> at all). */
.transcript-exclusion {
  margin: var(--space-2) 0; padding: var(--space-2) var(--space-3);
  border-left: 3px solid var(--pending); border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--pending) 8%, transparent);
}
.transcript-exclusion--redacted { border-left-color: var(--danger); background: color-mix(in srgb, var(--danger) 8%, transparent); }
.transcript-exclusion > summary {
  cursor: pointer; list-style: none; font-size: var(--text-ui-sm); font-weight: 700; color: var(--pending);
}
.transcript-exclusion--redacted > summary { color: var(--danger); }
.transcript-exclusion > summary::-webkit-details-marker { display: none; }
.transcript-exclusion > summary::before { content: '\203a\a0'; }
.transcript-exclusion[open] > summary::before { content: '\2039\a0'; }
.transcript-exclusion-lines { margin-top: var(--space-2); padding-top: var(--space-2); border-top: 1px solid var(--line); display: flex; flex-direction: column; gap: var(--space-2); }
/* The share-safe placeholder line (_partials/transcript_share.html) --
   no real text ever reaches this markup, so it just needs to read as
   "something was left out here" at a glance, no owner-only affordances. */
.live-transcript-line.is-excluded { color: var(--muted); font-style: italic; }

/* Scroll target when a Marks-panel row's jump link
   (static/marks.js:initMarksJumpLinks) lands on a transcript line --
   a brief highlight so "there it is" is obvious even after the smooth
   scroll settles, then fades back to normal rather than staying flagged. */
.is-jump-target { background-color: color-mix(in srgb, var(--info) 20%, transparent); }

/* 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-sm);
}
.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: var(--text-body); }
/* Title links (Trash, Archives, All sessions) are the row's main target but
   were only as tall as their line -- 21px, under WCAG 2.2's 24px floor.
   inline-flex adds height only where the text is shorter than that. */
.record h3 a, .list-row strong a { display: inline-flex; align-items: center; min-height: 24px; }
.record p { margin: var(--space-1) 0 0; color: var(--muted); font-size: var(--text-ui); }
.record.completed h3, .completed h3 { text-decoration: line-through; }
.completed { opacity: .58; }
.meta { font-size: var(--text-ui-sm); 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-lg);
  box-shadow: var(--shadow); overflow: hidden; display: flex; flex-direction: column; max-height: calc(100vh - 220px - var(--topbar-h));
}
/* 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(--info));
}
.bucket-column-header:active { cursor: grabbing; }
.bucket-column-header h2 { margin: 0; font-size: var(--text-body); line-height: var(--text-body-lh); 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(--info)); 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: var(--text-subsection); 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: var(--text-ui-sm); font-weight: 700;
  padding: 3px 9px; border-radius: var(--radius-sm); 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-sm); 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(--info); color: var(--info); 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(--info); }
@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: var(--text-ui); 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(--info); }
/* 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(--info); }
.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: var(--text-ui); 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-lg); 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: var(--text-ui); font-weight: 700; color: var(--ink); }
.session-type-help { font-size: var(--text-ui-sm); color: var(--muted); }
.session-type-auto { border-color: var(--info); }
/* 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(--info); }

/* 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: var(--text-ui-sm); 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: var(--text-ui-sm); 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(--info); }
/* 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 {
  /* 24, not the drawn 22: SC 2.5.8's floor applies to a destructive control
     as much as any other, and the glyph inside is unchanged -- only the hit
     area grows. The 36px clearance reserved for it below still covers this. */
  display: grid; place-items: center; width: 24px; height: 24px; padding: 0;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  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: var(--text-ui-sm); 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. */
/* A tinted pill: role background, role foreground, role dot. Each variant sets
   the three together via --badge-* so a new status is three lines, not three
   scattered rules.

   The dot is `var(--badge-dot)`, NOT currentColor. In this palette the dot and
   the foreground are deliberately different values -- a dot only has to clear
   3:1 (non-text), so it stays saturated, while the text has to clear 4.5:1 and
   goes darker. currentColor would drag the dot down to the text's value and
   lose exactly the signal the dot exists to carry.

   Two behaviours below are load-bearing and were each fixed once already:
   the badge WRAPS rather than nowrap (a long motion label measured 282px past
   a 375px viewport), and align-items:flex-start + the margin-top optically
   centre the dot on the FIRST line of a wrapped label rather than floating it
   beside the vertical middle. Adding a background makes wrapping more visible,
   not less -- do not "fix" it back to nowrap. */
.badge {
  display: inline-flex; align-items: flex-start; gap: .42em; padding: 3px 9px 3px 8px;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: var(--badge-bg, var(--neutral-bg));
  color: var(--badge-fg, var(--neutral));
  font-size: var(--text-label); font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  max-width: 100%;
}
.badge::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%;
  background: var(--badge-dot, currentColor);
  flex-shrink: 0; margin-top: .32em;
}
.badge-open { --badge-fg: var(--live); --badge-bg: var(--live-bg); --badge-dot: var(--live-dot); }
.badge-open::before { animation: status-pulse 1.8s ease-in-out infinite; }
.badge-contact { --badge-fg: var(--info); --badge-bg: var(--info-bg); --badge-dot: var(--info-dot); }
.badge-reminder { --badge-fg: var(--pending); --badge-bg: var(--pending-bg); --badge-dot: var(--pending-dot); }
.badge-reminder::before { animation: status-pulse 2.2s ease-in-out infinite; }
/* An exclusion mark still open (end_chunk_index null) -- someone's
   currently off the record/redacted, same amber pulse language as a
   flagged reminder for "this is ongoing, not just noted." */
.badge-ongoing { --badge-fg: var(--pending); --badge-bg: var(--pending-bg); --badge-dot: var(--pending-dot); }
.badge-ongoing::before { animation: status-pulse 1.8s ease-in-out infinite; }
.badge-vote-passed { --badge-fg: var(--live); --badge-bg: var(--live-bg); --badge-dot: var(--live-dot); }
.badge-vote-failed { --badge-fg: var(--danger); --badge-bg: var(--danger-bg); --badge-dot: var(--danger-dot); }
.badge-vote-neutral { --badge-fg: var(--neutral); --badge-bg: var(--neutral-bg); --badge-dot: var(--neutral-dot); }
/* a motion/second is procedural, not pass/fail -- always the same
   neutral tone regardless of outcome, distinct from the vote badges */
.badge-motion { --badge-fg: var(--neutral); --badge-bg: var(--neutral-bg); --badge-dot: var(--neutral-dot); }
/* 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 { --badge-fg: var(--neutral); --badge-bg: var(--neutral-bg); --badge-dot: var(--neutral-dot); font-weight: 600; }

.topics { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
.topic { font-size: var(--text-label); color: var(--muted); background: var(--soft); border: 1px solid var(--line); padding: 3px 9px; border-radius: var(--radius-sm); }
/* 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-1) 0 0; padding-left: 0; list-style: none; }
/* Density, deliberately. A meeting summary is SCANNED -- you are looking
   for the one line that concerns you -- and 8px between every bullet plus
   1.55 leading spread a dozen facts over a screen and a half, so the shape
   of the meeting never fit in one view. Tightened to 4px and 1.45, which
   is still comfortably above the 1.4 where wrapped bullets start to run
   together. The bullets themselves are unchanged; only the air is. */
.key-points li {
  margin: var(--space-1) 0; color: var(--ink); font-size: var(--text-ui); line-height: 1.45;
  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.
     `1lh` IS one line box, so this tracks the li's own line-height; the
     previous 1.55em was a hardcoded copy of the body line-height and was
     already out of step with the li's 1.45. */
  width: 5px; height: 1lh;
  background: radial-gradient(circle at 50% 50%, var(--info) 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(--info); 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-sm);
  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. */
/* Section headings sit closer to the bullets they head and further from
   the section above -- the gap should say "new section", and an equal gap
   above and below says nothing. */
.key-point-group-topic {
  margin: var(--space-5) 0 var(--space-2); font-size: var(--text-body); font-weight: 700; color: var(--ink);
  letter-spacing: -.005em; padding-bottom: var(--space-1); border-bottom: 1px solid var(--line);
}
.key-point-group-topic:first-child { margin-top: var(--space-2); }
/* 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: var(--text-ui); 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: var(--text-ui-sm); 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(--info); outline-offset: 1px; border-color: var(--info); }
::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: var(--text-ui-sm); 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(--primary); color: var(--primary-ink); border-color: var(--primary); }
.button.primary:hover, button.primary:hover, button[type="submit"]:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
.button.secondary, button.secondary { background: var(--surface); color: var(--info); border: 1px solid var(--line); }
.button.secondary:hover, button.secondary:hover { border-color: var(--info); }
.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: var(--text-ui-sm); }
.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-sm); 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; }

/* Exactly the same collision, same fix. button[type="submit"] paints a solid
   --primary fill with white text, and a bare `.link-button` is a class short
   of it -- so every link-styled button that happens to sit in a form (the
   Meeting Coach's Ignore and flag toggle) rendered as a filled navy button
   instead of a link. Repeating the element matches the specificity; being
   below button[type="submit"] is what actually settles it, which is why this
   is HERE and not next to the .link-button rule 1100 lines up. */
button.link-button {
  background: none; border: 0; padding: 0; color: var(--info);
}

.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: var(--text-ui); }

.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-sm); padding: var(--space-2) var(--space-3); font-family: var(--font-mono);
  font-size: var(--text-ui-sm); 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: var(--text-ui-sm);
  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);
}
/* --primary, not --info: this is a SOLID block of role color, which
   is what the fill tokens exist for (see :root) -- --info 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(--primary); }
.view-order-switch.on .view-order-thumb { transform: translateX(13px); }
@media (prefers-reduced-motion: reduce) {
  .view-order-track, .view-order-thumb { transition: none; }
}
/* One H1 size everywhere (guide v2: 28/36). A smaller "contextual" h1 here
   used to sit level with, or on phones below, the 20px card headings. */
.kiosk-feed-header h1 { margin: 0; font-size: var(--text-title); line-height: var(--text-title-lh); }
.kiosk-feed #live-summary-panel { font-size: var(--text-subsection); }
/* In the popped-out window the feed IS the thing being watched, and there is
   a second monitor's worth of height to watch it in. 11rem is sized for a
   panel competing with everything else on the live page; here it just means
   four lines of a conversation and a scrollbar. */
.kiosk-feed .live-text-feed { max-height: 40vh; }
.kiosk-feed #live-summary-panel .session-summary { font-size: var(--text-subsection); 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: var(--text-subsection); }
/* :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: var(--text-subsection); }
.kiosk-feed #live-summary-panel .key-points li { font-size: var(--text-body); 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: var(--text-subsection); }
.kiosk-feed #live-transcript-panel .live-transcript-line { font-size: var(--text-body); line-height: 1.6; }
@media (max-width: 650px) {
  .kiosk-feed { padding: var(--space-5) var(--space-4); }
}
.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-mark { display: block; width: 48px; height: auto; }
.login-mark-reverse { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .login-mark-primary { display: none; }
  :root:not([data-theme="light"]) .login-mark-reverse { display: block; }
}
:root[data-theme="dark"] .login-mark-primary { display: none; }
:root[data-theme="dark"] .login-mark-reverse { display: block; }
.login-card h1 { margin: 0 0 var(--space-2); }
.login-card > p { color: var(--muted); margin: 0; }
/* Label above the field (the guide's field anatomy), so the name stays
   visible after typing starts -- a placeholder disappears. */
.login-field { display: grid; gap: var(--space-1); text-align: left; }
.login-field label { font-size: var(--text-ui); line-height: var(--text-ui-lh); font-weight: 500; color: var(--ink); }
.stacked-form .login-field input { text-align: left; }

/* 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: var(--text-ui-sm); 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: var(--text-ui-sm); 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: var(--text-ui); 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: var(--text-ui); 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: var(--text-ui);
}
.console-line { padding: .1rem 0; }
.console-ts { color: #7f8aa3; margin-right: var(--space-3); }
.console-status {
  font-size: var(--text-label); font-weight: 700; color: var(--muted); border: 1px solid var(--line);
  border-radius: var(--radius-sm); 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(--info); 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: var(--text-subsection); }
.list-count { font-size: var(--text-label); font-weight: 700; color: var(--info); background: var(--soft); border: 1px solid var(--line); padding: 3px 9px; border-radius: var(--radius-sm); 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: var(--text-ui-sm); 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: var(--text-ui); 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(--info); font-weight: 700; font-size: var(--text-ui-sm);
}
.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: var(--text-ui-sm); 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: var(--text-ui-sm); 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: var(--text-ui); }
.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(--primary); color: var(--primary-ink); font-size: var(--text-ui-sm); font-weight: 700;
}
.chat-answer-wrap { min-width: 0; }
.chat-answer { line-height: 1.7; font-size: var(--text-body); 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-sm); 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: var(--text-ui-sm); line-height: var(--text-body-lh); 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(--info); font-weight: 700; text-decoration: underline; }
.response-link { color: var(--info); text-decoration: none; border-bottom: 1px solid var(--info); }
.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-sm); font-size: var(--text-ui-sm); cursor: pointer; display: inline-flex; align-items: center;
}
.chat-copy-btn:hover { background: var(--soft); color: var(--info); }
.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(--info); text-decoration: none; font-size: var(--text-ui); 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: var(--text-title); }
.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-2); flex-wrap: wrap; min-width: 0; }
/* Share / export / More -- the row someone actually came here to use.
   Scope sits INSIDE the export form next to the two buttons it governs, so
   what a download will contain is readable without hunting for a separate
   control. */
/* min-width:0 and wrap are both load-bearing on a phone: a flex item defaults
   to min-width:auto -- "never shrink below my content" -- so without them this
   row held the whole header open at ~512px inside a 460px viewport and pushed
   the page into horizontal scroll. */
.session-export-form {
  display: flex; align-items: center; gap: var(--space-2); margin: 0;
  flex-wrap: wrap; min-width: 0;
}
.session-export-form select { min-width: 0; width: auto; flex: 0 1 auto; }

/* "More" holds Live view, Archive and Delete. A native <details> rather than
   another scripted popover: it needs no JS, closes on Esc for free, and the
   two destructive items in it are exactly what should NOT sit one stray click
   away from Share and Download. */
.session-more { position: relative; }
.session-more > summary { list-style: none; cursor: pointer; }
.session-more > summary::-webkit-details-marker { display: none; }
.session-more-menu {
  position: absolute; top: calc(100% + var(--space-1)); right: 0; z-index: 20;
  min-width: 190px; display: grid; gap: 1px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-lg); padding: var(--space-1); box-shadow: var(--shadow-overlay);
}
.session-more-menu form { margin: 0; }
.session-more-menu .nav-more-item,
.session-more-menu button.nav-more-item {
  width: 100%; justify-content: flex-start; text-align: left;
  background: none; border: 0; color: var(--ink);
  padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm);
  font: inherit; font-size: var(--text-ui); cursor: pointer;
}
.session-more-menu .nav-more-item:hover { background: var(--soft); }
.session-more-menu .is-danger, .session-more-menu button.is-danger { color: var(--danger); }
/* 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: var(--text-ui-sm); 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: var(--text-ui-sm); 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: var(--text-ui-sm); color: var(--muted); white-space: nowrap; }
.mic-meter-label.mic-silent { color: var(--danger); font-weight: 600; }
.rd-mic-picker { display: flex; align-items: center; gap: var(--space-3); margin-top: var(--space-3); }
.rd-mic-picker[hidden] { display: none; }
.rd-mic-picker label { font-size: var(--text-ui-sm); color: var(--muted); white-space: nowrap; }
.mic-picker-select { font-size: var(--text-ui-sm); padding: var(--space-1) var(--space-2); width: auto; max-width: 16rem; }

/* 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: var(--text-ui); 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); 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: var(--text-label); font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted); vertical-align: 1px;
}
.fact-kind-decision { color: var(--info); border-color: var(--info); }
.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: var(--text-label); 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(--info); outline-offset: -1px;
  background: color-mix(in srgb, var(--info) 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: var(--text-label); font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase;
  background: var(--primary); color: var(--primary-ink);
}

/* "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-fill); color: #fff;
  font-size: var(--text-ui-sm); 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: var(--space-1) 0; line-height: 1.45; }
.headline-points li { margin-bottom: var(--space-2); line-height: var(--text-body-lh); }
: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: var(--text-ui);
}
.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: var(--text-ui-sm); 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(--info); border-left-width: 4px;
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--info) 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: var(--text-label); font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--info);
}
.current-topic-time { font-size: var(--text-label); 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: var(--text-subsection); 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; }
/* The earlier points ship with the panel and are hidden until asked for --
   see the partial. State lives on <html>, not in here, because this box is
   replaced wholesale every 3s. */
.current-topic-points .is-earlier { display: none; }
:root[data-topic-expanded] .current-topic-points .is-earlier { display: list-item; }
/* Now a button rather than a <p>. One rule, not two -- a second block setting
   `font: inherit` after this one would quietly undo the font-size. */
.current-topic-more {
  margin: var(--space-2) 0 0; font-size: var(--text-ui-sm); color: var(--muted);
  border: 0; background: none; padding: 0; cursor: pointer;
  font-family: inherit; font-weight: 600;
  text-decoration: underline; text-underline-offset: 2px;
}
.current-topic-more:hover { color: var(--ink); }
/* A real control, so it takes the same 24px pointer floor as everything else
   (WCAG 2.2 SC 2.5.8) -- as a bare button it was landing at 21px. */
.current-topic-more { min-height: 24px; }

/* 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: var(--text-ui-sm); 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: var(--text-ui-sm); 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: var(--text-ui);
}
.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: var(--text-ui-sm); 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; } }
/* The Mark button's persistent state while an exclusion is open (see
   session_live.html) -- solid fill rather than the pulse above, since
   "off the record" can sit open for minutes and a pulsing button that
   long reads as an alarm, not a status. Amber for off_the_record (needs
   attention, but nothing's wrong), red for redacted (the stronger,
   destructive-toward-the-record reason), matching the same two role
   colors _partials/transcript.html's exclusion border-stripe uses. */
.record-toggle-btn.mark-open-otr { background: var(--pending-fill); color: #fff; border-color: var(--pending-fill); }
.record-toggle-btn.mark-open-redacted { background: var(--danger-fill); color: #fff; border-color: var(--danger-fill); }

/* 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: var(--text-ui-sm); }
.dropzone.drag-active { border-color: var(--info); 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(--info); font-weight: 700; font-size: var(--text-ui); overflow-wrap: anywhere;
}
.screenshot-body { padding: var(--space-3); }
.screenshot-body p { margin: var(--space-2) 0; font-size: var(--text-ui); 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) {
  /* Below the rail breakpoint the nav collapses to an accordion under the
     brand, so the topbar becomes the only place to open it. */
  .topbar-menu-button { display: inline-flex; }
  .topbar { padding-left: max(var(--space-5), env(safe-area-inset-left)); padding-right: max(var(--space-5), env(safe-area-inset-right)); }
  /* 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. */
  /* Added to the padding rather than max()'d against it: installed to the
     home screen, base.html's viewport-fit=cover + black-translucent status
     bar mean the page really does start at the physical top of the screen,
     so the inset is the distance to CLEAR the clock and battery, not a
     padding value competing with ours. max() would leave the brand jammed
     against the underside of the status bar on a notched phone, which is
     the bug this fixes; in a browser tab (and on any phone without a
     notch) the inset is 0 and this is exactly the old padding. The
     sidebar's own dark background fills the inset, so the white status-bar
     text keeps something dark to sit on. */
  /* Off-canvas, not in-flow. This used to be position:relative with its
     height left to its content -- a NORMAL block sitting above the topbar,
     so an open drawer's ~1000px of nav tree physically pushed the topbar and
     the whole page down below it. That reads as broken formatting because it
     is: content should never change position depending on whether a menu
     happens to be open elsewhere on the same page.

     Fixed positioning removes it from the page's flow entirely -- nothing it
     contains can push anything else again, open or closed, regardless of how
     tall the bucket tree grows. Sliding it via transform (not toggling
     display/width) is what makes that translation animatable; the open/close
     glide comes from this one property.

     Left OFF-screen by default at every width, harmlessly: the transform
     only has an effect once .sidebar is also position:fixed, which only
     happens inside this same media query, so nothing here reaches the
     desktop rail. */
  .sidebar {
    position: fixed; top: 0; left: 0; z-index: 40;
    width: min(320px, 85vw);
    /* The base (desktop) rule sets height:100vh, and on a phone that is the
       wrong number -- 100vh is measured against the viewport with the
       browser's own address bar and toolbar HIDDEN, which is taller than
       what is actually on screen while they're showing (Chrome/Safari on
       iOS never hides both here, and often shows neither collapsed).
       height:100vh on a position:fixed drawer sized to that larger,
       theoretical viewport put the footer -- Sign out, the version -- below
       the real bottom edge of the screen, behind the browser's own UI,
       with no way to scroll to it since the drawer's own scrolling lives on
       .sidebar nav, one level in, not on this outer box.
       100dvh is the dynamic viewport height -- the actual visible area,
       shrinking and growing live as the browser's chrome shows or hides.
       The 100vh above stays as the fallback for a browser old enough not to
       know dvh, where the declaration is simply invalid and skipped. */
    height: 100vh;
    height: 100dvh;
    padding-top: calc(var(--space-4) + env(safe-area-inset-top));
    padding-bottom: max(var(--space-4), env(safe-area-inset-bottom));
    padding-left: max(var(--space-4), env(safe-area-inset-left));
    padding-right: var(--space-4);
    box-shadow: var(--shadow-overlay);
    transform: translateX(-100%);
    transition: transform .32s cubic-bezier(.4, 0, .2, 1);
  }
  .sidebar.mobile-menu-open { transform: translateX(0); }
  /* Dims and blocks the page behind the open drawer, and is the tap target
     for "close it by tapping outside" -- the standard way an overlay this
     size gets dismissed on a phone, where there is no space to show both the
     menu and a visible edge of the page at once. Sizing/positioning only
     here -- the base display:none rule sits unconditionally next to
     .sidebar-close-btn's near the top of the file, not inside this query;
     see its own comment for why that placement is load-bearing.

     display flips to block unconditionally IN THIS QUERY (position:fixed
     already takes it out of flow here, so the desktop grid bug that
     comment describes can't recur) -- opacity/visibility do the actual
     show/hide, which is what makes the dim fade rather than pop. */
  .mobile-nav-backdrop {
    position: fixed; inset: 0; z-index: 39; display: block;
    background: rgba(0, 0, 0, .45);
    opacity: 0; visibility: hidden;
    transition: opacity .28s cubic-bezier(.4, 0, .2, 1), visibility 0s linear .28s;
  }
  .sidebar.mobile-menu-open + .mobile-nav-backdrop {
    opacity: 1; visibility: visible;
    transition: opacity .28s cubic-bezier(.4, 0, .2, 1);
  }
  /* The page behind an open overlay must not scroll out from under it --
     otherwise a swipe meant for the drawer's own nav list can instead move
     the page underneath the dimmed backdrop, which feels like the menu
     glitching rather than like anything intentional. .mobile-nav-open is
     stamped on <html> only while the drawer is open (see base.html), so this
     never lingers once it closes. */
  html.mobile-nav-open, html.mobile-nav-open body { overflow: hidden; }

  /* The More menu anchors right:0 to its own button, which on a desktop row
     puts it under the button's right edge. On a phone the action row wraps and
     that button sits near the LEFT, so a 190px menu hung off right:0 lands at
     left:-98 -- half of it past the edge of the screen, which is what it was
     doing. Anchored to the left edge instead, and bounded, so it opens inward
     wherever the button has wrapped to. */
  .session-more-menu {
    left: 0; right: auto;
    max-width: calc(100vw - 2 * var(--space-5));
  }
  .mobile-nav-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); }
  .brand { font-size: var(--text-subsection); }
  .brand-mark { width: 26px; height: 26px; font-size: var(--text-ui); }
  .main-content {
    padding-top: var(--space-4);
    padding-left: max(var(--space-5), env(safe-area-inset-left));
    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: var(--text-label); 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: var(--text-ui); 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: var(--text-label); 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%; }
  /* A desktop page title plus its stacked subtitle and margins used to cost
     ~160px of a phone screen before any content. The title itself used to
     shrink here too (from 32px); at the guide's 28px it no longer needs to,
     and the guide gives H1 no smaller size -- the space comes back from the
     tighter gaps and the smaller subtitle instead. */
  .page-header { gap: var(--space-3); margin-bottom: var(--space-4); }
  h1, .page-header h1 { margin: 0; }
  .page-header p { font-size: var(--text-ui); }
  /* 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: var(--text-label); }
  .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;
  }
  /* The rail's own controls, which this block never covered. .nav-link was
     already handled just above (and .bucket-tree-link is a .nav-link, so it
     comes along); these are the ones that were left behind.
     .nav-subsection-header is the worst of them -- a real <summary> control
     computing to roughly 23px, i.e. under WCAG 2.2 SC 2.5.8's 24px floor, not
     merely under the 44px the house guide asks for.
     Scoped strictly to a coarse pointer: the dense ~31px mouse rhythm is a
     deliberate "a nav rail is an index you scan, not a page you read" call and
     stays correct for a mouse. */
  .nav-subsection-header, .sidebar-user, .sidebar-footer-row,
  .sidebar-collapse-btn {
    min-height: 44px;
  }
  .sidebar-collapse-btn { min-width: 44px; }
  .bucket-group-items { gap: var(--space-1); }
}

/* Injected by base.html only when the page is running as a home-screen
   app installed back when the manifest still asked for standalone mode --
   the mode in which iOS mutes the mic seconds into a screen lock. Pinned
   to the bottom rather than the top so it can't collide with the status
   bar (the other thing standalone mode breaks), and deliberately not
   dismissible: it is describing the app silently failing at the one job
   the phone is for. --pending-fill is the app's "needs attention" solid,
   already paired with white text and constant across both themes. */
.standalone-warning {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  background: var(--pending-fill); color: #fff;
  padding: var(--space-3) var(--space-4);
  padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom));
  font-size: var(--text-ui-sm); line-height: 1.45;
  box-shadow: 0 -2px 12px rgba(0, 0, 0, .28);
}
/* Each part on its own line. Left inline, the button landed mid-sentence
   ("...mutes the [Open in the browser] microphone in that mode.") -- it
   read as a link inside prose rather than the way out of the problem the
   paragraph is describing. */
.standalone-warning { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-2); }
.standalone-warning strong { font-weight: 700; }
/* White-on-amber rather than the app's own button colors: this sits on a
   solid --pending-fill block, where every accent in the palette is either
   invisible or a contrast problem. */
.standalone-warning-btn {
  align-self: stretch; text-align: center;
  padding: var(--space-3) var(--space-4);
  background: #fff; color: var(--pending-fill);
  border-radius: var(--radius-sm); text-decoration: none;
  font-weight: 700; min-height: 44px; line-height: 20px;
}

/* ---- Live suggestions (_partials/suggestions.html) ------------------
   The one panel in the app meant to be read while somebody else is
   talking, which is why it breaks the usual panel rhythm: the words to
   say are set larger than body copy and the reason is set smaller, so
   the two can be told apart without reading either. The left rail is
   --pending rather than --info because this is "needs your attention
   now", which is exactly what --pending means everywhere else here. */
.suggestions-panel { border-left: 3px solid var(--pending); }
.suggestions-caption { color: var(--muted); font-size: var(--text-ui-sm); }

.suggestion-list { display: flex; flex-direction: column; gap: var(--space-3); }
/* ~5 rows, then scroll. Each pass returns at most 3 but passes run every 45
   seconds, so open cards accumulate; uncapped, this grew to twenty-odd rows
   and pushed the notes off the screen. A short scroll inside the panel beats
   scrolling the whole page while somebody is talking.
   Capped by height rather than by truncating the list server-side: a flagged
   card must stay reachable no matter how many unflagged ones are above it. */
.suggestion-list { max-height: 19rem; overflow-y: auto; scrollbar-width: thin; }
/* A flag is "I am still going to say this", so it reads as held, not as new:
   the orange marker is the same "you are here" language the nav rail uses. */
.suggestion.is-flagged {
  background: var(--pending-bg);
  box-shadow: inset 3px 0 0 var(--pending-dot);
  border-radius: var(--radius-sm);
}
.suggestion-flag { display: inline-flex; align-items: center; gap: .35em; }
.suggestion-flag[aria-pressed="true"] { color: var(--pending); font-weight: 700; }

.suggestion {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-3); border: 1px solid var(--line);
  border-radius: var(--radius-md); background: var(--soft);
}

/* Big enough to take in at a glance from a second monitor, and set in
   the body face rather than quoted -- these are your words, not a
   citation of somebody else's. */
.suggestion-say { margin: 0; font-size: var(--text-subsection); line-height: 1.45; color: var(--ink); }
.suggestion-why { margin: 0; font-size: var(--text-ui-sm); line-height: 1.4; color: var(--muted); }

.suggestion-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.suggestion-actions form { margin: 0; }

.suggestion-error { margin: 0; font-size: var(--text-ui-sm); color: var(--danger); }


/* ---- Admin calendar rows (templates/admin.html) ---------------------
   Three controls per row -- a switch, a bucket picker and a destructive
   action -- which is more than .row-actions was built for, so they wrap
   and keep their baselines instead of squeezing onto one line. */
.calendar-admin-row .row-actions { flex-wrap: wrap; align-items: center; gap: var(--space-3); }
.calendar-admin-row .row-actions form { margin: 0; }
.calendar-admin-row .section-hint { margin-top: var(--space-1); }

/* ---- Calendar (templates/calendar.html) -----------------------------
   The status card carries a tone stripe rather than a coloured panel:
   this page is read at a glance to answer "is it working", and a full
   wash of red on a page that is otherwise fine reads as an alarm about
   the whole app rather than about one connection. */
.calendar-status { border-left: 3px solid var(--line); }
.calendar-status--ok { border-left-color: var(--live); }
.calendar-status--waiting { border-left-color: var(--pending); }
.calendar-status--bad { border-left-color: var(--danger); }

.calendar-status-head {
  display: flex; flex-wrap: wrap; gap: var(--space-3);
  align-items: flex-start; justify-content: space-between;
}
.calendar-status-head h2 { margin: 0; }
.calendar-status-who { margin: var(--space-1) 0 0; color: var(--muted); }
.calendar-status-detail { margin: var(--space-3) 0 0; line-height: var(--text-body-lh); max-width: 68ch; }

/* The payload, kept but folded away. Wraps and scrolls in its own box so
   a 400-character Microsoft error can never push the page sideways. */
.calendar-technical { margin-top: var(--space-3); }
.calendar-technical summary { cursor: pointer; color: var(--muted); font-size: var(--text-ui); }
.calendar-technical p {
  margin: var(--space-2) 0 0; padding: var(--space-3);
  background: var(--soft); border-radius: var(--radius-sm);
  font-family: var(--font-mono); font-size: var(--text-ui-sm); line-height: 1.5;
  overflow-wrap: anywhere; max-height: 14rem; overflow-y: auto;
}

/* One switch, its name, and the consequence of turning it on -- the help
   text belongs to the control, not to a paragraph somewhere below it. */
.calendar-switch .checkbox-label { align-items: flex-start; gap: var(--space-3); }
.calendar-switch-text { display: flex; flex-direction: column; gap: var(--space-1); }
.calendar-switch-help { color: var(--muted); font-size: var(--text-ui); line-height: 1.5; max-width: 62ch; }
/* .section-hint carries a global margin-top:-8px, which exists to tuck a
   hint under a single-line form row. This switch is a three-line block,
   so that pull lands the hint 8px INSIDE the help text. Corrected here
   rather than by touching the global rule, which is right everywhere
   else it is used. */
.calendar-switch + .section-hint { margin-top: var(--space-2); }

.calendar-day { padding-top: var(--space-3); }
.calendar-day-label {
  margin: 0 0 var(--space-2); font-size: var(--text-ui-sm); letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
}
.calendar-event h4 { margin: 0; font-size: var(--text-body); }
/* Tabular so the times down a day line up as a column. */
.calendar-event-time { font-variant-numeric: tabular-nums; }
.calendar-event-action { display: flex; align-items: center; gap: var(--space-2); }
.calendar-event-action form { margin: 0; }
/* Declined and cancelled meetings stay listed -- seeing that wired is
   deliberately skipping one is the point -- but recede. */
.calendar-event.is-skipped h4 { color: var(--muted); }
