/* =================================================================
   GLAZE TOKENS — ported directly from gridloop-mockups/mockups.html,
   the approved brand reference. Light mode only for now (dark mode is
   specified in the source brand guide, not yet built).
   ================================================================= */
:root {
  --canvas: #E7DFD1;
  --tile: #FBF8F2;
  --ink: #33465A;
  --muted: #6D7F8F;

  --sage: #7C9C8C;
  --clay: #C97B63;
  --honey: #DDA23E;

  --danger: #A6342E;
  --warning: #A85F26;
  --success: #3F7A56;
  --info: #3B6488;
  --focus: #3B6488;

  /* Bare "R,G,B" companions of the tokens above, so hardcoded tints/overlays
     scattered through this file (a faint border, a hover-state background,
     etc.) can be written as rgba(var(--ink-rgb),0.06) instead of a literal
     rgba(51,70,90,0.06) — letting them flip automatically under
     [data-theme="dark"] below, the same way a plain var(--ink) reference
     already would. */
  --ink-rgb: 51,70,90;
  --muted-rgb: 109,127,143;
  --sage-rgb: 124,156,140;
  --clay-rgb: 201,123,99;
  --honey-rgb: 221,162,62;
  --danger-rgb: 166,52,46;
  --warning-rgb: 168,95,38;
  --success-rgb: 63,122,86;
  --info-rgb: 59,100,136;

  /* Deliberately constant across both themes (never redefined under
     [data-theme="dark"]) — each one below has a job that stays the same
     regardless of which theme is active: */
  --scrim-rgb: 51,70,90;      /* dimming overlays (modal/sheet backdrops, tour
                                  spotlight, video-tile label backing) must
                                  stay dark in both themes — the opposite of
                                  --ink-rgb, which flips light in dark mode */
  --gloss-rgb: 255,255,255;   /* white shine/gloss highlights (shadow inset
                                  highlight, wordmark emboss, glaze-sweep) */
  --on-accent: #FBF8F2;       /* light text/icons drawn on a solid saturated
                                  fill (danger/success/warning/info pills and
                                  badges) — needs to stay light regardless of
                                  theme, since those fills stay saturated */
  --ink-on-accent: #33465A;   /* dark text drawn on a solid sage/clay/honey
                                  fill (pills, primary button, active nav
                                  states, own-message bubble) — fixed to
                                  today's --ink value since sage/clay/honey
                                  themselves don't change in dark mode, but
                                  --ink does */

  --radius-pill: 9999px;
  --radius-card: 20px;
  --radius-modal: 24px;

  /* .notif-menu (right: 4.2rem, 38px wide) + .settings-menu (right: 1.5rem,
     38px wide) are both position:fixed at top:1rem, always floating over
     page content regardless of viewport width — together they occupy the
     rightmost ~6.6rem of the viewport, in the topmost ~3.4rem. A page
     header's own action-button row sits at roughly that same height (right
     after .main-panel's top padding), so on a desktop window narrow enough
     that the row's content reaches this same horizontal band — narrower
     than full-width monitors, but still well above the mobile breakpoint —
     a button can end up rendered directly underneath one of these icons.
     Reserving this as right-side padding on that header row means content
     wraps (or just stops short) before ever reaching that zone, rather
     than colliding with it. */
  --fixed-icon-clearance: 7rem;

  --shadow-tile: 0 10px 20px rgba(51,70,90,0.12), inset 0 1px 0 rgba(255,255,255,0.6);
  --shadow-modal: 0 16px 32px rgba(51,70,90,0.18), inset 0 1px 0 rgba(255,255,255,0.6);

  --font: 'Roboto', -apple-system, "Segoe UI", Oxygen, Ubuntu, Helvetica, Arial, sans-serif;
}

/* =================================================================
   DARK MODE — same token names as the light :root block above, values
   only. Applied by setting data-theme="dark" on <html> (see the inline
   anti-flash script in index.html's <head> and the toggle wiring in
   app.js); everything else in this file references these tokens via
   var(...) and repaints automatically once the attribute flips.
   Kept warm (dark charcoal, not pure black) to match the light theme's
   own warm, editorial palette rather than a generic dark-mode default.
   ================================================================= */
:root[data-theme="dark"] {
  --canvas: #211D19;
  --tile: #2B2621;
  --ink: #EDE6DA;
  --muted: #9C8F80;

  /* sage/clay/honey intentionally unchanged — already muted mid-tones that
     read well as accents against a dark charcoal ground, and keeping them
     fixed means --ink-on-accent (also fixed) stays exactly as legible on
     them as it is today. */

  /* danger/warning/success/info/focus are tuned in light mode to work as
     *text on a light background* (dark, muted tones) — on a dark canvas
     they're nearly invisible. Brightened so they read clearly both as
     status text on dark tile and as pill/badge fills under --on-accent. */
  --danger: #E2685C;
  --warning: #E0A34A;
  --success: #4FB07A;
  --info: #5B9BD1;
  --focus: #5B9BD1;

  --ink-rgb: 237,230,218;
  --muted-rgb: 156,143,128;
  --danger-rgb: 226,104,92;
  --warning-rgb: 224,163,74;
  --success-rgb: 79,176,122;
  --info-rgb: 91,155,209;

  /* A navy-tinted shadow at these alphas is invisible against a dark
     canvas — needs near-black at higher alpha. Gloss highlight dialed way
     down so it reads as a subtle sheen rather than a glary white line. */
  --shadow-tile: 0 10px 20px rgba(0,0,0,0.45), inset 0 1px 0 rgba(var(--gloss-rgb),0.05);
  --shadow-modal: 0 16px 32px rgba(0,0,0,0.55), inset 0 1px 0 rgba(var(--gloss-rgb),0.05);
}

/* ---- Dark-mode special cases — colors baked into a fixed rule (not a
   var(...) reference) that var(--token) substitution alone can't reach,
   plus third-party (Quill) chrome that needs its own override layer. ---- */

/* The embossed/pressed-into-paper look was tuned for a light tile surface —
   dark shadow below, bright highlight above. On a dark tile, the analogous
   "engraved" look needs a near-black shadow and only a faint light edge. */
[data-theme="dark"] .wordmark {
  text-shadow: 0 -1px 0 rgba(0,0,0,0.5), 0 1px 0 rgba(var(--gloss-rgb),0.08);
}

/* Graph-paper texture's stroke color is baked into the data-URI string
   itself — can't reference var(--ink-rgb) inside url(), so it needs its own
   dark-mode background-image with a light-on-dark stroke instead. */
[data-theme="dark"] .sidebar {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath d='M 24 0 L 0 0 0 24' fill='none' stroke='rgba(255,255,255,0.05)' stroke-width='1'/%3E%3C/svg%3E");
}

/* Same data-URI limitation for the custom <select> arrow — recolored to
   match the dark-mode --muted value (URL-encoded, since it's baked into an
   SVG string, not a live var() reference). */
[data-theme="dark"] select.field-input,
[data-theme="dark"] .team-select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239C8F80' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
}

/* Quill's own quill.snow.css (loaded from a CDN, index.html <head>, before
   this stylesheet) draws a light-only toolbar/editor chrome with colors we
   don't control at the source — these rules win on source order (loaded
   after) without needing !important. Best-effort: covers the toolbar icons/
   picker dropdowns/link-tooltip, but hasn't been visually verified in a
   browser (no screenshot tooling available while building this). */
[data-theme="dark"] .note-body .ql-toolbar.ql-snow,
[data-theme="dark"] .sheet-notes-body .ql-toolbar.ql-snow { background: var(--tile); }
[data-theme="dark"] .ql-toolbar.ql-snow .ql-stroke { stroke: var(--muted); }
[data-theme="dark"] .ql-toolbar.ql-snow .ql-fill { fill: var(--muted); }
[data-theme="dark"] .ql-toolbar.ql-snow button:hover .ql-stroke,
[data-theme="dark"] .ql-toolbar.ql-snow button.ql-active .ql-stroke { stroke: var(--ink); }
[data-theme="dark"] .ql-toolbar.ql-snow button:hover .ql-fill,
[data-theme="dark"] .ql-toolbar.ql-snow button.ql-active .ql-fill { fill: var(--ink); }
[data-theme="dark"] .ql-toolbar.ql-snow .ql-picker-label { color: var(--ink); }
[data-theme="dark"] .ql-snow .ql-picker-options { background: var(--tile); border-color: rgba(var(--ink-rgb),0.18); }
[data-theme="dark"] .ql-snow .ql-picker-item { color: var(--ink); }
[data-theme="dark"] .ql-editor.ql-blank::before { color: var(--muted); }
[data-theme="dark"] .ql-snow .ql-tooltip { background: var(--tile); color: var(--ink); border-color: rgba(var(--ink-rgb),0.18); box-shadow: var(--shadow-modal); }
[data-theme="dark"] .ql-snow .ql-tooltip input[type=text] { background: var(--canvas); color: var(--ink); border-color: rgba(var(--ink-rgb),0.18); }

* { box-sizing: border-box; }
/* Only .board should ever scroll horizontally (many category columns) —
   without this, some sub-pixel/rounding overflow lets the whole page shift
   sideways too, taking the sidebar along with it. Kept off body: Safari
   breaks position: sticky on any descendant (like .sidebar) when body has
   a non-visible overflow, so the actual clipping happens on .main-panel
   instead — a sibling of .sidebar under #app-shell, not an ancestor of it,
   so it can't trip that bug while still containing anything that overflows
   within it (e.g. a too-wide queue row) before it reaches html's own edge. */
html { overflow-x: hidden; }
body { margin: 0; font-family: var(--font); background: var(--canvas); color: var(--ink); }
h1, h2, h3 { font-family: var(--font); font-weight: 700; margin: 0; color: var(--ink); }
.hidden { display: none !important; }

/* Micro-texture — signature moment 6 */
.tile-surface { position: relative; background: var(--tile); overflow: hidden; }
.tile-surface::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  opacity: 0.05; mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Pressed wordmark — signature moment 1 */
.wordmark {
  font-weight: 700; font-size: 1.15rem; color: var(--ink);
  text-shadow: 0 -1px 0 rgba(51,70,90,0.3), 0 1px 0 rgba(255,255,255,0.85);
}
.logo-lockup { display: flex; align-items: center; gap: 0.6rem; }
/* The new mark is a wide isometric loop (~1.8:1), not a square glyph like
   the old one — fixed height + auto width keeps it proportional instead
   of squashing it into the old fixed 30x30 box. */
.logo-lockup img { height: 28px; width: auto; }

/* ---- App shell ---- */
.app-shell { display: flex; min-height: 100vh; }
.sidebar {
  width: 250px; flex-shrink: 0; background: var(--tile);
  border-right: 1px solid rgba(var(--ink-rgb),0.1); padding: 1.25rem 1rem;
  /* Sits above the task-detail sheet (Layer Two, meant to be a contextual,
     non-blocking overlay per the architecture doc) so nav stays reachable
     in a single click while it's open. Real modals (add task/space/team)
     keep the default .sheet-overlay z-index and still block everything,
     which is correct for them. */
  position: sticky; top: 0; z-index: 15;
  /* Own scroll container, capped to the viewport height — otherwise a long
     board/queue in .main-panel would scroll the whole page (sidebar
     included) out from under the fold instead of just its own content.
     Overridden below the 720px breakpoint (off-canvas drawer instead). */
  height: 100vh; overflow-y: auto;
  /* Faint graph-paper texture — a 24px grid, each cell's top+left edge
     drawn so adjacent tiles form one continuous mesh when repeated. Opacity
     is baked into the stroke color (rgba) rather than a separate CSS
     opacity, so it doesn't also fade .tile-surface's own noise texture
     (that's a sibling ::before layer, unaffected by this background-image). */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath d='M 24 0 L 0 0 0 24' fill='none' stroke='rgba(51,70,90,0.06)' stroke-width='1'/%3E%3C/svg%3E");
}
/* ---- Global settings menu — a single persistent entry point (top-right,
   every view) for account-level actions that don't belong to any one
   page: team management, the guided tour, logout, account deletion. Sits
   in #app-shell as a sibling of .sidebar/.main-panel, not inside either,
   so its visibility is tied only to being logged in, not to whichever
   view happens to be showing. ---- */
/* z-index 19: above .sidebar-backdrop (17) and the off-canvas .sidebar
   itself (18, mobile only), so the gear stays reachable in a single tap
   even while the mobile drawer/backdrop is open instead of the full-
   viewport backdrop swallowing the tap and just closing the drawer. */
.settings-menu { position: fixed; top: 1rem; right: 1.5rem; z-index: 19; }
.settings-menu-btn {
  width: 38px; height: 38px; border-radius: 50%; background: var(--tile);
  border: 1px solid rgba(var(--ink-rgb),0.15); box-shadow: var(--shadow-tile);
  color: var(--muted); display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.settings-menu-btn:hover { color: var(--ink); }
.settings-menu-btn .icon { width: 1.15em; height: 1.15em; }
.settings-menu-dropdown {
  position: absolute; top: calc(100% + 0.5rem); right: 0; width: 200px;
  background: var(--tile); border-radius: var(--radius-card); box-shadow: var(--shadow-modal);
  padding: 0.4rem; display: flex; flex-direction: column; gap: 0.1rem;
}
.settings-menu-item {
  display: flex; align-items: center; gap: 0.5rem; width: 100%; text-align: left;
  padding: 0.55rem 0.7rem; border-radius: 8px; border: none; background: none; cursor: pointer;
  color: var(--ink); font-family: var(--font); font-size: 0.88rem; font-weight: 500;
}
.settings-menu-item:hover { background: rgba(var(--ink-rgb),0.06); }
.settings-menu-item.is-danger { color: var(--danger); }

.notif-menu { position: fixed; top: 1rem; right: 4.2rem; z-index: 19; }
.notif-menu-btn {
  position: relative; width: 38px; height: 38px; border-radius: 50%; background: var(--tile);
  border: 1px solid rgba(var(--ink-rgb),0.15); box-shadow: var(--shadow-tile);
  color: var(--muted); display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.notif-menu-btn:hover { color: var(--ink); }
.notif-menu-btn .icon { width: 1.15em; height: 1.15em; }
.notif-badge {
  position: absolute; top: -2px; right: -2px; min-width: 16px; height: 16px; padding: 0 3px;
  border-radius: 8px; background: var(--danger); color: var(--on-accent); font-size: 0.68rem; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
}
.notif-menu-dropdown {
  position: absolute; top: calc(100% + 0.5rem); right: 0; width: 300px; max-height: 360px; overflow-y: auto;
  background: var(--tile); border-radius: var(--radius-card); box-shadow: var(--shadow-modal); padding: 0.4rem;
}
.notif-menu-empty { padding: 1rem 0.7rem; color: var(--muted); font-size: 0.84rem; text-align: center; }
.notif-item { padding: 0.6rem 0.7rem; border-radius: 8px; font-size: 0.84rem; }
.notif-item:not(:last-child) { border-bottom: 1px solid rgba(var(--ink-rgb),0.08); }
.notif-item-clickable { cursor: pointer; }
.notif-item-clickable:hover { background: rgba(var(--ink-rgb),0.05); }
.notif-item-text { color: var(--ink); margin-bottom: 0.4rem; }
.notif-item-actions { display: flex; gap: 0.5rem; }
.notif-item-actions button {
  flex: 1; padding: 0.35rem 0; border-radius: 6px; border: none; cursor: pointer;
  font-family: var(--font); font-size: 0.8rem; font-weight: 600;
}
.notif-item-accept { background: var(--success); color: var(--on-accent); }
.notif-item-decline { background: none; color: var(--muted); border: 1px solid rgba(var(--ink-rgb),0.2) !important; }

/* ---- Mobile sidebar drawer trigger — hidden on desktop (the sidebar is
   normal in-flow content there); shown only under the 720px breakpoint
   below, where the sidebar becomes an off-canvas panel instead. Sits at
   top-left so it never competes with .settings-menu, which stays top-right
   at every width. ---- */
.sidebar-toggle-btn {
  /* Same z-index reasoning as .settings-menu above — stays reachable (as a
     close trigger) even while its own backdrop is up. */
  display: none; position: fixed; top: 1rem; left: 1.5rem; z-index: 19;
  width: 38px; height: 38px; border-radius: 50%; background: var(--tile);
  border: 1px solid rgba(var(--ink-rgb),0.15); box-shadow: var(--shadow-tile);
  color: var(--muted); align-items: center; justify-content: center; cursor: pointer;
}
.sidebar-toggle-btn .icon { width: 1.15em; height: 1.15em; }
/* Fixed to the viewport, so once the drawer slides open (also fixed, but
   animated in from off-canvas) this button stayed put in the exact same
   screen position — sitting right on top of the drawer's own logo row
   instead of moving out of the way with it. Hide it whenever the drawer is
   open; #sidebar-close-btn (inside the drawer's own logo row, so it slides
   with the panel) takes over as the way to close it. */
#app-shell:has(.sidebar.is-open) .sidebar-toggle-btn { display: none; }
.sidebar-close-btn {
  display: none; background: none; border: none; color: var(--muted);
  cursor: pointer; padding: 0.2rem; line-height: 1; flex-shrink: 0;
}
.sidebar-close-btn:hover { color: var(--ink); }
.sidebar-close-btn .icon { width: 1.2em; height: 1.2em; }
.sidebar-backdrop {
  display: none; position: fixed; inset: 0; background: rgba(20,26,33,0.35);
  z-index: 17; opacity: 0; pointer-events: none;
  /* Deliberately quicker than .sidebar's own 0.32s slide — the backdrop
     dims in first, then the panel glides in over it, instead of both
     finishing at the same moment and reading as one flat motion. */
  transition: opacity 0.22s ease-out;
}
.sidebar-backdrop.is-open { opacity: 1; pointer-events: auto; }

.sidebar-section-label {
  font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--ink); font-weight: 700; margin: 1.1rem 0 0.4rem;
  display: flex; align-items: center; gap: 0.35rem;
}
.space-link {
  display: block; padding: 0.4rem 0.65rem; border-radius: 10px;
  color: var(--ink); font-size: 0.9rem; margin-bottom: 2px; text-decoration: none;
  cursor: pointer; border: none; background: none; width: 100%; text-align: left; font-family: var(--font);
}
.space-link:hover { background: rgba(var(--ink-rgb),0.06); }
.space-link.is-active { background: var(--sage); color: var(--ink-on-accent); font-weight: 600; }

.team-select {
  width: 100%; padding: 0.45rem 0.6rem; border-radius: var(--radius-pill);
  border: 1px solid rgba(var(--ink-rgb),0.15); background: var(--tile); color: var(--ink);
  font-family: var(--font); font-size: 0.85rem; margin-bottom: 1.1rem;
}

/* min-width: 0 overrides the flex-item default of min-width: auto — without
   it, a wide-content child like .board (which can now hold arbitrarily many
   category columns) would grow this whole panel past the viewport instead
   of scrolling internally. */
.main-panel { flex: 1; min-width: 0; padding: 2rem 2.25rem; overflow-x: hidden; }
.page-title { font-size: 1.4rem; margin-bottom: 0.2rem; }
.page-subtitle { color: var(--muted); font-size: 0.85rem; margin-bottom: 1.5rem; }

/* Space-view header: title sits on its own line (rather than sharing a row
   with the action buttons, which used to squeeze both together and wrap
   messily at anything less than a wide desktop width) with clear margin
   before the buttons below it. Buttons are split into two groups — the
   space's primary actions (call/add task) and its management actions
   (categories/edit/delete) — so the row reads as two related clusters
   instead of one long undifferentiated wrapped line. No divider between
   the groups: at roughly 420-690px the outer row wraps BETWEEN the two
   groups (not just within one), which left an earlier border-left version
   of this floating at the start of the second row with nothing beside it
   — a stray line, not a divider. The gap plus each group's own distinct
   button colors (primary green vs. neutral vs. danger red) already imply
   the grouping without needing a border that can't reliably tell which
   row it's about to render on. Button size/padding (hence touch target)
   is untouched from the shared .btn rule. */
/* ---- Space toolbar card (board and note headers both use this) — wraps
   the primary action row (and, for boards, a second row of view controls
   below a hairline divider) in one contained surface instead of loose
   pills floating directly on the canvas. */
.space-toolbar-card { background: var(--tile); border-radius: var(--radius-card); box-shadow: var(--shadow-tile); padding: 1rem 1.1rem 1.1rem; margin: 1rem 0 1.25rem; }
.space-header-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 0.6rem; }
/* flex: 1 on both of these (not a single .toolbar-spacer on one side) is
   what actually centers #board-view-mode-segmented between them — a
   spacer on only one side centers relative to whichever side is empty,
   not relative to the row as a whole. */
.space-header-actions-side { display: flex; align-items: center; gap: 0.6rem; flex: 1; min-width: 0; }
.space-header-actions-end { justify-content: flex-end; }
.toolbar-spacer { flex: 1; }
.toolbar-label { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); flex-shrink: 0; }
/* border-top lives on .board-toolbar-row (not border-bottom on
   .space-header-actions above it) so a card with no second row — the
   note-view header, which has no grouping/filtering — never renders an
   orphaned divider line with nothing below it. */
.board-toolbar-row { display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap; border-top: 1px solid rgba(var(--ink-rgb),0.1); margin-top: 0.9rem; padding-top: 0.9rem; }
.bulk-tag-bar { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; border-top: 1px solid rgba(var(--ink-rgb),0.1); margin-top: 0.9rem; padding-top: 0.9rem; }
.bulk-tag-bar #bulk-tag-count { font-size: 0.82rem; font-weight: 600; color: var(--muted); flex-shrink: 0; }
.board-card-select { display: block; margin-bottom: 0.5rem; }
.grid-cell-select { flex-shrink: 0; width: 1px; white-space: nowrap; }
/* Group by/Filter only mean anything for the column view — Grid mode hides
   this whole cluster (via #board-view-mode-segmented's click handler in
   app.js) rather than leaving controls visible that don't do anything. */
.board-groupby-controls { display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap; flex: 1; min-width: 0; }
.filter-badge {
  position: absolute; top: -2px; right: -2px; min-width: 16px; height: 16px; padding: 0 3px;
  border-radius: 8px; background: var(--sage); color: var(--ink-on-accent); font-size: 0.68rem; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
}

/* ---- Segmented control (replaces the old native <select> for board
   grouping) — a track of equal-weight options with the active one raised
   on its own tile, same visual language as everything else on --tile
   surfaces (shadow-tile, not a full elevation shadow, since it's a small
   in-page control, not a floating one). */
.segmented { display: inline-flex; background: rgba(var(--ink-rgb),0.1); border-radius: var(--radius-pill); padding: 0.2rem; gap: 0.15rem; }
.segmented button {
  font-family: inherit; font-size: 0.8rem; font-weight: 600; border: none; background: none; color: var(--muted);
  padding: 0.4rem 0.75rem; border-radius: var(--radius-pill); cursor: pointer; white-space: nowrap;
}
.segmented button:hover { color: var(--ink); }
.segmented button.is-active { background: var(--tile); color: var(--ink); box-shadow: 0 1px 3px rgba(var(--ink-rgb),0.18); }
/* Board/Grid now sits in the primary action row next to Add task/Start
   call (both .btn-sized), not tucked into the secondary Group-by row —
   sized up to match rather than reading as a smaller, lesser control
   beside them. */
.segmented-lg { padding: 0.25rem; flex-shrink: 0; }
.segmented-lg button { font-size: 0.92rem; padding: 0.6rem 1.15rem; }
/* Five segments don't reliably fit a phone width without wrapping onto a
   cramped extra line — scrolls horizontally instead, with the same
   fade-to-edge mask already used on .message-list, so a cut-off segment
   reads as "more this way" rather than looking clipped. Harmless (mask has
   nothing to fade) whenever the segments already fit without scrolling. */
.segmented-scroll { overflow-x: auto; flex: 0 1 auto; min-width: 0; scrollbar-width: none; -ms-overflow-style: none; }
.segmented-scroll::-webkit-scrollbar { display: none; }
.segmented-scroll {
  mask-image: linear-gradient(to right, black calc(100% - 1.25rem), transparent);
  -webkit-mask-image: linear-gradient(to right, black calc(100% - 1.25rem), transparent);
}

/* ---- Queue stat widgets ---- */
#queue-stats { display: flex; gap: 0.9rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
.queue-stat-tile {
  flex: 1 1 140px; background: var(--tile); border-radius: var(--radius-card); box-shadow: var(--shadow-tile);
  padding: 0.9rem 1rem;
}
.queue-stat-value { font-size: 1.6rem; font-weight: 700; color: var(--ink); line-height: 1.1; }
.queue-stat-value.is-overdue { color: var(--danger); }
.queue-stat-label { font-size: 0.78rem; color: var(--muted); margin-top: 0.2rem; }

/* ---- Queue rows ---- */
/* flex-wrap so a task with several tag pills overflows onto a second line
   instead of squeezing the title column (flex:1, no minimum width) down to
   a sliver it has to wrap word-by-word to fit. */
.queue-row {
  display: flex; align-items: center; gap: 0.6rem 0.9rem; flex-wrap: wrap;
  border-radius: var(--radius-card); box-shadow: var(--shadow-tile);
  padding: 0.9rem 1.15rem; margin-bottom: 0.7rem; cursor: pointer;
}
.task-title { font-weight: 500; color: var(--ink); font-size: 0.94rem; }
.task-title.is-done { text-decoration: line-through; color: var(--muted); }
.space-name-muted { color: var(--muted); font-size: 0.8rem; margin-top: 1px; }

/* ---- Status / priority pills ---- */
.pill {
  font-size: 0.72rem; font-weight: 600; padding: 0.22rem 0.7rem;
  border-radius: var(--radius-pill); white-space: nowrap;
  display: inline-flex; align-items: center; gap: 0.3rem;
  /* Category names and tags are both free-form/user-authored text with no
     hard length limit enforced everywhere (tags are capped server-side,
     but this also covers any legacy data from before that cap existed) —
     truncate rather than let a long value overflow the card/row. */
  max-width: 12rem; overflow: hidden; text-overflow: ellipsis;
}
.pill .icon { width: 0.85em; height: 0.85em; flex-shrink: 0; }
.pill-sage   { background: var(--sage);  color: var(--ink-on-accent); }
.pill-clay   { background: var(--clay);  color: var(--ink-on-accent); }
.pill-honey  { background: var(--honey); color: var(--ink-on-accent); }
.pill-todo   { background: transparent; color: var(--muted); border: 1px solid rgba(var(--muted-rgb),0.4); }
.pill-danger  { background: var(--danger);  color: var(--on-accent); }
.pill-success { background: var(--success); color: var(--on-accent); }
.pill-warning { background: var(--warning); color: var(--on-accent); }
.pill-info    { background: var(--info);    color: var(--on-accent); }

.avatar {
  width: 26px; height: 26px; border-radius: 50%; color: var(--on-accent); font-size: 0.68rem;
  display: flex; align-items: center; justify-content: center; font-weight: 700; flex-shrink: 0;
}
.collab-avatars { display: flex; }
.collab-avatars .avatar { margin-left: -8px; border: 2px solid var(--canvas); }
.collab-avatars .avatar:first-child { margin-left: 0; }
.avatar-unassigned { background: rgba(var(--ink-rgb),0.12); color: var(--muted); border: 1px dashed rgba(var(--ink-rgb),0.3); }
.account-avatar-preview .avatar { width: 56px; height: 56px; font-size: 1.1rem; }

/* ---- Live cursor presence (quill-cursors) on collaborative note pages ----
   quill-cursors' default "name flag" is a wide pill that only shows briefly
   on move/hover — restyled here into a small persistent avatar-style badge
   (matching .avatar above) that stays visible, centered above the caret. */
#note-editor .ql-cursor .ql-cursor-flag {
  opacity: 1 !important;
  visibility: visible !important;
  transition: none !important;
  width: 20px !important;
  height: 20px !important;
  border-radius: 50%;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transform: translate3d(-50%, -130%, 0) !important;
  cursor: default;
}
#note-editor .ql-cursor .ql-cursor-name {
  margin: 0 !important;
  font-size: 0.6rem;
  font-weight: 700;
  color: #fff;
  white-space: nowrap;
}
/* Positioned in JS via getBoundingClientRect() against the hovered badge —
   appended to <body> rather than nested inside it so it isn't clipped by
   quill-cursors' own .ql-container{overflow:hidden}. */
.note-cursor-tooltip {
  position: fixed; transform: translate(-50%, calc(-100% - 8px));
  background: var(--tile); color: var(--ink); border-radius: var(--radius-card);
  box-shadow: var(--shadow-modal); padding: 0.4rem 0.6rem; font-size: 0.8rem;
  white-space: nowrap; pointer-events: none; z-index: 35;
  display: flex; flex-direction: column; gap: 0.1rem;
}
.note-cursor-tooltip.hidden { display: none; }
.note-cursor-tooltip strong { font-weight: 700; }
.note-cursor-tooltip span { color: var(--muted); font-size: 0.72rem; }

/* ---- Task checklist / subtasks ---- */
.checklist-item { display: flex; align-items: center; gap: 0.6rem; padding: 0.3rem 0; }
.checklist-item-text { flex: 1; font-size: 0.9rem; }
.checklist-item-text.is-done { text-decoration: line-through; color: var(--muted); }
.checklist-item-actions { display: flex; gap: 0.1rem; flex-shrink: 0; }
.checklist-icon-btn {
  background: none; border: none; cursor: pointer; color: var(--muted); padding: 0.3rem;
  display: flex; align-items: center; border-radius: 6px;
}
.checklist-icon-btn:hover { color: var(--ink); background: rgba(var(--ink-rgb),0.06); }
.checklist-icon-btn .icon { width: 1rem; height: 1rem; }
/* The chip a promoted checklist item becomes — deliberately styled like a
   link (info color, pill shape) rather than a plain row, so it's visually
   obvious this item now points at a real task elsewhere instead of just
   being text with a checkbox. */
.checklist-chip {
  flex: 1; background: rgba(var(--info-rgb),0.1); color: var(--info); border: none; cursor: pointer;
  font-size: 0.85rem; font-weight: 600; padding: 0.4rem 0.75rem; border-radius: var(--radius-pill);
  display: inline-flex; align-items: center; gap: 0.4rem; text-align: left; max-width: 100%; overflow: hidden;
}
.checklist-chip:hover { background: rgba(var(--info-rgb),0.18); }
.checklist-chip .icon { width: 0.85em; height: 0.85em; flex-shrink: 0; }
.checklist-chip-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.checklist-chip-title.is-done { text-decoration: line-through; opacity: 0.7; }
.promoted-from-chip {
  display: inline-flex; align-items: center; gap: 0.35rem; background: rgba(var(--sage-rgb),0.18);
  color: var(--ink); font-size: 0.78rem; font-weight: 600; padding: 0.3rem 0.65rem;
  border-radius: var(--radius-pill); border: none; cursor: pointer;
}
.promoted-from-chip:hover { background: rgba(var(--sage-rgb),0.3); }

/* ---- Notes — Quill rich text, Yjs-collaborative for space notes via
   y-quill (architecture doc Section 4, mockups Screen 5); plain
   load/save-on-change for per-task notes, no CRDT needed there. Quill
   draws its own toolbar/editor chrome (loaded via the quill.snow.css CDN
   link) — these rules just make it match the rest of this form language
   instead of Quill's stock look. ---- */
/* Note folder/page tree — rendered inline in the left sidebar under the
   active note space's own link (see spaceLink()/renderNoteTreeHtml() in
   app.js), not as a separate panel in the content area, so it reads as
   part of the app's normal navigation instead of a one-off widget. */
.note-sidebar-tree { margin: 0.2rem 0 0.5rem 0.6rem; }
.note-tree-actions { display: flex; gap: 0.3rem; margin-bottom: 0.4rem; }
.note-tree-action-btn {
  flex: 1; font-family: var(--font); font-size: 0.78rem; font-weight: 600; color: var(--muted);
  background: none; border: 1px dashed rgba(var(--ink-rgb),0.3); border-radius: var(--radius-pill);
  padding: 0.4rem 0.65rem; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 0.3rem;
}
.note-tree-action-btn:hover { color: var(--ink); border-color: rgba(var(--ink-rgb),0.5); }
/* Replaces four separate always-visible Page/Folder/Table/Import buttons
   with one "+ Add" trigger — same anchored-dropdown shape as .settings-menu,
   just relative-positioned instead of fixed since it lives inline in the
   sidebar rather than the top-right corner. Item rows reuse
   .settings-menu-item directly rather than duplicating that styling. */
.note-add-menu { position: relative; flex: 1; display: flex; }
.note-add-menu-dropdown {
  position: absolute; top: calc(100% + 0.3rem); left: 0; width: 170px; z-index: 5;
  background: var(--tile); border-radius: var(--radius-card); box-shadow: var(--shadow-modal);
  padding: 0.4rem; display: flex; flex-direction: column; gap: 0.1rem;
}
.note-tree-row {
  display: flex; align-items: center; gap: 0.3rem; padding: 0.3rem 0.4rem; border-radius: 8px;
  cursor: pointer; font-size: 0.82rem; color: var(--ink);
}
.note-tree-row:hover { background: rgba(var(--ink-rgb),0.06); }
.note-tree-row.is-active { background: var(--sage); color: var(--ink-on-accent); font-weight: 600; }
.note-tree-row.is-drag-over { box-shadow: inset 0 0 0 2px var(--sage); }
.note-tree-row.is-dragging { opacity: 0.4; }
.note-tree-toggle {
  width: 0.9rem; flex-shrink: 0; background: none; border: none; cursor: pointer; color: var(--muted);
  font-size: 0.65rem; padding: 0; display: flex; align-items: center; justify-content: center;
}
.note-tree-row .icon { flex-shrink: 0; color: var(--muted); width: 0.95em; height: 0.95em; }
.note-tree-label {
  flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.note-tree-row-actions { display: none; gap: 0.2rem; flex-shrink: 0; }
.note-tree-row:hover .note-tree-row-actions { display: flex; }
.note-tree-icon-btn {
  background: none; border: none; cursor: pointer; color: var(--muted); padding: 0.3rem; display: flex;
  border-radius: 6px;
}
.note-tree-icon-btn:hover { color: var(--ink); background: rgba(var(--ink-rgb),0.08); }
.note-tree-icon-btn .icon { width: 0.9em; height: 0.9em; }
.note-tree-children { margin-left: 0.9rem; }
.note-tree-empty { color: var(--muted); font-size: 0.75rem; padding: 0.3rem 0.4rem; }

.note-column { max-width: 600px; }
.note-page-title { font-size: 1.05rem; margin: 0.2rem 0 0.8rem; color: var(--ink); }
.note-meta { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.2rem; }
.note-meta-text { color: var(--muted); font-size: 0.8rem; }
.note-body { width: 100%; }
.note-body .ql-toolbar.ql-snow {
  border-radius: 10px 10px 0 0; border-color: rgba(var(--ink-rgb),0.18); font-family: var(--font);
}
.note-body .ql-container.ql-snow {
  border-radius: 0 0 10px 10px; border-color: rgba(var(--ink-rgb),0.18);
}
.note-body .ql-editor {
  font-family: var(--font); font-size: 0.96rem; line-height: 1.75; color: var(--ink); min-height: 60vh;
}
.sheet-notes-body .ql-editor { min-height: 140px; }

/* ---- Note table view — a plain versioned grid, not a Yjs doc; see the
   comment on POST /api/spaces/:spaceId/pages in server.js for why. ---- */
.note-table-actions { display: flex; gap: 0.4rem; margin-bottom: 0.75rem; }
.note-table-scroll { overflow-x: auto; max-width: 100%; }
.note-table { border-collapse: collapse; font-size: 0.88rem; }
.note-table th, .note-table td {
  border: 1px solid rgba(var(--ink-rgb),0.15); padding: 0; background: var(--tile);
}
.note-table-corner { background: transparent; border: none; }
.note-table-cell {
  font-family: var(--font); font-size: 0.88rem; color: var(--ink); background: none;
  border: none; padding: 0.5rem 0.6rem; width: 140px; min-width: 100px;
}
.note-table-cell:focus { outline: 2px solid var(--focus); outline-offset: -2px; }
/* The first data row reads as a header purely by styling convention — the
   underlying data is a plain 2D array with no structural distinction, so
   this is the only thing that makes it look like one. */
.note-table tbody tr:first-child .note-table-cell { font-weight: 700; }
.note-table-row-handle, .note-table th { position: relative; }
.note-table-delete-btn {
  background: none; border: none; cursor: pointer; color: var(--muted); padding: 0.3rem;
  display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;
}
.note-table-delete-btn:hover { color: var(--danger); }
.note-table-delete-btn .icon { width: 0.7em; height: 0.7em; }
.note-table-row-handle { width: 1.6rem; background: rgba(var(--ink-rgb),0.04); }

/* ---- Calling — bare-bones P2P, no SFU (architecture doc Section 7) ---- */
.call-toggle-btn { background: transparent; border: 1px solid rgba(var(--ink-rgb),0.3); color: var(--ink); }
.call-toggle-btn.is-active { background: var(--danger); color: var(--on-accent); border-color: transparent; }
.call-panel { margin-bottom: 1.5rem; }
.call-controls { display: flex; gap: 0.5rem; margin-bottom: 0.6rem; }
.call-grid { display: flex; gap: 0.9rem; flex-wrap: wrap; }
.call-tile {
  position: relative; width: 200px; height: 150px; border-radius: var(--radius-card);
  overflow: hidden; box-shadow: var(--shadow-tile); background: var(--ink);
}
.call-tile video { width: 100%; height: 100%; object-fit: cover; display: block; }
.call-tile-label {
  position: absolute; left: 0.5rem; bottom: 0.5rem; font-size: 0.72rem; font-weight: 600;
  color: var(--on-accent); background: rgba(var(--scrim-rgb),0.55); padding: 0.15rem 0.5rem; border-radius: var(--radius-pill);
}

/* ---- Voice channels — persistent, join-anytime rooms (distinct from the
   ad-hoc space calls above). This floating indicator is a direct child of
   #app-shell in the HTML (not nested in any view), so it stays visible
   across every navigation while connected — same z-index tier as
   .settings-menu/.notif-menu since it's another persistent chrome element. ---- */
.voice-indicator {
  position: fixed; bottom: 1.25rem; right: 1.5rem; z-index: 19;
  display: flex; align-items: center; gap: 0.6rem; background: var(--tile);
  border-radius: var(--radius-pill); box-shadow: var(--shadow-modal); padding: 0.5rem 0.6rem 0.5rem 1rem;
  font-size: 0.85rem; font-weight: 600; color: var(--ink);
}
.voice-indicator-btn {
  background: transparent; border: 1px solid rgba(var(--ink-rgb),0.25); color: var(--ink);
  border-radius: var(--radius-pill); padding: 0.35rem 0.7rem; font-size: 0.8rem; font-weight: 600; cursor: pointer;
}
.voice-indicator-btn.is-active { background: rgba(var(--ink-rgb),0.12); }
.voice-indicator-btn.is-danger { border-color: transparent; background: var(--danger); color: var(--on-accent); }

/* ---- Kanban board ---- */
/* Categories are user-defined and can grow past however many columns fit
   in the viewport at once (unlike the old fixed 4-status pipeline) — the
   board scrolls horizontally instead of squeezing columns illegibly thin
   or silently overflowing off-screen with no way to reach them. */
.board { display: flex; gap: 1.1rem; align-items: flex-start; overflow-x: auto; padding-bottom: 0.5rem; }
.board-col {
  flex: 1 0 220px; min-width: 220px; min-height: 3rem; border-radius: var(--radius-card);
  transition: background-color 120ms ease, box-shadow 120ms ease;
  /* A column with many tasks used to grow the whole page's scroll height
     instead of scrolling on its own — every other column (and the page
     header/sidebar) scrolled along with it, making it impossible to
     compare two columns side by side once either had enough cards to
     scroll at all. --board-col-max-height is set by updateBoardColMaxHeight()
     in app.js from the board's actual measured position, not a guessed
     constant — a fixed "100vh minus an estimate of everything above the
     board" can't be right at every viewport height, since the header/
     filter row above the board is a roughly fixed pixel amount while 100vh
     scales with the screen, so any fixed guess is either too tight on a
     short viewport or (as originally shipped here) wastes a growing amount
     of space below the column on a tall one. The calc() fallback only
     matters for the brief window before JS first runs. Even if the JS
     value is ever off, min-height above still wins over max-height when
     they conflict (CSS's own conflict-resolution rule) — the column is
     never at real risk of collapsing to an unusable sliver. */
  max-height: var(--board-col-max-height, calc(100vh - 20rem)); overflow-y: auto;
}
.board-col.is-drag-over { background: rgba(var(--sage-rgb),0.15); box-shadow: inset 0 0 0 2px var(--sage); }
/* Pinned to the top of its own column's scroll area (not the page's) so
   the "12 tasks" count and column name stay visible while scrolling
   through a long column — background matches the page so cards scrolling
   underneath don't show through the sticky header transparently.
   .board-col's overflow-y: auto (added above) combined with its 20px
   border-radius means content is now actually clipped to the rounded-rect
   shape, not just visually rounded — .board-card already clears that via
   its own padding, but this header used to sit flush at the column's
   (0,0) origin with none, so its first character(s) were clipped by the
   corner curve. padding-top+padding-left here (rather than adding padding
   to .board-col itself) clears the curve without touching the scroll-
   height budget tuned above. */
.board-col-header { position: sticky; top: 0; background: var(--canvas); padding: 0.5rem 0 0.4rem 0.5rem; z-index: 1; }
.board-col-header {
  display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.8rem;
  font-size: 0.82rem; font-weight: 600; color: var(--muted);
}
.board-card {
  border-radius: var(--radius-card); box-shadow: var(--shadow-tile);
  padding: 0.9rem 1rem; margin-bottom: 0.75rem; cursor: pointer;
}
.board-card.is-dragging { opacity: 0.4; }
.board-card-title { font-weight: 500; font-size: 0.9rem; margin-bottom: 0.55rem; }
.board-card-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 0.6rem; }
.board-card-status-select {
  font-size: 0.72rem; color: var(--muted); background: var(--canvas); cursor: pointer;
  border: 1px solid rgba(var(--ink-rgb),0.15); border-radius: 6px; padding: 0.15rem 0.35rem;
  max-width: 60%; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;
}
.board-card-status-select:hover { color: var(--ink); }

/* ---- Grid view: a flat, sortable, spreadsheet-style alternative to the
   column board — every task in the space as one row, no grouping. Reuses
   .tile-surface's own shadow-tile via the table sitting on --tile directly
   rather than each row being its own elevated card, since a dense table
   reads better as one contained surface than N stacked tiles. Clicking a
   row opens the same task sheet a board card's click does — this view
   only changes how tasks are laid out to look at, not how they're
   edited. */
.grid-table-scroll { overflow-x: auto; border-radius: var(--radius-card); box-shadow: var(--shadow-tile); background: var(--tile); }
.grid-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.grid-table th {
  text-align: left; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--muted); padding: 0.75rem 1rem; cursor: pointer; white-space: nowrap; user-select: none;
  border-bottom: 1px solid rgba(var(--ink-rgb),0.1);
}
.grid-table th:hover { color: var(--ink); }
/* ▲/▼ only rendered on whichever header is actually driving the current
   sort — a static arrow on every header would look like every column is
   sorted at once, which is never true. */
.grid-table th.is-sorted-asc::after { content: ' \25B2'; font-size: 0.65em; }
.grid-table th.is-sorted-desc::after { content: ' \25BC'; font-size: 0.65em; }
.grid-table td { padding: 0.7rem 1rem; border-bottom: 1px solid rgba(var(--ink-rgb),0.07); vertical-align: middle; }
.grid-table tr:last-child td { border-bottom: none; }
.grid-row { cursor: pointer; }
.grid-row:hover td { background: rgba(var(--ink-rgb),0.035); }
.grid-cell-title { font-weight: 500; color: var(--ink); }
.grid-cell-title.is-done { text-decoration: line-through; color: var(--muted); }
.grid-cell-empty { color: var(--muted); }

/* ---- Calendar view ---- */
.calendar-weekday-row, .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); }
.calendar-weekday-row {
  gap: 0.5rem; margin-bottom: 0.5rem; text-align: center;
  font-size: 0.75rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em;
}
.calendar-grid { gap: 0.5rem; }
/* min-width: 0 overrides the grid-item default of min-width: auto — without
   it, a long task title (via .calendar-task-chip's own white-space: nowrap)
   forces its whole column wider than 1fr instead of eliding, pushing the
   7-column grid past the viewport on a narrow screen. Same fix, same
   reasoning as .main-panel's identical override elsewhere in this file. */
.calendar-day-cell {
  min-width: 0; min-height: 6.5rem; border-radius: var(--radius-card); background: var(--tile);
  box-shadow: var(--shadow-tile); padding: 0.5rem; display: flex; flex-direction: column; gap: 0.3rem;
}
/* A day outside the currently-viewed month still needs to render (it fills
   out the grid's leading/trailing week) but shouldn't visually compete with
   the month actually being looked at. */
.calendar-day-cell.is-other-month { opacity: 0.45; }
.calendar-day-cell.is-today { box-shadow: var(--shadow-tile), inset 0 0 0 2px var(--sage); }
.calendar-day-number { font-size: 0.8rem; font-weight: 600; color: var(--muted); }
.calendar-day-cell.is-today .calendar-day-number { color: var(--ink); }
/* Cell height is fixed (min-height above) rather than growing with content —
   a day with many due tasks scrolls internally instead of blowing out every
   other cell in its row to match. */
.calendar-day-tasks { display: flex; flex-direction: column; gap: 0.2rem; overflow-y: auto; min-height: 0; flex: 1; }
.calendar-task-chip {
  font-family: var(--font); font-size: 0.72rem; text-align: left; border: none; cursor: pointer;
  background: rgba(var(--info-rgb),0.12); color: var(--ink); border-radius: 6px; padding: 0.2rem 0.4rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 0;
}
.calendar-task-chip:hover { background: rgba(var(--info-rgb),0.22); }
.calendar-task-chip.is-urgent { background: rgba(var(--clay-rgb),0.2); }
.calendar-task-chip.is-urgent:hover { background: rgba(var(--clay-rgb),0.32); }
.calendar-task-chip.is-done { text-decoration: line-through; color: var(--muted); background: rgba(var(--ink-rgb),0.08); }

.btn {
  font-family: var(--font); font-weight: 600; font-size: 0.92rem; border: none; cursor: pointer;
  padding: 0.65rem 1.25rem; border-radius: var(--radius-pill);
  display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
}
.btn-primary { background: var(--sage); color: var(--ink-on-accent); }
/* .btn.btn-icon-only (not a bare .btn-icon-only rule) — a single-class
   selector here would tie on specificity with .btn's own padding above
   and lose since source order would put .btn last; PR #199 hit and fixed
   this exact trap once already for #space-header-more-btn's display
   toggle. Compounding onto .btn wins regardless of declaration order. */
.btn.btn-icon-only { padding: 0.65rem; border-radius: 50%; width: 2.6rem; height: 2.6rem; flex-shrink: 0; position: relative; }
.btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

/* ---- Icon pack (Brand/Icons): monochrome SVGs rendered via mask-image so
   they inherit currentColor through background-color — no inline-SVG
   duplication and no build step to tint them per-button/per-state. Size is
   controlled by font-size since width/height are set in em. ---- */
.icon {
  display: inline-block; width: 1.05em; height: 1.05em; flex-shrink: 0;
  background-color: currentColor; vertical-align: -0.15em;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
}
.icon-add      { -webkit-mask-image: url(icons/add.svg);      mask-image: url(icons/add.svg); }
.icon-bell     { -webkit-mask-image: url(icons/bell.svg);     mask-image: url(icons/bell.svg); }
.icon-calendar { -webkit-mask-image: url(icons/calendar.svg); mask-image: url(icons/calendar.svg); }
.icon-call       { -webkit-mask-image: url(icons/call.svg);       mask-image: url(icons/call.svg); }
.icon-camera-off { -webkit-mask-image: url(icons/camera-off.svg); mask-image: url(icons/camera-off.svg); }
.icon-check    { -webkit-mask-image: url(icons/check.svg);    mask-image: url(icons/check.svg); }
.icon-close    { -webkit-mask-image: url(icons/close.svg);    mask-image: url(icons/close.svg); }
.icon-delete   { -webkit-mask-image: url(icons/delete.svg);   mask-image: url(icons/delete.svg); }
.icon-edit     { -webkit-mask-image: url(icons/edit.svg);     mask-image: url(icons/edit.svg); }
.icon-emoji    { -webkit-mask-image: url(icons/emoji.svg);    mask-image: url(icons/emoji.svg); }
.icon-filter   { -webkit-mask-image: url(icons/filter.svg);   mask-image: url(icons/filter.svg); }
.icon-folder   { -webkit-mask-image: url(icons/folder.svg);   mask-image: url(icons/folder.svg); }
.icon-grip     { -webkit-mask-image: url(icons/grip.svg);     mask-image: url(icons/grip.svg); }
.icon-menu     { -webkit-mask-image: url(icons/menu.svg);     mask-image: url(icons/menu.svg); }
.icon-message  { -webkit-mask-image: url(icons/message.svg);  mask-image: url(icons/message.svg); }
.icon-page     { -webkit-mask-image: url(icons/page.svg);     mask-image: url(icons/page.svg); }
.icon-pin      { -webkit-mask-image: url(icons/pin.svg);      mask-image: url(icons/pin.svg); }
.icon-reply    { -webkit-mask-image: url(icons/reply.svg);    mask-image: url(icons/reply.svg); }
.icon-search   { -webkit-mask-image: url(icons/search.svg);   mask-image: url(icons/search.svg); }
.icon-send     { -webkit-mask-image: url(icons/send.svg);     mask-image: url(icons/send.svg); }
.icon-settings { -webkit-mask-image: url(icons/settings.svg); mask-image: url(icons/settings.svg); }
.icon-star     { -webkit-mask-image: url(icons/star.svg);     mask-image: url(icons/star.svg); }
.icon-table    { -webkit-mask-image: url(icons/table.svg);    mask-image: url(icons/table.svg); }
.icon-user     { -webkit-mask-image: url(icons/user.svg);     mask-image: url(icons/user.svg); }
.icon-video    { -webkit-mask-image: url(icons/video.svg);    mask-image: url(icons/video.svg); }

.modal-close-btn {
  position: absolute; top: 1.1rem; right: 1.25rem; background: none; border: none;
  cursor: pointer; color: var(--muted); padding: 0.25rem; line-height: 1;
}
.modal-close-btn:hover { color: var(--ink); }
.modal-close-btn .icon { width: 1.15em; height: 1.15em; }

.field-input {
  width: 100%; padding: 0.6rem 0.75rem; border-radius: 10px; margin-bottom: 0.8rem;
  border: 1px solid rgba(var(--ink-rgb),0.18); background: var(--tile); color: var(--ink);
  font-family: var(--font); font-size: 0.88rem;
}
.field-input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* ---- Multi-tag chip input — same visual chrome as .field-input, but a
   flex container holding rendered chips plus the actual text input rather
   than being the input itself. ---- */
.tag-chip-input {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem;
  border: 1px solid rgba(var(--ink-rgb),0.18); border-radius: 10px;
  padding: 0.4rem 0.5rem; background: var(--tile); margin-bottom: 0.8rem;
}
.tag-chip-input:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }
.tag-chip-list { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.tag-chip { display: inline-flex; align-items: center; gap: 0.3rem; }
.tag-chip-remove { background: none; border: none; cursor: pointer; display: inline-flex; padding: 0; color: inherit; opacity: 0.8; }
.tag-chip-remove:hover { opacity: 1; }
.tag-chip-remove .icon { width: 0.7em; height: 0.7em; }
.tag-chip-text { flex: 1 1 6rem; min-width: 6rem; border: none; background: none; outline: none; font: inherit; color: var(--ink); padding: 0.15rem 0; }

/* ---- Multi-assignee checklist — a known, fixed roster (team members), so
   a checkbox list fits better than the tag chip-input's free-text model.
   Same row shape as .checklist-item above, minus the done/remove affordances. ---- */
.assignee-checklist { border: 1px solid rgba(var(--ink-rgb),0.18); border-radius: 10px; padding: 0.3rem 0.6rem; margin-bottom: 0.8rem; max-height: 160px; overflow-y: auto; }
.assignee-checklist-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0; font-size: 0.88rem; cursor: pointer; }
.assignee-checklist-row input[type="checkbox"] { flex-shrink: 0; }

/* Height is set directly on the element by the auto-grow input listener
   (see app.js) as content wraps to multiple lines — resize/scroll are
   disabled so it never shows its own scrollbar or a manual drag handle.
   min-height matters even though the listener also runs on every input:
   an EMPTY textarea's scrollHeight is 0 in Chrome once its own height is
   reset to 'auto' (there's no content to size to yet), so without a
   floor here the field would render half-collapsed, clipping its own
   placeholder, every time the modal opens — not an edge case, its
   default resting state. Matches .field-input's own rendered height
   (padding + one line at this font-size) so it looks identical to the
   plain <input> it replaced and to the tag field right below it. */
.auto-grow-textarea {
  resize: none; overflow: hidden; line-height: 1.35; min-height: 2.35rem;
}

/* Native <select> arrows sit in a browser-controlled gutter that ignores
   the box's own padding — on a wide, mostly-empty select (team switcher,
   "Group by") that reads as the arrow floating oddly far from the text.
   Drawn manually instead, inset to match the rest of this form language. */
select.field-input, .team-select {
  appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236D7F8F' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 0.65rem center; background-size: 0.85em;
  padding-right: 2rem;
}
.queue-row:focus-visible, .board-card:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* ---- Homepage — marketing placeholder in front of auth; deliberately
   minimal for now, expected to grow into a full marketing surface. ---- */
.home-wrap { min-height: 100vh; background: var(--canvas); display: flex; flex-direction: column; }
.home-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 1.5rem 2rem; gap: 1rem;
}
.home-header-actions { display: flex; gap: 0.6rem; flex-shrink: 0; }
.home-hero {
  max-width: 640px; margin: 3rem auto 2.5rem; padding: 0 1.5rem; text-align: center;
}
.home-hero h1 { font-size: 2.1rem; line-height: 1.25; margin-bottom: 1rem; }
.home-hero-sub { color: var(--muted); font-size: 1rem; line-height: 1.6; margin-bottom: 1.75rem; }
.home-hero-actions { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; }
.home-features {
  display: flex; flex-wrap: wrap; gap: 1.25rem; justify-content: center;
  max-width: 1000px; margin: 0 auto 3rem; padding: 0 1.5rem;
}
.home-feature-card {
  flex: 1 1 260px; max-width: 300px; border-radius: var(--radius-card); box-shadow: var(--shadow-tile);
  padding: 1.5rem;
}
.home-feature-card .icon { font-size: 1.6rem; color: var(--sage); margin-bottom: 0.75rem; }
.home-feature-card h3 { font-size: 1rem; margin-bottom: 0.4rem; }
.home-feature-card p { color: var(--muted); font-size: 0.88rem; line-height: 1.55; margin: 0; }
.home-footer { text-align: center; color: var(--muted); font-size: 0.8rem; padding: 1.5rem; margin-top: auto; }

/* ---- Auth ---- */
.auth-wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: var(--canvas); }
.auth-card { position: relative; width: 360px; border-radius: var(--radius-card); box-shadow: var(--shadow-tile); padding: 2rem 1.75rem; }
.auth-card .logo-lockup { justify-content: center; margin-bottom: 0.75rem; }
.auth-back-link {
  position: absolute; top: 1.1rem; left: 1.25rem; background: none; border: none; cursor: pointer;
  color: var(--muted); font-size: 0.82rem; font-family: var(--font); padding: 0.2rem;
}
.auth-back-link:hover { color: var(--ink); }
.auth-subtitle { text-align: center; color: var(--muted); font-size: 0.86rem; margin-bottom: 1.4rem; }
.auth-switch { text-align: center; font-size: 0.84rem; color: var(--muted); margin-top: 1rem; }
.auth-switch a { color: var(--info); font-weight: 600; text-decoration: none; }
.auth-error {
  background: rgba(var(--danger-rgb),0.1); color: var(--danger); border-left: 3px solid var(--danger);
  padding: 0.6rem 0.8rem; border-radius: 8px; font-size: 0.84rem; margin-bottom: 1rem;
}
.auth-info {
  background: rgba(var(--success-rgb),0.1); color: var(--success); border-left: 3px solid var(--success);
  padding: 0.6rem 0.8rem; border-radius: 8px; font-size: 0.84rem; margin-bottom: 1rem;
}
.auth-forgot-link { text-align: right; font-size: 0.82rem; margin: -0.5rem 0 0.9rem; }
.auth-forgot-link a { color: var(--info); text-decoration: none; }

.notif-banner {
  display: flex; align-items: center; gap: 0.8rem; flex-wrap: wrap;
  background: rgba(var(--info-rgb),0.1); color: var(--info); border-left: 3px solid var(--info);
  padding: 0.7rem 0.9rem; border-radius: 8px; font-size: 0.86rem; margin-bottom: 1.2rem;
}
.notif-banner span { flex: 1; min-width: 200px; }

/* ---- Layer Two: task detail sheet / modals ---- */
.sheet-overlay { position: fixed; inset: 0; z-index: 20; }
/* Task-detail sheet only (fixes #62): lower than the sidebar so a nav click
   while it's open both dismisses it and navigates, instead of the backdrop
   swallowing the first click. This alone isn't enough on wide viewports,
   though — #sheet-overlay's own z-index/position creates a stacking
   context, so #bottom-sheet's content was trapped inside it and could
   never visually beat the sidebar no matter what z-index IT had, which
   silently hid every field that fell under the sidebar's 250px column
   (in practice: Status/Priority, later Category/Priority too). Fixed at
   the desktop breakpoint by not letting the backdrop/sheet occupy that
   column at all, so there's nothing left to stack-order — the sidebar
   stays clickable through the sheet the same way, but the sheet's own
   content is never covered by it either. Left as the original z-index-only
   fix below that width, where the sidebar is a top bar instead of a left
   column and this offset wouldn't apply correctly. */
#sheet-overlay { z-index: 10; }
.sheet-backdrop { position: absolute; inset: 0; background: rgba(var(--scrim-rgb),0.35); }
.bottom-sheet {
  position: absolute; left: 0; right: 0; bottom: 0; max-height: 80%;
  background: var(--tile); border-radius: var(--radius-modal) var(--radius-modal) 0 0;
  box-shadow: var(--shadow-modal); padding: 1.5rem 1.75rem 2rem; overflow-y: auto;
}
/* Every centered (as opposed to slide-up-from-the-bottom) dialog used the
   same inline left:50%/right:auto/transform:translateX(-50%) pattern with
   no explicit width, relying on max-width alone to cap it. That's a real
   bug, not just a style preference: a position:absolute box with left set,
   right:auto, and width:auto uses CSS shrink-to-fit sizing, whose
   available-width ceiling is (containing block width − left offset) —
   at left:50%, that's HALF the viewport, regardless of max-width. Wide
   enough to not matter on desktop (half of 1280px comfortably exceeds a
   420px max-width), but on a phone (half of 375px = 187.5px) it clamps
   every one of these modals to a box narrower than their own content
   wants, well before max-width ever gets a chance to apply — every child
   layout inside then has to squeeze into that too-narrow box. width: 100%
   alongside max-width restores the intended "full width up to the cap"
   behavior these were always supposed to have. */
.bottom-sheet.is-centered { left: 50%; right: auto; transform: translateX(-50%); width: 100%; border-radius: var(--radius-modal); }
@media (min-width: 721px) {
  #sheet-backdrop, #bottom-sheet { left: 250px; }
}
/* Search is a lookup you dip in and out of from anywhere, not a form tied
   to the content below it (the reason every other .bottom-sheet use here
   slides up from the bottom) — centering it reads more like a command
   palette than a sheet, and doesn't fight the sidebar's own left edge. */
#search-modal {
  position: fixed; top: 50%; left: 50%; right: auto; bottom: auto;
  transform: translate(-50%, -50%); max-height: 80vh;
}
.sheet-handle { width: 40px; height: 4px; background: rgba(var(--muted-rgb),0.4); border-radius: var(--radius-pill); margin: 0 auto 1.25rem; }
.sheet-field-label {
  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); font-weight: 600;
  margin: 1rem 0 0.4rem; display: flex; align-items: center; gap: 0.3rem;
}
.account-danger-zone { border-top: 1px solid rgba(var(--ink-rgb),0.1); margin-top: 1.25rem; padding-top: 0.25rem; }

/* ---- Team management modal ---- */
.member-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.35rem 0; font-size: 0.88rem; }
.member-row .member-email { flex: 1; color: var(--ink); }

/* Category reorder drag handle — cursor:grab is a pure desktop-mouse
   affordance (no effect on touch); the actual cross-input drag mechanism
   is pointer events in app.js, not this. */
.category-drag-handle { cursor: grab; color: var(--muted); }
.category-manage-row.is-dragging { opacity: 0.4; }
.team-invite-error {
  background: rgba(var(--danger-rgb),0.1); color: var(--danger); border-left: 3px solid var(--danger);
  padding: 0.5rem 0.7rem; border-radius: 8px; font-size: 0.82rem; margin-bottom: 0.6rem;
}
.team-invite-status {
  background: rgba(var(--info-rgb),0.1); color: var(--info); border-left: 3px solid var(--info);
  padding: 0.5rem 0.7rem; border-radius: 8px; font-size: 0.82rem; margin-bottom: 0.6rem;
}

/* ---- Messaging: DMs and channels ---- */
.space-link { position: relative; } /* base rule already there; unread dot below anchors off it */
.unread-dot {
  position: absolute; right: 0.65rem; top: 50%; transform: translateY(-50%);
  width: 8px; height: 8px; border-radius: 50%; background: var(--clay);
}
/* ---- Message list: chat bubbles, own messages right-aligned/sage-filled,
   others left-aligned/white with avatar+name ---- */
#conversation-view { display: flex; flex-direction: column; height: calc(100vh - 4rem); }
#conversation-view > .conversation-header { flex-shrink: 0; }
/* A short thread sits flush against the compose box with empty space
   above it, instead of flush against the top with empty space below,
   matching how every other chat app lays out a growing thread — but NOT
   via justify-content: flex-end, which looks equivalent for a short list
   and is a well-known trap for a long one: combined with overflow-y:auto
   on the same element, the browser can't correctly compute a scrollable
   area for content that overflows past the (visually shifted) top edge,
   permanently hiding older messages with no way to reach them. The
   standard fix instead: keep the container's own alignment at the default
   (flex-start), and give a ::before pseudo-element margin-top:auto — it
   absorbs all leftover space and pushes the real content down when the
   list doesn't fill the panel, but simply doesn't have any leftover space
   to absorb once real content overflows, so normal top-to-bottom
   scrolling is preserved. */
/* mask-image (not a top border/shadow) fades scrolled-past messages out
   near the header instead of hard-clipping them at the container's edge —
   only the top fades; the bottom stays fully opaque since that's always
   the newest message and the compose bar, which should never look like
   they're fading away. */
.message-list {
  display: flex; flex-direction: column; gap: 1rem; flex: 1; min-height: 0; overflow-y: auto; margin-bottom: 1rem;
  mask-image: linear-gradient(to bottom, transparent, black 1.5rem);
  -webkit-mask-image: linear-gradient(to bottom, transparent, black 1.5rem);
}
.message-list::before { content: ''; margin-top: auto; }
.message-row { display: flex; flex-direction: column; max-width: 70%; }
.message-row.is-own { align-self: flex-end; align-items: flex-end; }
.message-row.is-other { align-self: flex-start; align-items: flex-start; }

.message-row-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.35rem; }
.message-author { font-weight: 600; font-size: 0.85rem; color: var(--ink); }
.message-author.is-deleted { color: var(--muted); font-style: italic; font-weight: 500; }
.message-time-actions {
  display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.35rem;
  font-size: 0.72rem; color: var(--muted);
}
.message-time { font-size: 0.72rem; color: var(--muted); }

.message-bubble {
  border-radius: 18px; padding: 0.65rem 1rem; font-size: 0.92rem; line-height: 1.45;
  color: var(--ink); white-space: pre-wrap; word-break: break-word;
}
.message-bubble.is-own { background: var(--sage); color: var(--ink-on-accent); border-bottom-right-radius: 6px; }
.message-bubble.is-other { background: var(--tile); box-shadow: var(--shadow-tile); border-bottom-left-radius: 6px; }
.mention-highlight { background: rgba(var(--sage-rgb),0.45); padding: 0.05rem 0.4rem; border-radius: 6px; font-weight: 600; }

.message-icon-btn {
  background: none; border: none; color: var(--muted); cursor: pointer;
  font-size: 0.85rem; padding: 0.1rem 0.15rem; line-height: 1;
}
.message-icon-btn:hover { color: var(--ink); }
/* Hidden until the message is clicked (see the delegated listener that
   toggles .is-selected) — pointer-events: none while hidden so an
   invisible button can never eat the click meant to reveal it. Scoped to
   .message-row specifically: .message-icon-btn is also reused by the
   category-management modal's move-up/move-down/delete buttons (inside
   .member-row, not .message-row), which should stay always-visible. */
.message-row .message-icon-btn { opacity: 0; pointer-events: none; transition: opacity 0.15s ease; }
.message-row.is-selected .message-icon-btn { opacity: 1; pointer-events: auto; }
.message-icon-btn[data-pinned="true"] { color: var(--honey); }
/* Deliberately outside .message-icon-btn's hide-until-selected rule above
   — a pinned message should stay recognizable without clicking it first. */
.pinned-badge { display: inline-flex; color: var(--honey); font-size: 0.85rem; }

/* ---- Pinned messages bar — a sticky index into the conversation, not a
   reorder of the main feed (which stays in real chronological order). ---- */
.pinned-bar {
  display: flex; flex-direction: column; gap: 0.3rem; flex-shrink: 0;
  background: rgba(var(--honey-rgb),0.12); border: 1px solid rgba(var(--honey-rgb),0.3);
  border-radius: var(--radius-card); padding: 0.5rem 0.7rem; margin-bottom: 0.75rem;
}
.pinned-bar-item {
  display: flex; align-items: center; gap: 0.5rem; cursor: pointer;
  color: var(--honey); font-size: 0.82rem;
}
.pinned-bar-item .icon { flex-shrink: 0; }
.pinned-bar-text {
  flex: 1; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pinned-bar-unpin {
  background: none; border: none; color: var(--muted); cursor: pointer; padding: 0.1rem; line-height: 1; flex-shrink: 0;
}
.pinned-bar-unpin:hover { color: var(--ink); }
.pinned-bar-unpin .icon { width: 0.85em; height: 0.85em; }

/* ---- Message reactions ---- */
.message-reactions { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.3rem; }
.message-reactions:empty { margin-top: 0; }
.reaction-pill {
  display: inline-flex; align-items: center; gap: 0.3rem; font-family: var(--font);
  font-size: 0.78rem; padding: 0.15rem 0.55rem; border-radius: var(--radius-pill);
  background: var(--tile); border: 1px solid rgba(var(--ink-rgb),0.15); color: var(--ink); cursor: pointer;
}
.reaction-pill:hover { background: rgba(var(--ink-rgb),0.06); }
.reaction-pill.is-mine { background: rgba(var(--sage-rgb),0.3); border-color: var(--sage); }
.reaction-count { font-weight: 600; color: var(--muted); }
.reaction-pill.is-mine .reaction-count { color: var(--ink); }

/* Fixed-position popover anchored to the react button that opened it (see
   openReactionPicker) — holds a real <emoji-picker>, registered via the
   side-effecting import at the top of app.js, rather than a hand-rolled
   fixed emoji set. Not inserted inline in the message list: doing so
   pushed later messages down and could end up far from the button on a
   tall conversation. */
.reaction-picker-popover {
  position: fixed; z-index: 30; border-radius: var(--radius-card);
  box-shadow: var(--shadow-modal); overflow: hidden;
}
.reaction-picker-popover emoji-picker {
  --emoji-size: 1.4rem; --num-columns: 8; max-width: 320px;
  --background: var(--tile); --border-color: rgba(var(--ink-rgb),0.15);
  --indicator-color: var(--sage); --input-border-color: rgba(var(--ink-rgb),0.15);
  --input-font-color: var(--ink); --input-placeholder-color: var(--muted);
  --outline-color: var(--focus); --category-font-color: var(--muted);
}

/* Board filter popover — same fixed-position, anchored-to-its-button
   pattern as .reaction-picker-popover above, for the same reason (a board
   with many columns shouldn't push this down inline). */
.board-filter-popover {
  position: fixed; z-index: 30; background: var(--tile); border-radius: var(--radius-card);
  box-shadow: var(--shadow-modal); padding: 0.6rem; width: 220px; max-height: 320px;
  display: flex; flex-direction: column;
}
.board-filter-popover-actions {
  display: flex; gap: 0.75rem; padding: 0.2rem 0.4rem 0.5rem; border-bottom: 1px solid rgba(var(--ink-rgb),0.1); margin-bottom: 0.3rem;
}
.board-filter-popover-list { overflow-y: auto; display: flex; flex-direction: column; gap: 0.1rem; }
.board-filter-option {
  display: flex; align-items: center; gap: 0.5rem; padding: 0.35rem 0.4rem; border-radius: 8px;
  font-size: 0.85rem; color: var(--ink); cursor: pointer;
}
.board-filter-option:hover { background: rgba(var(--ink-rgb),0.06); }
#board-filter-btn.is-active { background: rgba(var(--sage-rgb),0.25); border-color: var(--sage); color: var(--ink); }

/* Tag autocomplete popover — same fixed-position, anchored pattern as
   .board-filter-popover above, but width matches the anchor input rather
   than a fixed size, and each option carries a tagColorClass() swatch. */
.tag-autocomplete-popover {
  position: fixed; z-index: 30; background: var(--tile); border-radius: var(--radius-card);
  box-shadow: var(--shadow-modal); padding: 0.3rem; max-height: 220px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 0.1rem;
}
.tag-autocomplete-option {
  display: flex; align-items: center; gap: 0.55rem; padding: 0.35rem 0.5rem; border-radius: 8px;
  font-size: 0.85rem; color: var(--ink); cursor: pointer; background: none; border: none;
  text-align: left; width: 100%; font-family: inherit;
}
.tag-autocomplete-option:hover { background: rgba(var(--ink-rgb),0.06); }
.tag-autocomplete-swatch {
  width: 0.65rem; height: 0.65rem; border-radius: 50%; flex-shrink: 0;
}

/* Inline delete confirmation — replaces window.confirm() so it's actually
   testable headlessly and matches the rest of this UI instead of a native
   browser dialog. */
.delete-confirm-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  background: rgba(var(--danger-rgb),0.08); border: 1px solid rgba(var(--danger-rgb),0.25); border-radius: 12px;
  padding: 0.6rem 1rem; margin-top: 0.5rem; font-size: 0.86rem; color: var(--danger); width: 100%;
}
.delete-confirm-actions { display: flex; gap: 0.9rem; flex-shrink: 0; }
.link-btn {
  background: none; border: none; cursor: pointer; font-size: 0.86rem; font-family: var(--font);
  font-weight: 600; text-decoration: underline; padding: 0; color: var(--ink);
}
.link-btn.is-danger { color: var(--danger); }
#compose-form { position: relative; flex-shrink: 0; }
.mention-dropdown {
  position: absolute; bottom: 100%; left: 0; margin-bottom: 0.4rem; width: 260px;
  background: var(--tile); border-radius: 10px; box-shadow: var(--shadow-modal);
  max-height: 180px; overflow-y: auto; z-index: 5;
}
.mention-option {
  display: block; width: 100%; text-align: left; padding: 0.5rem 0.75rem; font-size: 0.85rem;
  border: none; background: none; cursor: pointer; color: var(--ink); font-family: var(--font);
}
.mention-option:hover, .mention-option.is-active { background: rgba(var(--ink-rgb),0.06); }

/* Quill mention popover (task notes, standalone note pages) — same visual
   language as .mention-dropdown above, but position: fixed and anchored via
   quill.getBounds() math instead of `position: absolute` off a fixed compose
   bar, since a Quill editor can be tall and scrollable. */
.quill-mention-popover {
  position: fixed; z-index: 30; background: var(--tile); border-radius: 10px;
  box-shadow: var(--shadow-modal); padding: 0.3rem; width: 260px; max-height: 200px;
  overflow-y: auto; display: flex; flex-direction: column; gap: 0.1rem;
}
.quill-mention-option {
  display: block; width: 100%; text-align: left; padding: 0.5rem 0.75rem; font-size: 0.85rem;
  border: none; background: none; cursor: pointer; color: var(--ink); font-family: var(--font);
  border-radius: 8px;
}
.quill-mention-option:hover { background: rgba(var(--ink-rgb),0.06); }

/* ---- Referencing a task (or a note page — identical presentation, just a
   different picker/chip pair) from a message ---- */
.task-ref-picker, .note-ref-picker {
  position: absolute; bottom: 100%; left: 0; margin-bottom: 0.4rem; width: 280px;
  background: var(--tile); border-radius: 10px; box-shadow: var(--shadow-modal);
  padding: 0.5rem; z-index: 5;
}
#task-ref-results, #note-ref-results { max-height: 180px; overflow-y: auto; }
.task-ref-option, .note-ref-option {
  display: block; width: 100%; text-align: left; padding: 0.5rem 0.6rem; font-size: 0.85rem;
  border: none; background: none; cursor: pointer; color: var(--ink); font-family: var(--font); border-radius: 6px;
}
.task-ref-option:hover, .note-ref-option:hover { background: rgba(var(--ink-rgb),0.06); }
.task-ref-option .task-ref-option-space, .note-ref-option .note-ref-option-space { display: block; color: var(--muted); font-size: 0.76rem; }
.compose-task-ref-preview, .compose-note-ref-preview {
  display: flex; align-items: center; gap: 0.4rem; background: rgba(var(--info-rgb),0.1); color: var(--info);
  font-size: 0.82rem; font-weight: 600; padding: 0.35rem 0.7rem; border-radius: var(--radius-pill);
  margin-bottom: 0.5rem; width: fit-content;
}
.compose-task-ref-preview span:not(.icon), .compose-note-ref-preview span:not(.icon) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px; }
#compose-task-ref-clear, #compose-note-ref-clear, #note-page-task-ref-clear {
  background: none; border: none; cursor: pointer; color: var(--info); padding: 0; display: flex; flex-shrink: 0;
}
#compose-task-ref-clear .icon, #compose-note-ref-clear .icon, #note-page-task-ref-clear .icon { width: 0.85em; height: 0.85em; }
.message-task-ref-chip, .message-note-ref-chip {
  margin-top: 0.35rem; background: rgba(var(--info-rgb),0.1); color: var(--info); border: none; cursor: pointer;
  font-size: 0.82rem; font-weight: 600; padding: 0.35rem 0.7rem; border-radius: var(--radius-pill);
  display: inline-flex; align-items: center; gap: 0.4rem; max-width: 100%; overflow: hidden; text-align: left;
}
.message-task-ref-chip:hover, .message-note-ref-chip:hover { background: rgba(var(--info-rgb),0.18); }
.message-task-ref-chip .icon, .message-note-ref-chip .icon { width: 0.85em; height: 0.85em; flex-shrink: 0; }
.message-task-ref-chip span:not(.icon), .message-note-ref-chip span:not(.icon) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.compose-reply-preview {
  display: flex; align-items: center; gap: 0.4rem; background: rgba(var(--ink-rgb),0.08); color: var(--muted);
  font-size: 0.82rem; padding: 0.35rem 0.7rem; border-radius: var(--radius-pill);
  margin-bottom: 0.5rem; width: fit-content; max-width: 100%;
}
.compose-reply-preview span:not(.icon) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.compose-reply-preview strong { color: var(--ink); }
#compose-reply-clear {
  background: none; border: none; cursor: pointer; color: var(--muted); padding: 0; display: flex; flex-shrink: 0; margin-left: 0.2rem;
}
#compose-reply-clear .icon { width: 0.85em; height: 0.85em; }

/* Quoted context shown above a reply's own bubble — clicking it scrolls to
   the original message, same idea as clicking a pinned-bar item. */
.message-reply-context {
  display: flex; align-items: center; gap: 0.35rem; background: none;
  cursor: pointer; font-family: var(--font); font-size: 0.78rem; color: var(--muted);
  padding: 0 0 0 0.5rem; margin-bottom: 0.25rem; max-width: 100%; text-align: left;
  border: none; border-left: 2px solid rgba(var(--ink-rgb),0.25);
}
.message-reply-context:hover { color: var(--ink); }
.message-reply-context .icon { width: 0.8em; height: 0.8em; flex-shrink: 0; }
.message-reply-context strong { color: var(--ink); font-weight: 600; }
.message-reply-context span:not(.icon) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.referencing-message-row {
  display: block; width: 100%; text-align: left; background: none; border: none; cursor: pointer;
  padding: 0.5rem 0; border-bottom: 1px solid rgba(var(--ink-rgb),0.08); font-family: var(--font);
}
.referencing-message-row:last-child { border-bottom: none; }
.referencing-message-row .referencing-message-author { font-weight: 600; font-size: 0.82rem; color: var(--ink); }
.referencing-message-row .referencing-message-body { font-size: 0.85rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ---- Global search modal ---- */
.search-input-wrap { position: relative; margin-bottom: 0.5rem; }
.search-input-wrap .icon {
  position: absolute; left: 0.9rem; top: 50%; transform: translateY(-50%); color: var(--muted);
}
#global-search-input { padding-left: 2.4rem; margin-bottom: 0; }
.search-results { max-height: 50vh; overflow-y: auto; }
.search-result-group-label {
  font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted); font-weight: 600; margin: 1rem 0 0.3rem;
}
.search-result-group-label:first-child { margin-top: 0.75rem; }
.search-result-row {
  display: block; width: 100%; text-align: left; background: none; border: none; cursor: pointer;
  padding: 0.55rem 0.4rem; border-radius: 10px; font-family: var(--font);
}
.search-result-row:hover { background: rgba(var(--ink-rgb),0.06); }
.search-result-title {
  font-weight: 600; font-size: 0.9rem; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.search-result-context {
  font-size: 0.8rem; color: var(--muted); margin-top: 1px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.search-empty-state { color: var(--muted); font-size: 0.88rem; text-align: center; padding: 1.5rem 0; }

/* ---- Empty state — signature moment 5 ---- */
.empty-wrap { min-height: 280px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.9rem; }
.empty-tile { width: 90px; height: 90px; border-radius: var(--radius-card); border: 1.5px dashed rgba(var(--muted-rgb),0.4); background: transparent; }
.empty-copy { color: var(--muted); font-size: 0.92rem; }

/* ---- Signature moment 3: tiles settle into place ---- */
@keyframes tile-settle {
  0% { opacity: 0; transform: translateY(-26px) scale(0.94); }
  60% { opacity: 1; transform: translateY(3px) scale(1.02); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}
.tile-settle { animation: tile-settle 480ms cubic-bezier(0.34, 1.56, 0.64, 1) both; }

/* ---- Signature moment 4: glaze-sweep on task completion ---- */
@keyframes glaze-sweep {
  0% { transform: translateX(-60%) skewX(-15deg); }
  100% { transform: translateX(130%) skewX(-15deg); }
}
.glaze-sweep { position: relative; overflow: hidden; }
.glaze-sweep::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 40%;
  background: linear-gradient(115deg, transparent, rgba(var(--gloss-rgb),0.75), transparent);
  animation: glaze-sweep 900ms ease-out;
}

/* ---- Guided tour: a spotlight cutout (one element, box-shadow trick —
   a giant shadow radiating out from a small transparent box IS the dimmed
   background) plus a card positioned next to whichever element it's
   framing. Above absolutely everything (modals included), since nothing
   should be interactable mid-step except the tour's own buttons. ---- */
.tour-overlay { position: fixed; inset: 0; z-index: 1000; }
.tour-spotlight {
  position: fixed; border-radius: 10px; box-shadow: 0 0 0 9999px rgba(var(--scrim-rgb),0.6);
  transition: top 200ms ease, left 200ms ease, width 200ms ease, height 200ms ease;
  pointer-events: none;
}
.tour-spotlight.is-centered { box-shadow: none; background: rgba(var(--scrim-rgb),0.6); inset: 0; border-radius: 0; }
.tour-card {
  position: fixed; width: 300px; max-width: calc(100vw - 2rem); border-radius: var(--radius-card);
  box-shadow: var(--shadow-tile); padding: 1.25rem 1.4rem; transition: top 200ms ease, left 200ms ease;
}
.tour-card.is-centered { left: 50%; top: 50%; transform: translate(-50%, -50%); }
.tour-step-count { color: var(--muted); font-size: 0.78rem; font-weight: 600; margin-bottom: 0.3rem; }
.tour-card h3 { font-size: 1.02rem; margin-bottom: 0.5rem; }
.tour-card p { color: var(--muted); font-size: 0.88rem; line-height: 1.5; margin: 0 0 1.1rem; }
.tour-card-actions { display: flex; align-items: center; gap: 0.5rem; }

@media (prefers-reduced-motion: reduce) {
  .tour-spotlight, .tour-card { transition: none; }
  .tile-settle { animation: none; opacity: 1; transform: none; }
  .tile-settle.fade-in { animation: fade-in 200ms ease-out both; }
  .glaze-sweep::after { animation: none; display: none; }
  .glaze-sweep.flash { animation: flash-opacity 200ms ease-out; }
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes flash-opacity { 0% { opacity: 0.4; } 100% { opacity: 1; } }

@media (max-width: 720px) {
  .app-shell { flex-direction: column; }
  /* The equal-flex-basis centering trick for #board-view-mode-segmented
     doesn't degrade gracefully once the row can't fit on one line — with
     flex-wrap, extra space gets redistributed per line rather than across
     the whole row, which visibly overlapped Add task/Start call's text
     against the segmented control on a phone width. display:contents
     un-wraps the two .space-header-actions-side groups back into plain
     flex-wrap children of .space-header-actions, the same simple wrap
     behavior this row had before centering was added — the trade is
     losing true centering at this width in exchange for not overlapping. */
  .space-header-actions-side { display: contents; }
  /* The sidebar goes off-canvas instead of stacking above the main content
     — on a real phone width, a full-height nav list pushed the queue/board
     below the fold. It slides in from the left behind the hamburger
     trigger; .settings-menu is unaffected and keeps floating top-right. */
  .sidebar {
    position: fixed; top: 0; left: 0; width: 82%; max-width: 320px; height: 100vh;
    border-right: 1px solid rgba(var(--ink-rgb),0.1); border-bottom: none;
    z-index: 18; transform: translateX(-100%);
    /* cubic-bezier(0.32, 0.72, 0, 1) is a fast-start/soft-landing decel
       curve (the same family iOS uses for sheet/panel presentations) —
       plain "ease" has a much gentler, more symmetric curve that reads as
       sluggish for a short slide-in like this. will-change hints the
       browser to promote this to its own compositor layer before the
       transition starts, rather than partway through the first frame. */
    transition: transform 0.32s cubic-bezier(0.32, 0.72, 0, 1);
    will-change: transform;
    overflow-y: auto; box-shadow: var(--shadow-modal);
  }
  .sidebar.is-open { transform: translateX(0); }
  .sidebar-toggle-btn { display: flex; }
  .sidebar-close-btn { display: flex; align-items: center; justify-content: center; }
  .sidebar-backdrop { display: block; }
  .board { flex-direction: column; }
  /* .board-col's flex: 1 0 220px and .board's align-items: flex-start are
     both written for the desktop row layout — 220px as a minimum WIDTH,
     flex-start meaning "don't stretch column HEIGHT to the tallest
     sibling." Flipping .board to flex-direction: column above swaps which
     axis is which for both of them: flex-basis now clamps every column's
     HEIGHT to ~220px regardless of card count (overflowing cards spilled
     into whatever column rendered next, looking like duplicated tasks),
     and flex-start now means "don't stretch column WIDTH to fill the
     container" (columns shrink-wrapped to their own content's width
     instead of filling the screen). Resetting flex/min-width restores
     content-based height; align-self: stretch restores full-width —
     together, each column looks the same shape mobile always intended,
     just derived from properties that meant something different before
     the axis flip. */
  .board-col { flex: initial; min-width: 0; align-self: stretch; }
  /* The independent-scroll fix above is a desktop-Kanban concept — compare
     two columns side by side, scroll one without losing your place in the
     other. Stacked on mobile, columns are read one after another anyway,
     so a nested scroll box per column (scroll the column, then
     scroll the page to reach the next one) is worse than just letting the
     page scroll through everything in one motion, same as before this fix
     existed. */
  .board-col { max-height: none; overflow-y: visible; }
  /* Top padding clears the fixed hamburger/settings buttons that now float
     over the very top of the main panel, since the sidebar no longer
     occupies that space in normal flow. */
  .main-panel { padding: 1.25rem; padding-top: 4rem; }
  /* Matches .main-panel's total top+bottom padding above (4rem + 1.25rem),
     same as the pre-existing pattern this replaces (was 2.5rem = 1.25rem*2
     before the extra top padding existed). */
  #conversation-view { height: calc(100vh - 5.25rem); }
  .home-header { flex-direction: column; align-items: stretch; padding: 1.25rem; }
  .home-header .logo-lockup { justify-content: center; }
  .home-header-actions { justify-content: center; }
  .home-hero h1 { font-size: 1.6rem; }

  /* A 7-column grid leaves each day only ~50px wide on a phone — too
     narrow for even a heavily-ellipsized title to mean anything (two
     different tasks both starting with "Task…" become indistinguishable).
     Below the same breakpoint every other view goes off-canvas at, swap
     each day's task titles for plain dots instead: the day itself is still
     tappable-dense information ("something's due"), and the full title is
     still reachable via the existing tap-to-open-sheet behavior. */
  .calendar-day-cell { min-height: 3.5rem; padding: 0.35rem; gap: 0.2rem; }
  .calendar-day-tasks { flex-direction: row; flex-wrap: wrap; gap: 0.2rem; overflow: visible; }
  .calendar-task-chip {
    width: 0.45rem; height: 0.45rem; padding: 0; border-radius: 50%; text-indent: -999px;
  }

  /* Icon-only buttons sized for mouse precision (message pin/delete/reply,
     note-tree row rename/delete, note-tree expand arrow, checklist item
     actions) are too small a target for a finger at this width. Bumps the
     tappable box without touching each button's own .icon size rule, so
     the glyph itself stays visually the same — just easier to actually
     hit. 36px rather than the full ~44px usually recommended: several of
     these sit multiple-in-a-row in already-narrow spaces (a nested
     note-tree row, a message's action cluster), where 44px each would
     force wrapping or crowd out the row's own label text. */
  .message-icon-btn, .note-tree-icon-btn, .note-tree-toggle, .checklist-icon-btn {
    min-width: 36px; min-height: 36px; display: flex; align-items: center; justify-content: center;
  }
  /* .note-tree-row-actions is gated behind :hover by default, which
     doesn't exist on touch — a tap either never triggers it (buttons stay
     permanently unreachable) or simulates it on first tap (needing an
     actual double-tap: one to reveal, one to activate). Unlike
     .message-icon-btn's tap-to-reveal pattern (real click/tap state via
     .is-selected), a note-tree row's own tap already means "open this
     page" — repurposing that same tap to toggle a reveal state instead
     would fight the row's primary action, not complement it. Simplest fix
     that doesn't require a new gesture or a "..." menu: just always show
     these at mobile widths, same breakpoint as the tap-target bump above. */
  .note-tree-row-actions { display: flex; }
}
