/* Minimal dark-theme base. Full warm/playful design lands in the frontend
   milestone (M7), aligned to design/Workflow.dc.html. */

/* Self-hosted fonts (docs/DESIGN.md typography), vendored under static/fonts/ so we never
   hit an external CDN — CSP is font-src 'self' (no Google Fonts). Both are the Latin-subset
   VARIABLE files (one file covers the whole weight range). NOTE: Plus Jakarta Sans has NO
   Cyrillic glyphs, so Bulgarian (Cyrillic) UI text falls back to the system stack below —
   an intentional, documented gap (see NEXT_STEPS). JetBrains Mono does include Cyrillic. */
@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal; font-weight: 400 800; font-display: swap;
  src: url("../fonts/plusjakarta.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal; font-weight: 400 700; font-display: swap;
  src: url("../fonts/jetbrainsmono.woff2") format("woff2");
}

:root {
  /* Warm dark palette — docs/DESIGN.md tokens, adopted 2026-07-07 (was a cool
     placeholder ramp). The full token set is here for later views; the legacy aliases
     (--surface/--muted/--accent) are remapped onto the warm ramp so existing rules
     need no churn. */
  --bg:          #17130e; /* app background */
  --bg-sidebar:  #110e09; /* sidebar, compose bar */
  --bg-panel:    #1b1610; /* board columns, tables, cards */
  --bg-panel-2:  #1a160f; /* search box, inactive pills */
  --bg-detail:   #15110b; /* detail panel */
  --bg-card:     #221c14; /* raised card / hover row */
  --bg-code:     #0e0c08; /* <pre> code block */
  --border:      #221c14;
  --border-2:    #271f17;
  --border-3:    #322a1f;
  --border-4:    #2a241b; /* inputs */
  --text:        #f3ede2; /* primary text */
  --text-2:      #cabfae; /* subtitle / note body */
  --text-3:      #9d9384; /* secondary */
  --text-muted:  #7d7263; /* labels, captions */
  --brand:       #e8924a; /* primary action (orange) */
  --on-brand:    #2a1606; /* text on the orange */

  /* Line / overlay tokens. Tokenized (2026-07-07) so the light theme can flip them —
     as raw rgba(255,255,255,…) they'd vanish on a light background. Three border tiers
     + a hover fill + the code-pill fill. */
  --line:    rgba(255,255,255,.07); /* subtle dividers, card/section borders */
  --line-2:  rgba(255,255,255,.13); /* inputs, ghost buttons, badges */
  --line-3:  rgba(255,255,255,.28); /* strongest (hover border) */
  --hover:   rgba(255,255,255,.07); /* hover fill */
  --pill-bg: rgba(255,255,255,.05); /* code-pill background */

  /* The accent drives every primary action; a theme (see [data-accent=…]) overrides it.
     Default = brand orange. --on-accent is the readable text laid over the accent. */
  --accent:    var(--brand);
  --on-accent: var(--on-brand);

  /* Role treatment token (docs/DESIGN.md Dev blue / QA coral). Reserved for role-tinted
     surfaces per the mock (note left-borders, role chips in the thread). The nav avatar +
     role badge deliberately follow --accent so they track the user's chosen theme
     (user decision 2026-07-07). */
  --role-color: var(--accent);

  /* legacy aliases still referenced throughout the sheet */
  --surface:   var(--bg-card);
  --muted:     var(--text-3);
}

[data-role="programmer"] { --role-color: #6aa9f0; }
[data-role="qa"]         { --role-color: #ec7e63; }
[data-role="admin"]      { --role-color: #c77dff; }

/* ─── Accent themes (user-chosen via the nav palette menu; default orange) ────────────
   Every hue sits in the same lightness/saturation band as the brand orange so the set
   reads as one family on the warm dark canvas, and dark on-accent text stays legible.
   Colors are drawn from the palette already in the project (docs/DESIGN.md state/type
   hues) so a theme never feels foreign. */
[data-accent="orange"] { --accent: #e8924a; --on-accent: #2a1606; }
[data-accent="honey"]  { --accent: #d9a441; --on-accent: #2a1c06; } /* deep golden */
[data-accent="yellow"] { --accent: #e8c65c; --on-accent: #2a2306; }
[data-accent="red"]    { --accent: #e56a5a; --on-accent: #2c0d08; } /* warm coral-red */
[data-accent="green"]  { --accent: #5cc28d; --on-accent: #06231a; } /* resolved green */
[data-accent="blue"]   { --accent: #6aa9f0; --on-accent: #08182e; } /* Dev blue */
[data-accent="purple"] { --accent: #b98be0; --on-accent: #1e0e2e; } /* discuss violet */
[data-accent="pink"]   { --accent: #e88ab4; --on-accent: #2e0c1c; }
[data-accent="gray"]   { --accent: #b4a894; --on-accent: #241d14; } /* warm taupe */

/* ─── Light theme ────────────────────────────────────────────────────────────────────
   A whole warm-cream light mode: flip only the neutral ramp + line/overlay tokens; the
   chosen accent (above) rides on top unchanged, so any accent works in either mode. */
[data-theme="light"] {
  --bg:          #f4eee3;
  --bg-sidebar:  #ede4d5;
  --bg-panel:    #faf5ec;
  --bg-panel-2:  #f1eadd;
  --bg-detail:   #faf5ec;
  --bg-card:     #ffffff;
  --bg-code:     #efe7d8;
  --text:        #2a2117;
  --text-2:      #4a4032;
  --text-3:      #6f6557;
  --text-muted:  #8a8073;
  --line:    rgba(60,42,20,.12);
  --line-2:  rgba(60,42,20,.2);
  --line-3:  rgba(60,42,20,.34);
  --hover:   rgba(60,42,20,.05);
  --pill-bg: rgba(60,42,20,.06);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  line-height: 1.5;
}

#app { max-width: 1100px; margin: 0 auto; padding: 2rem; }

a { color: var(--accent); }

/* ═══ Public landing (templates/home.html, anonymous) ═══════════════════════════════════
   Built on the app's own tokens + the real .badge-* chips, so the marketing page IS the
   product. Header/theme picker come from base.html's nav; no page-specific JS. */
.landing { display: flex; flex-direction: column; gap: 1rem; }

/* Hero */
.lp-hero {
  display: grid; grid-template-columns: 1.05fr 1fr; gap: 3rem; align-items: center;
  padding: 2.5rem 0 1.5rem;
}
.lp-eyebrow {
  text-transform: uppercase; letter-spacing: .16em; font-size: .72rem; font-weight: 700;
  color: var(--muted); margin: 0 0 1rem;
}
.lp-h1 { font-size: clamp(2.2rem, 5vw, 3.4rem); font-weight: 800; line-height: 1.08;
  letter-spacing: -.02em; margin: 0; text-wrap: balance; }
.lp-h1 em { font-style: normal; color: var(--accent); }
.lp-lead { color: var(--text-2); font-size: 1.1rem; margin: 1.1rem 0 1.6rem; max-width: 36ch; }
.lp-cta { display: flex; flex-wrap: wrap; gap: .7rem; align-items: center; }
/* The hero pair is sized by `.btn-lg` (both CTAs carry it) — this only adds the hero's heavier
   weight, so the outlined «Log in» holds its own next to the filled primary. Size lives in one
   place on purpose: this rule used to set its own padding/font-size, which (at 0,2,0) out-specified
   `.btn-lg` and left the primary shorter than the secondary next to it. */
.lp-cta .btn-ghost { font-weight: 700; }
.lp-cta-note { color: var(--muted); font-size: .82rem; margin: 1.1rem 0 0; }

/* Hero board (dogfoods the real chips) */
.lp-board {
  background: var(--bg-panel); border: 1px solid var(--line); border-radius: 16px;
  box-shadow: 0 24px 60px rgba(0,0,0,.4); padding: .9rem;
}
.lp-board-top { display: flex; align-items: center; gap: .4rem; padding: .2rem .4rem .8rem; }
.lp-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--line-3); }
.lp-board-title { margin-left: .5rem; font-size: .74rem; }
.lp-cols { display: grid; grid-template-columns: repeat(4, minmax(148px, 1fr)); gap: .5rem;
  overflow-x: auto; padding-bottom: .2rem; }
.lp-col-head { margin-bottom: .5rem; }
.lp-note { background: var(--bg-card); border: 1px solid var(--line); border-radius: 10px;
  padding: .6rem .65rem; }
.lp-note-title { font-size: .82rem; font-weight: 600; line-height: 1.3; margin-bottom: .5rem; }
.lp-note-chips { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .55rem; }
.lp-note-foot { display: flex; align-items: center; gap: .35rem; }
.lp-who { display: inline-grid; place-items: center; width: 20px; height: 20px; border-radius: 50%;
  font-size: .58rem; font-weight: 800; color: #fff; }
.lp-who.qa { background: #ec7e63; } .lp-who.dev { background: #6aa9f0; } .lp-who.pm { background: #c77dff; }
.lp-arrow { color: var(--muted); font-size: .7rem; }

/* Sections */
.lp-section { padding: 3rem 0; border-top: 1px solid var(--line); }
.lp-kicker { text-transform: uppercase; letter-spacing: .16em; font-size: .72rem; font-weight: 700;
  color: var(--accent); margin: 0 0 .8rem; }
.lp-h2 { font-size: clamp(1.5rem, 3.4vw, 2.1rem); font-weight: 800; letter-spacing: -.02em;
  margin: 0; text-wrap: balance; }
.lp-section-lead { color: var(--text-2); max-width: 52ch; margin: .8rem 0 0; }

/* Lifecycle pipeline */
.lp-pipe { display: grid; grid-template-columns: repeat(4, 1fr); gap: .8rem; margin-top: 1.8rem; }
.lp-stage { position: relative; background: var(--bg-panel); border: 1px solid var(--line);
  border-radius: 12px; padding: 1.1rem; }
.lp-stage::after { content: "→"; position: absolute; right: -.72rem; top: 50%;
  transform: translateY(-50%); color: var(--line-3); font-weight: 700; z-index: 2; }
.lp-stage:last-child::after { content: none; }
.lp-stage h3 { font-size: .76rem; text-transform: uppercase; letter-spacing: .06em; margin: 0 0 .55rem; }
.lp-stage.d h3 { color: var(--st-discuss, #b98be0); } .lp-stage.pr h3 { color: #6aa9f0; }
.lp-stage.q h3 { color: #e6b15c; } .lp-stage.r h3 { color: #5cc28d; }
.lp-stage p { color: var(--text-3); font-size: .86rem; margin: 0; }

/* Feature grid */
.lp-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-top: 1.8rem; }
.lp-feature { background: var(--bg-panel); border: 1px solid var(--line); border-radius: 14px; padding: 1.4rem; }
.lp-feature h3 { font-size: 1.02rem; font-weight: 700; margin: 0 0 .35rem; }
.lp-feature p { color: var(--text-3); font-size: .88rem; margin: 0; }

/* Final CTA */
.lp-final { text-align: center; }
.lp-final .lp-section-lead { margin-left: auto; margin-right: auto; margin-bottom: 1.6rem; }

/* Member home (authenticated) */
.member-home h1 { margin: 0 0 .3rem; }
.home-shortcuts { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin-top: 1.5rem; }
.home-card { display: flex; flex-direction: column; gap: .3rem; text-decoration: none;
  background: var(--bg-panel); border: 1px solid var(--line); border-radius: 12px; padding: 1.2rem;
  color: var(--text); transition: border-color .12s, background .12s; }
.home-card:hover { border-color: var(--line-3); background: var(--hover); }
.home-card-title { font-weight: 700; color: var(--accent); }

/* Landing responsive (its own breakpoints — the hero + grids reflow before the phone width) */
@media (max-width: 820px) {
  .lp-hero { grid-template-columns: 1fr; gap: 2rem; padding-top: 1.5rem; }
  /* A grid item's `min-width: auto` refuses to shrink below its content's min-content width, and
     .lp-board's is 616px (its 4 board columns are `minmax(148px, 1fr)`). Stacked, the board and the
     copy share ONE track, so the board held that track — and the whole PAGE — open at 646.8px: at a
     390px viewport that was 273px of horizontal scroll on the one page every prospect sees, against
     Rule 12. `min-width: 0` lets the track follow the viewport; .lp-cols then scrolls inside its own
     `overflow-x: auto` (MOBILE.md's convention for a block that can't wrap) — which is what "the hero
     board scrolls-x on a phone" was always meant to mean.
     Scoped to the STACKED hero on purpose: side by side, the board's track is meant to open to its
     natural 616px and show all four columns. Applying this at every width shrinks the desktop board
     to 482px and hides a column behind an internal scrollbar. */
  .lp-hero > * { min-width: 0; }
  .lp-grid, .lp-pipe { grid-template-columns: 1fr 1fr; }
  .lp-stage:nth-child(2)::after { content: none; }
}
@media (max-width: 560px) {
  .lp-grid, .lp-pipe, .home-shortcuts { grid-template-columns: 1fr; }
  .lp-stage::after { content: none; }
}

/* --- top nav (role-aware; full design in M7) --- */
.topnav {
  display: flex; align-items: center; gap: 1rem;
  padding: .75rem 1.25rem; background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.topnav .brand { font-weight: 800; letter-spacing: -.3px; color: var(--accent); text-decoration: none; }
.topnav-spacer { flex: 1; }
.navlinks { display: flex; gap: .9rem; }
.navlinks a, .navlink { color: var(--muted); text-decoration: none; font-weight: 600; font-size: .9rem; }
.navlinks a:hover, .navlink:hover { color: var(--text); }
.user-chip {
  display: inline-flex; align-items: center; gap: .5rem;
  text-decoration: none; color: var(--text); border-radius: 999px;
  padding: 2px 8px 2px 2px; transition: background .12s;
}
.user-chip:hover { background: var(--hover); }
.user-chip .uname { font-weight: 600; font-size: .9rem; color: var(--text); }
.logout-link { margin-left: .3rem; }
.avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 50%;
  background: var(--accent); color: var(--on-accent); font-weight: 700; font-size: .8rem; flex: none;
}
.avatar-lg { width: 56px; height: 56px; font-size: 1.4rem; }
.role-badge {
  font-size: .7rem; font-weight: 700; letter-spacing: .4px; text-transform: uppercase;
  color: var(--accent); border: 1px solid var(--accent); border-radius: 6px; padding: 1px 7px;
}
/* Consolidated mobile account menu — desktop keeps the full nav, so this is hidden here and
   revealed in the @media (max-width: 640px) block below (user request 2026-07-10). */
.user-menu { display: none; }
/* --- projects index --- */
.project-list { list-style: none; margin: 0 0 1.5rem; padding: 0; }
.project-list li {
  display: flex; align-items: center; gap: .6rem;
  padding: .55rem .8rem; margin-bottom: .4rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: 10px;
}
.project-list .muted { color: var(--muted); background: none; border: none; }
/* per-row action buttons, pushed to the row's right edge */
.row-actions { margin-left: auto; display: inline-flex; align-items: center; gap: .45rem; }
/* Badge group inside a "Your projects" row. On desktop it's transparent (`contents`) so the
   badges are ordinary flex items of the row, exactly as before; a phone turns it into the row's
   second line (see #mine-list in the @media block). */
.row-badges { display: contents; }
/* The ⋯ kebab replaces the inline row actions on phones (always) and on desktop when the user
   opts into compact menus (body.kebab-actions — profile preference, 2026-07-11). Its appearance
   lives here in the base (not the media query) so the desktop opt-in can reveal it too; the
   visibility flip happens in the two rules just below + the @media block. */
.row-menu { display: none; position: relative; margin-left: auto; }
.btn-kebab {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 30px; border-radius: 8px;
  border: 1px solid var(--line-2); color: var(--muted);
}
.btn-kebab::-webkit-details-marker { display: none; }
.btn-kebab:hover { color: var(--text); }
.row-menu[open] .btn-kebab { color: var(--accent); border-color: var(--accent); }
.btn-kebab .icon { width: 18px; height: 18px; }
.row-menu-panel {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 60;
  min-width: 168px; padding: .4rem;
  display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--line-2); border-radius: 10px;
  box-shadow: 0 14px 36px rgba(0,0,0,.4);
}
/* The Make-default / Make-start items are <button type=submit> inside a <form class=inline>, not
   <a> links (2026-07-18 QA: they showed as a light-blue browser-default button with unreadable
   text). Let the wrapping form vanish so the button is the flex child, and reset the button chrome
   so it matches the <a> items exactly. */
.row-menu-panel form.inline { display: contents; }
.row-menu-item {
  display: flex; align-items: center; gap: .6rem; width: 100%;
  padding: .55rem; border: 0; border-radius: 7px;
  background: none; cursor: pointer; text-align: left;
  color: var(--text); text-decoration: none;
  font-family: inherit; font-weight: 600; font-size: .9rem;
}
.row-menu-item:hover { background: var(--hover); }
.row-menu-item .icon { flex: none; width: 16px; height: 16px; }
.row-menu-item.danger { color: #ff9a9a; }
.row-menu-item.danger:hover { background: rgba(255,122,122,.12); color: #ffbcbc; }
/* Desktop opt-in: compact ⋯ menus instead of inline buttons (phones force it via @media). */
body.kebab-actions .row-actions { display: none; }
body.kebab-actions .row-menu { display: block; }
.project-name { font-weight: 600; }
.code-pill {
  font-family: "JetBrains Mono", ui-monospace, monospace; font-size: .78rem;
  color: var(--muted); background: var(--pill-bg);
  border: 1px solid var(--line-2); border-radius: 6px; padding: 1px 6px;
}

.crumbs { margin-bottom: 1rem; font-size: .9rem; }
.crumbs a { color: var(--muted); text-decoration: none; font-weight: 600; }
.crumbs a:hover { color: var(--text); }
.crumb-sep { color: var(--muted); margin: 0 .35rem; }
.crumb-here { color: var(--text); font-weight: 700; }
/* The container header's title cluster: the project name + its lock state (user request
   2026-07-15). A phone flattens this with display:contents (see the @media block) so the lock
   keeps grouping with the ⋯ kebab at the right edge, exactly as before. */
.head-title { display: flex; align-items: center; gap: .6rem; min-width: 0; }
.head-actions { display: inline-flex; gap: .5rem; }
/* Container-header management actions (Lock/Members/Edit/Delete). On desktop the menu is
   transparent (display:contents) so they render inline as before and the ⋯ toggle is hidden;
   the @media block turns it into a dropdown on phones (user request 2026-07-10). */
.head-menu, .head-menu-panel { display: contents; }
.head-menu-cb, .head-menu > label.btn-kebab { display: none; }
/* Desktop: the New note/New section group is transparent (renders inline in the header) and the
   per-section-heading copies are hidden. A phone (see @media) flips both: hides `.head-add`, shows
   `.head-add-inline` next to each section heading (user request 2026-07-14). */
.head-add { display: contents; }
.section-head { display: contents; }
/* Two-class selector so it beats `.btn`/`.btn-ghost` (defined later) on desktop. */
.section-head .head-add-inline { display: none; }

/* --- DESKTOP-ONLY container-header refinements (user request 2026-07-15). The complement of the
   `max-width: 640px` phone block at the end of this file (640/641 — they can never both match), so
   the phone layout is untouched. Fixes a real misalignment: `#lock-control` and `#members-attn`
   exist as HTMX swap/OOB targets, but each was *also* the flex item of `.head-actions` — so the
   wrapper stretched while the control inside it did not. Members' <a> stayed `display:inline`
   (31.2px tall, 3px above its neighbours) and the lock's <button> sat at its natural 33.8px, while
   the unwrapped buttons stretched to 36.4px: three heights in one row. Handing the real controls to
   the flex line (display:contents on the wrappers — swap targets don't need boxes; `id` lookup and
   `form.htmx-request .spinner` are DOM-based and unaffected) + one explicit height fixes all of it. */
@media (min-width: 641px) {
  /* The lock's wrapper + no-JS form are plumbing, so the <button>/<span class=badge> itself is the
     flex item and centres against the title. An empty wrapper (non-manager, unlocked) then draws
     nothing at all, instead of a phantom item eating a .6rem gap. */
  .head-title #lock-control, .head-title #lock-control .inline { display: contents; }

  .container-page .head-actions { align-items: center; }
  .container-page .head-actions #members-attn { display: contents; }

  /* Collapse toggle sits at the FAR RIGHT of the header on desktop (user request 2026-07-18): the
     .list-title is a full-width flex row here (its .section-head parent is display:contents), so
     margin-left:auto pushes the button to the section's right edge, past the title + reorder ⇅. On a
     phone the rule is absent, so the button keeps its natural place right after the reorder button. */
  .container-page .btn-collapse { margin-left: auto; }
}
.muted-text { color: var(--muted); font-size: .85rem; }

.lang-switch { display: inline-flex; gap: 2px; }
.lang-switch button {
  border: 1px solid var(--line-2); background: none; color: var(--muted);
  font: inherit; font-weight: 700; font-size: .7rem; letter-spacing: .4px;
  padding: 2px 7px; border-radius: 6px; cursor: pointer;
}
.lang-switch button:hover { color: var(--text); }
.lang-switch button.active { color: var(--accent); border-color: var(--accent); cursor: default; }

/* --- profile --- */
.profile-head { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; }
.profile-head h1 { margin: 0 0 .35rem; }
.profile-fields { display: grid; grid-template-columns: max-content 1fr; gap: .5rem 1.5rem; }
.profile-fields dt { color: var(--muted); font-weight: 600; }
.profile-fields dd { margin: 0; }
/* Row-actions preference: current value + the toggle button, side by side. */
.pref-row { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
/* Inline name editor on the profile page (mirrors the Members row editor). */
.name-field { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.name-value { min-height: 1.2em; }
/* A small square icon button for the pencil — sized like the row buttons, muted until hover. */
.btn-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--line);
  color: var(--muted); background: transparent; cursor: pointer; text-decoration: none;
}
.btn-icon:hover { background: var(--hover); color: var(--text); border-color: var(--line-2); }
.btn-icon .icon { display: block; }
.name-edit { display: inline-flex; align-items: center; gap: .5rem; margin: 0; flex-wrap: wrap; }
.name-edit input[type=text] {
  padding: .35rem .55rem; border: 1px solid var(--line-2); border-radius: 8px;
  background: var(--pill-bg); color: var(--text); font: inherit; min-width: 8rem;
}
.name-edit input[type=text]:focus { border-color: var(--accent); outline: none; }
.name-edit .btn-save {
  display: inline-flex; align-items: center; justify-content: center; min-width: 34px;
  padding: .38rem .5rem; border: none; border-radius: 8px; cursor: pointer;
  background: var(--accent); color: var(--on-accent);
}
.name-edit .btn-save:hover { filter: brightness(1.06); }

/* --- allauth pages (login / signup / verify / reset / MFA …) --- */
.auth-wrap { display: flex; justify-content: center; padding: 1rem 0 2.5rem; }
.auth-card {
  width: 100%; max-width: 420px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 16px; padding: 1.75rem 1.75rem 2rem;
}
.auth-card h1 { margin: 0 0 1.1rem; font-size: 1.5rem; letter-spacing: -.3px; }
.auth-card > p, .auth-card form > p:not(:has(label)) { color: var(--muted); }
.auth-card a { color: var(--accent); font-weight: 600; }

/* Django renders allauth fields via form.as_p → <p><label>…</label><input></p> */
.auth-card form p { margin: 0 0 1rem; display: flex; flex-direction: column; gap: .35rem; }
.auth-card label { color: var(--muted); font-weight: 600; font-size: .85rem; }
.auth-card input:not([type=checkbox]):not([type=radio]),
.auth-card select, .auth-card textarea {
  width: 100%; padding: .6rem .7rem;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--line-2); border-radius: 9px;
  font: inherit; outline: none;
}
.auth-card input:focus, .auth-card select:focus, .auth-card textarea:focus {
  border-color: var(--accent);
}
.auth-card input[type=checkbox], .auth-card input[type=radio] { accent-color: var(--accent); }

/* allauth buttons render as bare <button> (submit) — make the primary action pop */
.auth-card button, .auth-card [type=submit] {
  width: 100%; margin-top: .35rem; padding: .65rem 1rem;
  background: var(--accent); color: var(--on-accent);
  border: none; border-radius: 10px;
  font: inherit; font-weight: 700; cursor: pointer;
}
.auth-card button:hover, .auth-card [type=submit]:hover { filter: brightness(1.06); }
.auth-card hr { border: none; border-top: 1px solid var(--line); margin: 1.25rem 0; }

/* errors + messages */
.auth-card .errorlist, .auth-card ul.errorlist {
  list-style: none; margin: 0 0 .5rem; padding: 0; color: #ff7a7a; font-size: .85rem;
}
.auth-card .helptext, .auth-card span { color: var(--muted); font-size: .8rem; }
.messages { list-style: none; margin: 0 0 1.1rem; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
.messages .msg {
  padding: .6rem .75rem; border-radius: 9px; font-size: .9rem;
  background: var(--bg); border: 1px solid var(--line-2);
}
.messages .msg-error { border-color: #ff7a7a; color: #ff9a9a; }
.messages .msg-success { border-color: var(--accent); }

/* --- members page: company members, role editing, invitations (M2.8.4 / M2.8.5) --- */
.members h1 { margin: 0; font-size: 1.5rem; letter-spacing: -.3px; }
.members h2 {
  font-size: .75rem; text-transform: uppercase; letter-spacing: .5px;
  color: var(--muted); margin: 1.75rem 0 .65rem;
}
/* Members list heading + its Reorder ⇅ toggle (QA point 8, 2026-07-18): the h2's asymmetric
   margin box (1.75rem top / .65rem bottom) threw off the flex `align-items: center` — the button
   centred against the margin box, not the text, so it floated above the heading. Move the spacing
   onto the .list-title wrapper and zero the h2's margin so text + button centre together (same fix
   the settings page uses via .vocab-head). Scoped to #members-list so teams/settings are untouched. */
.members #members-list .list-title { margin: 1.75rem 0 .65rem; }
.members #members-list .list-title h2 { margin: 0; }
.members-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; margin-bottom: .5rem;
}

/* ── Buttons ──────────────────────────────────────────────────────────────────────────────
   `.btn` (primary) and `.btn-ghost` (secondary) share ONE box — same type size, padding, border
   box and radius — so they line up wherever they sit side by side (user request 2026-07-15).
   They differ only in SKIN (fill + weight), which is the hierarchy signal.

   They used to drift: `.btn` was .5rem/.95rem, .85rem type, NO border; `.btn-ghost` .38rem/.85rem,
   .82rem type, a 1px border. That's ~2.6px apart on desktop — and, because the phone block below
   overrides only the vertical padding, ~1.3px apart the OTHER way (38 vs 39.3) on a phone.

   `inline-flex` is load-bearing: `.btn-ghost` set no display, so an `<a class="btn-ghost">` stayed
   inline, ignored its vertical padding and rendered ~5px short of a sibling `<button>`.

   Deliberately NOT set here:
   - `justify-content` — the phone kebab panel gives its items width:100% and left-aligns them.
   - `gap` — `.attention-dot` (the Members "!") carries its own margin-left.
   Height comes from type + padding, never a fixed `height`, so the phone block's bigger padding
   still buys Rule 12 tap targets. Size overrides must out-specify these (see `.lp-cta .btn-ghost`). */
.btn, .btn-ghost {
  display: inline-flex; align-items: center;
  box-sizing: border-box; padding: .45rem .9rem;
  border: 1px solid transparent; border-radius: 9px;
  font: inherit; font-size: .82rem;
  text-decoration: none; cursor: pointer; white-space: nowrap;
}
/* primary action button (also used as a link, e.g. "Invite a member") */
.btn { background: var(--accent); color: var(--on-accent); font-weight: 700; }
.btn:hover { filter: brightness(1.06); }

/* Demo lockdown (2026-08-04, the user's choice): at the user cap the Invite button stays visible
   but inert, with the reason beneath it — a primary action that simply vanishes reads as a bug.
   `:disabled` only matches real form controls, which is why the capped state renders a
   <button disabled> rather than an <a> (an <a> cannot be disabled, only stripped of its href).
   The :hover reset must out-specify `.btn:hover` above, or the brightness lift still fires and
   the button reads as live. */
.btn:disabled, .btn:disabled:hover { opacity: .45; cursor: not-allowed; filter: none; }

/* Demo lockdown: the landing's "Coming soon — private demo" strip (the user's choice,
   2026-08-04). Sits above the hero and spans the landing's own width rather than the viewport,
   so it lines up with the content instead of fighting .landing's padding. */
.lp-soon-banner {
  margin: 0 0 1.25rem; padding: .6rem 1rem; border-radius: 10px;
  background: var(--pill-bg); border: 1px solid var(--line-2); color: var(--muted);
  font-size: .85rem; font-weight: 600; letter-spacing: .01em; text-align: center;
}
.invite-action { display: inline-flex; flex-direction: column; align-items: flex-end; gap: .2rem; }
.invite-capped-why { color: var(--muted); font-size: .72rem; }

/* The marketing-CTA size (landing hero pair + the closing CTA), on either variant. It MUST stay
   after the shared `.btn, .btn-ghost` rule: it lived up in the landing block for months and, at
   equal specificity, simply lost to the `.btn` further down — so it did nothing at all and the
   hero's primary rendered smaller than the secondary beside it. The phone block's padding
   override still applies below (both CTAs shrink together, so they stay level). */
.btn-lg { padding: .7rem 1.4rem; border-radius: 11px; font-size: 1rem; }

/* row lists */
.member-list, .invite-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
.member-list > li, .invite-list > li {
  display: flex; align-items: center; flex-wrap: wrap; gap: .55rem;
  padding: .65rem .9rem; background: var(--surface);
  border: 1px solid var(--line); border-radius: 12px;
}
.member-name,
.invite-list > li > span:first-child { font-weight: 600; margin-right: auto; }
.member-gone { color: var(--muted); font-style: italic; font-weight: 400; }
/* Groups inside a member row. Transparent (`contents`) on desktop so name/role/badges stay inline
   exactly as before; a phone turns them into the row's two lines (see #member-rows in @media). */
.member-main, .member-badges { display: contents; }
/* Edit + Remove cluster (QA point 14): inline on desktop, the phone grid's col-3 stack. */
.member-actions { display: contents; }

/* Managers-only roster subgroups: people who left / pending invites (2026-07-11). */
.member-subgroup { margin-top: 1rem; }
.member-subgroup-label {
  font-size: .7rem; font-weight: 700; letter-spacing: .5px; text-transform: uppercase;
  color: var(--muted); margin: 0 0 .45rem;
}
.member-list > li.needs-attention { border-color: #e6b15c; }
/* The red "!" alert on a project with departures to review. */
.attention-dot {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.15rem; height: 1.15rem; padding: 0 .3rem; margin-left: .1rem;
  font-size: .72rem; font-weight: 800; line-height: 1; color: var(--on-accent, #17130e);
  background: #ff7a7a; border-radius: 999px; text-decoration: none;
}
.attention-dot:hover { background: #ff9a9a; }

/* Rule 14: "Available to join" hides itself while its list has no rows (so live-adding the first
   joinable row reveals it and deleting the last hides it — no placeholder needed). Keyed on the
   presence of an <li> child (not :empty) so leftover whitespace text nodes don't keep it shown. */
#projects-joinable:not(:has(#join-list > li)) { display: none; }
#invites:not(:has(#invite-list > li)) { display: none; }
#roster-left-block:not(:has(#roster-left > li)) { display: none; }
#roster-pending-block:not(:has(#roster-pending > li)) { display: none; }

/* inline icons in buttons/badges (padlock etc.) */
.icon { vertical-align: -2px; }

/* red remove/delete button — kept red (user request 2026-07-07). Square + matches the
   other row buttons' height WITHOUT depending on the row resolving a height: equal padding
   around a square icon is always square, and a 19px icon + .38rem padding lands at the same
   ~33px height as .btn-ghost (font .82rem × 1.5 + .38rem padding). line-height:0 keeps the
   inline box from adding stray height. */
.btn-remove {
  display: inline-flex; align-items: center; justify-content: center;
  padding: .38rem; flex: 0 0 auto; line-height: 0;
  cursor: pointer; font: inherit; border-radius: 8px;
  color: #ff9a9a; background: transparent;
  border: 1px solid rgba(255,122,122,.4);
}
.btn-remove:hover { background: rgba(255,122,122,.12); border-color: #ff7a7a; color: #ffbcbc; }
.btn-remove .icon { display: block; width: 19px; height: 19px; }

/* add-member block under the project roster (M3.4c2; redesigned 2026-07-06 after user
   feedback — divider, uppercase mini-label, styled select that grows across the row) */
.member-add-block {
  margin-top: 1.1rem; padding-top: 1rem;
  border-top: 1px solid var(--line);
}
.member-add-label {
  display: block; margin-bottom: .55rem;
  font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted);
}
.member-add { display: flex; align-items: center; gap: .6rem; }
.member-add select {
  flex: 1; min-width: 0;
  appearance: none; -webkit-appearance: none;
  padding: .55rem 2.3rem .55rem .8rem;
  background-color: var(--bg);
  /* chevron as a data: URI — img-src already allows data: (favicon precedent) */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239d93ad' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right .75rem center; background-size: 14px;
  color: var(--text);
  border: 1px solid var(--line-2); border-radius: 10px;
  font: inherit; font-size: .9rem; cursor: pointer;
}
.member-add select:hover { border-color: var(--line-3); }
.member-add select:focus { border-color: var(--accent); outline: none; }

/* inline role / Manager edit form on a member row */
.member-edit { display: inline-flex; align-items: center; gap: .6rem; margin: 0; }
.member-edit select {
  padding: .35rem .55rem; background: var(--bg); color: var(--text);
  border: 1px solid var(--line-2); border-radius: 8px; font: inherit; font-size: .85rem;
}
.member-edit select:focus { border-color: var(--accent); outline: none; }
.member-edit label { display: inline-flex; align-items: center; gap: .35rem; color: var(--muted); font-size: .85rem; }
.member-edit input[type=checkbox] { accent-color: var(--accent); }
.member-edit button {
  padding: .38rem .9rem; border: none; border-radius: 8px;
  background: var(--accent); color: var(--on-accent); font: inherit; font-weight: 700; font-size: .82rem; cursor: pointer;
}
.member-edit button:hover { filter: brightness(1.06); }

/* async-button spinner: htmx puts .htmx-request on the in-flight form — swap the button label
   for the spinner. Works on any form carrying a .save-label/.spinner button pair
   (member editor, invite rows, inline invite form). currentColor follows the button's text. */
.spinner {
  display: none; width: 14px; height: 14px; vertical-align: -2px;
  border: 2px solid transparent; border-top-color: currentColor;
  border-right-color: currentColor; border-radius: 50%;
  animation: spin .6s linear infinite;
}
form.htmx-request .save-label { display: none; }
form.htmx-request .spinner { display: inline-block; }
form.htmx-request button[type=submit] { pointer-events: none; }
@keyframes spin { to { transform: rotate(360deg); } }

/* shared modal outlet — a native <dialog id="modal"> (base.html): the browser renders it in
   the top layer and paints ::backdrop, so it works even before this stylesheet refreshes */
dialog#modal, dialog#att-error, dialog#att-viewer, dialog#att-confirm {
  margin: auto; padding: 0; border: none; background: transparent; color: var(--text);
  width: min(480px, calc(100vw - 2rem)); max-height: calc(100vh - 2rem);
}
/* The viewer shows a PICTURE, not a form, so it takes far more room than the 480px the other
   two dialogs want (2026-08-02, QA point 11). */
dialog#att-viewer { width: min(1000px, calc(100vw - 3rem)); max-height: calc(100vh - 3rem); }
dialog#modal::backdrop, dialog#att-error::backdrop, dialog#att-viewer::backdrop,
dialog#att-confirm::backdrop {
  background: rgba(10, 8, 16, .6); backdrop-filter: blur(2px);
}
dialog#modal .modal-card, dialog#att-error .modal-card, dialog#att-viewer .modal-card,
dialog#att-confirm .modal-card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 16px; padding: 1.4rem 1.5rem 1.6rem;
  overflow-y: auto; box-shadow: 0 24px 64px rgba(0,0,0,.5);
}
.modal-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: .9rem; }
.modal-head h2 { margin: 0; font-size: 1.1rem; letter-spacing: -.2px; }
.modal-close {
  background: none; border: none; color: var(--muted);
  font: inherit; font-size: 1.35rem; line-height: 1; cursor: pointer; padding: .1rem .35rem;
}
.modal-close:hover { color: var(--text); }
/* Footer row of the dialog: primary action left, Cancel pushed to the right edge. */
.modal-actions { display: flex; justify-content: space-between; align-items: center; margin-top: 1.1rem; }

/* the invite form fragment (lives inside the modal; bare when rendered no-JS full-page) */
.invite-inline {
  margin: .9rem 0; padding: 1rem 1.1rem; max-width: 480px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
}
#modal .invite-inline { margin: 0; padding: 0; background: none; border: none; max-width: none; }
.invite-inline p { margin: 0 0 .75rem; }
.invite-inline label { display: block; margin-bottom: .25rem; color: var(--muted); font-size: .85rem; }
.invite-inline .helptext { display: block; color: var(--muted); font-size: .78rem; margin-top: .2rem; }
.invite-inline input:not([type=checkbox]):not([type=radio]),
.invite-inline select, .invite-inline textarea {
  width: 100%; padding: .6rem .7rem;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--line-2); border-radius: 9px; font: inherit;
}
/* The description textarea matches the other fields' width; let it grow vertically only. */
.invite-inline textarea { resize: vertical; min-height: 4.5rem; }
.invite-inline input:focus, .invite-inline select:focus,
.invite-inline textarea:focus { border-color: var(--accent); outline: none; }
.invite-inline input[type=checkbox] { accent-color: var(--accent); }
.invite-inline .errorlist { list-style: none; margin: 0 0 .5rem; padding: 0; color: #ff8f7a; font-size: .85rem; }

/* invitation row actions */
.invite-actions { display: inline-flex; gap: .4rem; }
.inline { display: inline; margin: 0; }
/* Secondary button — the box comes from the shared `.btn, .btn-ghost` rule above; this is skin
   only. `border-color` (not the `border` shorthand) so it keeps the shared 1px width/style, and
   so `.btn-danger` below only has to recolour it. */
.btn-ghost {
  background: transparent; color: var(--text); border-color: var(--line-2); font-weight: 600;
}
.btn-ghost:hover { background: var(--hover); }
.btn-danger { color: #ff9a9a; border-color: rgba(255,122,122,.4); }
.btn-danger:hover { background: rgba(255,122,122,.12); border-color: #ff7a7a; }

/* status pills */
.badge {
  font-size: .66rem; font-weight: 700; letter-spacing: .4px; text-transform: uppercase;
  color: var(--muted); border: 1px solid var(--line-2); border-radius: 6px; padding: 1px 7px;
}
.badge-warn { color: #e6b15c; border-color: #e6b15c; }
/* A pending project invite waiting for the member to accept (2026-07-11). */
.badge-invite { color: var(--accent); border-color: var(--accent); }
.you { color: var(--muted); font-size: .85rem; }

/* Semantic item chips — hues from docs/DESIGN.md. Class = badge-<facet>-<code>, where
   <code> is the raw TextChoices value (not the translated label), so colors survive
   i18n. Each carries its hue in the text + a translucent border; Critical also gets a
   faint fill so it reads as the alarm state. */
.badge-type-bug        { color: #ec7e63; border-color: rgba(236,126,99,.5); }
.badge-type-error      { color: #e8915c; border-color: rgba(232,145,92,.5); }
.badge-type-suggestion { color: #5cc2b4; border-color: rgba(92,194,180,.5); }

.badge-prio-low      { color: #a89d8b; border-color: rgba(168,157,139,.45); }
.badge-prio-medium   { color: #e6b15c; border-color: rgba(230,177,92,.5); }
.badge-prio-high     { color: #ec7e63; border-color: rgba(236,126,99,.5); }
.badge-prio-critical { color: #f08a80; border-color: rgba(224,87,74,.6); background: rgba(224,87,74,.12); }

.badge-state-discuss  { color: #b98be0; border-color: rgba(185,139,224,.5); }
.badge-state-pr       { color: #6aa9f0; border-color: rgba(106,169,240,.5); }
.badge-state-qa       { color: #e6b15c; border-color: rgba(230,177,92,.5); }
.badge-state-resolved { color: #5cc28d; border-color: rgba(92,194,140,.5); }

/* ─── The 25-colour badge palette (DESIGN.md open-Q #4) ────────────────────────────────
   Companies name their own roles / note types / states and pick a colour per term. The keys
   live in apps/common/palette.py and MUST match this list one-for-one (a browser test asserts
   they never drift). A picked colour can't be inlined as style="…" — our CSP is nonce-based
   with no unsafe-inline, and a nonce can't whitelist a style *attribute* — so each key is a
   class the stylesheet already knows. Same contract theme.py has with [data-accent="…"].

   Each key sets only a HUE + CHROMA; the LIGHTNESS comes from the theme, so all 25 stay legible
   on the dark panel AND the cream one from 25 lines instead of 50 hand-tuned hexes. (The older
   semantic chips above are fixed hexes with no light variant — this is strictly better, and they
   get migrated onto these keys as the vocabulary becomes data.)
   Hues 1-9 match the chips this app already ships, so seeded vocabulary keeps its colours.
   NB: never write a literal star-slash inside a CSS comment — it ends the comment early and the
   rest of the text is parsed as a selector, silently eating the rule that follows. */
:root {
  /* .74 is the measured mean lightness of the chips this app already ships (they fit oklch L
     .70-.79), so a palette chip sits beside a hard-coded priority chip without looking brighter. */
  --badge-l: .74;        /* text on the dark panels */
  --badge-bl: .58;       /* its border, dimmer so the chip reads as an outline */
}
[data-theme="light"] {
  --badge-l: .50;        /* text on the cream panels */
  --badge-bl: .72;
}
.badge[class*="badge-c-"] {
  color: oklch(var(--badge-l) var(--badge-ch) var(--badge-h));
  border-color: oklch(var(--badge-bl) var(--badge-ch) var(--badge-h) / .55);
}
/* The settings page's per-vocabulary heading row (h2 + its New button). `.members h2` is the
   uppercase mini-label, which reads right here too — it just needs the button beside it. */
.settings-page .vocab-head { margin-top: 1.5rem; }
.settings-page .vocab-head h2 { margin: 0; }
.settings-page #note-type-rows .row-main { display: flex; align-items: center; gap: .6rem; }
.settings-page #note-state-rows .row-main { display: flex; align-items: center; gap: .6rem; }

/* Drag-to-reorder (SortableJS POC, 2026-07-16 — workflow states). The handle is the only drag
   grip (Sortable `handle`); the ghost is the drop-placeholder, the chosen row is the one held. */
.drag-handle {
  display: none; align-items: center; color: var(--muted);
  cursor: grab; touch-action: none; /* let Sortable own the pointer on touch */
}
/* Reorder toggle (2026-07-18): handles are HIDDEN by default and revealed only while the list's
   scope is in reorder mode (app.js flips `.reordering` when the Reorder button next to the title is
   pressed). Hiding the handle also disables dragging until opt-in — you can't grip what isn't shown. */
[data-reorder-scope].reordering .drag-handle { display: inline-flex; }
/* Bigger, tighter grip (user request 2026-07-17): ~2× the old 14px, and the icon's viewBox is
   cropped to the dots (templates) so there's no empty margin inside the box. Same on desktop + phone
   — a drag target wants to be easy to hit with a finger. */
.drag-handle .icon { width: 18px; height: 28px; }
.drag-handle:active { cursor: grabbing; }
.sortable-ghost { opacity: .4; }
.sortable-chosen { background: var(--hover); border-radius: 8px; }

/* The Reorder toggle itself: a small icon button beside a list's title (personal-sort UX,
   2026-07-18). Muted until hovered/active; the pressed state (reorder mode on) tints it to the
   accent. `.list-title` groups the heading with the button so they cluster on the left even inside
   a space-between header. app.js hides the button (via [hidden]) when a list has <2 items. */
.list-title { display: flex; align-items: center; gap: .4rem; min-width: 0; }
.btn-reorder {
  display: inline-flex; align-items: center; justify-content: center;
  padding: .2rem; border: 1px solid transparent; border-radius: 7px;
  background: transparent; color: var(--muted); cursor: pointer; line-height: 0;
}
.btn-reorder[hidden] { display: none; }
.btn-reorder:hover { color: var(--text); background: var(--hover); }
.btn-reorder[aria-pressed="true"] {
  color: var(--accent); background: var(--hover); border-color: var(--line-2);
}
.btn-reorder .icon { width: 17px; height: 17px; }

/* Row enter/leave animations (QA point 9, 2026-07-19; user: "animate everything" — self-actions
   AND the 5s poller's background adds/removes share one look).
   LEAVE — a deleting row fades out AND shrinks to 20% (an 80% size reduction) before it's removed,
   so the removal reads clearly. Two triggers, one look:
   • DIRECT row swaps (e.g. invite Cancel) use htmx's own `.htmx-swapping` class, held for the
     `swap:Ns` window — keep N in sync with the transition duration below.
   • OOB / confirm-modal / poller deletes (`hx-swap-oob="delete"`, marked `.row-leaving` by
     oob_delete_row) can't get `.htmx-swapping`; static/js/app.js intercepts the OOB swap, keeps the
     existing row, adds `.row-leaving`, then removes the node after the transition (a scoped,
     CSP-safe alternative to the View Transitions API, whose per-row scoping needs an inline style
     attr the CSP forbids).
   ENTER — the mirror: a genuinely-new row fades in while growing from 20%. It's an ANIMATION (not a
   transition) so it plays on insertion with no reflow dance; app.js tags rows `.row-entering` only
   when they arrive inside a `rows-entering` OOB prepend wrapper, so a full page load never animates.
   Reduced-motion users skip both (removal stays instant — app.js removes without the class). */
@media (prefers-reduced-motion: no-preference) {
  li.htmx-swapping,
  li.row-leaving {
    opacity: 0;
    transform: scale(.2);
    transform-origin: center;
    transition: opacity .6s ease, transform .6s ease;
  }
  li.row-entering {
    animation: row-enter .6s ease;
    transform-origin: center;
  }
  @keyframes row-enter {
    from { opacity: 0; transform: scale(.2); }
  }
}

/* Collapse/expand (2026-07-18): the Alpine `collapsible` section folds its list. The animation is
   pure CSS via the grid-rows 0fr↔1fr trick (no fixed heights, no JS measuring) — Alpine only flips
   `.is-collapsed` on the section. The inner wrapper must clip (overflow:hidden, min-height:0) for the
   0fr row to actually hide it. */
.collapsible-body {
  display: grid; grid-template-rows: 1fr;
  transition: grid-template-rows .28s ease;
}
/* When EXPANDED the inner must NOT clip — otherwise a row's ⋯ kebab dropdown (position:absolute,
   opens below the last row) gets cut off (2026-07-18 QA). It only needs to clip while COLLAPSED, so
   the 0fr grid row actually hides the content. `transition-behavior: allow-discrete` + a delay keeps
   it hidden until the OPEN animation finishes, so nothing spills out mid-unfold. */
.collapsible-inner {
  overflow: visible; min-height: 0;
  transition: overflow 0s 0.28s allow-discrete;
}
.collapsible.is-collapsed .collapsible-body { grid-template-rows: 0fr; }
.collapsible.is-collapsed .collapsible-inner {
  overflow: hidden;
  transition: overflow 0s allow-discrete;  /* clip immediately when folding shut */
}

/* The collapse toggle: «−/+  N  ▴/▾» next to the list title. Sits at the FAR RIGHT on desktop
   (margin-left:auto below), and right after the reorder ⇅ on a phone (default flow). CSS picks the
   sign+chevron from `.is-collapsed`: expanded shows `.on-open` (−, ▴), collapsed shows `.on-closed`
   (+, ▾). */
.btn-collapse {
  display: inline-flex; align-items: center; gap: .18rem;
  padding: .12rem .35rem; border: 1px solid var(--line-2); border-radius: 7px;
  background: transparent; color: var(--muted); cursor: pointer;
  font-size: .82rem; line-height: 1;
}
.btn-collapse:hover { color: var(--text); background: var(--hover); }
.btn-collapse .icon { width: 14px; height: 14px; }
.btn-collapse .collapse-sign { font-weight: 700; }
.btn-collapse .collapse-count { font-variant-numeric: tabular-nums; }
.btn-collapse .on-closed { display: none; }
.collapsible.is-collapsed .btn-collapse .on-open { display: none; }
.collapsible.is-collapsed .btn-collapse .on-closed { display: inline-flex; }

/* The picker: 25 radios rendered as swatches (templates/tracker/_color_field.html). The input
   stays in the DOM and focusable — only visually replaced by its dot — so keyboard and screen
   readers get the real radiogroup for free. The dot is painted by the same badge-c-* key. */
.color-swatches { display: flex; flex-wrap: wrap; gap: .45rem; margin: .1rem 0 .2rem; }
/* NB `color-swatch`, not `swatch`: `.swatch` is already the THEME picker's accent button (below,
   with its own background + aria-pressed styling) and it is defined LATER, so it silently won.
   Two-class selectors throughout for the other half of the problem: these live inside
   `.invite-inline`, whose `label {display:block; color:var(--muted)}` (0,2,0) would otherwise
   flatten every swatch into a muted block row. Same trap as `.section-head .head-add-inline`. */
.color-swatches .color-swatch {
  display: inline-flex; cursor: pointer; border-radius: 50%; margin: 0; padding: 0;
}
.color-swatches .color-swatch input {
  position: absolute; width: 1px; height: 1px; opacity: 0;   /* focusable, not display:none */
  margin: 0; pointer-events: none;
}
.color-swatches .color-swatch-dot {
  display: block; width: 26px; height: 26px; border-radius: 50%;
  background: currentColor; color: oklch(var(--badge-l) var(--badge-ch) var(--badge-h));
  box-shadow: 0 0 0 2px var(--bg-panel);
  transition: box-shadow .12s, transform .12s;
}
.color-swatches .color-swatch:hover .color-swatch-dot { transform: scale(1.12); }
/* The label above the grid is a <span>, not a <label> (nothing to point at — the radios each
   carry their own aria-label), so it needs the field-label styling explicitly. */
.field-row .field-label {
  display: block; margin-bottom: .25rem; color: var(--muted); font-size: .85rem;
}
.field-row { margin: 0 0 .75rem; }
/* Checked + focus both read as a ring in the swatch's own hue, so the choice is obvious without
   relying on colour alone (the checked one also grows). */
.color-swatches .color-swatch input:checked + .color-swatch-dot {
  transform: scale(1.12);
  box-shadow: 0 0 0 2px var(--bg-panel), 0 0 0 4px currentColor;
}
.color-swatches .color-swatch input:focus-visible + .color-swatch-dot {
  box-shadow: 0 0 0 2px var(--bg-panel), 0 0 0 4px var(--text);
}

/* Hue wheel (the value is the stored key — never the label, so colour survives i18n). */
.badge-c-red     { --badge-h: 25;  --badge-ch: .15; }
.badge-c-coral   { --badge-h: 35;  --badge-ch: .13; }
.badge-c-orange  { --badge-h: 50;  --badge-ch: .13; }
.badge-c-amber   { --badge-h: 65;  --badge-ch: .12; }
.badge-c-honey   { --badge-h: 80;  --badge-ch: .12; }
.badge-c-yellow  { --badge-h: 100; --badge-ch: .13; }
.badge-c-lime    { --badge-h: 125; --badge-ch: .14; }
.badge-c-green   { --badge-h: 150; --badge-ch: .13; }
.badge-c-emerald { --badge-h: 165; --badge-ch: .12; }
.badge-c-teal    { --badge-h: 185; --badge-ch: .10; }
.badge-c-cyan    { --badge-h: 205; --badge-ch: .10; }
.badge-c-sky     { --badge-h: 230; --badge-ch: .11; }
.badge-c-blue    { --badge-h: 255; --badge-ch: .13; }
.badge-c-indigo  { --badge-h: 275; --badge-ch: .14; }
.badge-c-violet  { --badge-h: 295; --badge-ch: .14; }
.badge-c-purple  { --badge-h: 310; --badge-ch: .13; }
.badge-c-magenta { --badge-h: 330; --badge-ch: .14; }
.badge-c-pink    { --badge-h: 350; --badge-ch: .12; }
.badge-c-rose    { --badge-h: 10;  --badge-ch: .11; }
/* The muted family — same hues, low chroma, for terms that shouldn't shout. */
.badge-c-brown   { --badge-h: 45;  --badge-ch: .05; }
.badge-c-sand    { --badge-h: 75;  --badge-ch: .04; }
.badge-c-olive   { --badge-h: 115; --badge-ch: .04; }
.badge-c-slate   { --badge-h: 240; --badge-ch: .04; }
.badge-c-mauve   { --badge-h: 315; --badge-ch: .04; }
.badge-c-gray    { --badge-h: 70;  --badge-ch: .01; }

/* ─── Theme picker (nav) ───────────────────────────────────────────────────────────────
   A native <details> disclosure: <summary> is the trigger, the <form> is the dropdown.
   Swatches carry their own data-accent, so the [data-accent="…"] blocks above paint each
   one in its hue — the palette has a single source of truth. */
.theme-menu { position: relative; }
.theme-toggle {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 26px; border-radius: 7px;
  border: 1px solid var(--line-2); color: var(--muted);
}
.theme-toggle::-webkit-details-marker { display: none; } /* Safari's default triangle */
.theme-toggle:hover { color: var(--text); }
.theme-menu[open] .theme-toggle { color: var(--accent); border-color: var(--accent); }

.theme-panel {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 60;
  width: 216px; padding: .9rem;
  background: var(--surface); border: 1px solid var(--line-2); border-radius: 12px;
  box-shadow: 0 18px 44px rgba(0,0,0,.45);
}
.theme-section-label {
  margin: 0 0 .55rem; font-size: .68rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
}
.swatches + .theme-section-label { margin-top: .95rem; }

.swatches { display: grid; grid-template-columns: repeat(5, 1fr); gap: .5rem; }
.swatch {
  width: 100%; aspect-ratio: 1 / 1; padding: 0;
  border-radius: 50%; cursor: pointer;
  background: var(--accent); /* data-accent on the button scopes this to its own hue */
  border: 2px solid transparent; box-shadow: 0 0 0 1px var(--line-2);
  transition: transform .1s ease;
}
.swatch:hover { transform: scale(1.12); }
.swatch[aria-pressed="true"] {
  border-color: var(--surface); box-shadow: 0 0 0 2px var(--text);
}

.mode-toggle { display: flex; gap: .4rem; }
.mode-btn {
  flex: 1; padding: .42rem .5rem; border-radius: 8px; cursor: pointer;
  font: inherit; font-weight: 700; font-size: .8rem;
  background: var(--bg); color: var(--muted); border: 1px solid var(--line-2);
}
.mode-btn:hover:not([aria-pressed="true"]) { color: var(--text); }
.mode-btn[aria-pressed="true"] {
  background: var(--accent); color: var(--on-accent); border-color: var(--accent);
}

/* ─── Toggle switch (modern on/off; e.g. project "Locked") ──────────────────────────────
   A <label class="toggle"> wraps a visually-hidden checkbox + a track/thumb. The checkbox
   precedes the track, so :checked ~ .toggle-track slides + tints it. Fully keyboard- and
   no-JS-friendly (it's a real checkbox). */
/* label.toggle: the element+class specificity beats `.invite-inline label { display:block }`. */
label.toggle { display: flex; align-items: flex-start; gap: .7rem; cursor: pointer; margin: .2rem 0; }
.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
.toggle-track {
  position: relative; flex: 0 0 auto; margin-top: .1rem;
  width: 40px; height: 22px; border-radius: 999px;
  background: var(--bg); border: 1px solid var(--line-2); transition: background .15s, border-color .15s;
}
.toggle-thumb {
  position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%;
  background: var(--text-3); transition: transform .15s, background .15s;
}
.toggle input:checked ~ .toggle-track { background: var(--accent); border-color: var(--accent); }
.toggle input:checked ~ .toggle-track .toggle-thumb { transform: translateX(18px); background: var(--on-accent); }
.toggle input:focus-visible ~ .toggle-track { outline: 2px solid var(--accent); outline-offset: 2px; }
.toggle-text { display: flex; flex-direction: column; gap: .15rem; }
.toggle-label { font-weight: 600; color: var(--text); font-size: .9rem; }
.toggle-hint { color: var(--muted); font-size: .78rem; }

/* ─── Section item count (replaces the per-row code numbers — user request 2026-07-07) ─── */
.section-count {
  margin: .1rem 0 1.4rem; color: var(--muted);
  font-size: .78rem; letter-spacing: .02em;
}

/* ═══ Mobile (master.md Rule 12 — 100% usable on a phone) ════════════════════════════════
   Retrofit pass for everything built so far; see docs/MOBILE.md. Everything above is the
   desktop baseline — this block reflows it for small screens (no horizontal scroll, rows
   wrap, comfortable tap targets). New features should be built mobile-first from the start. */
@media (max-width: 640px) {
  #app { padding: 1.1rem 1rem 2.5rem; }

  /* --- Top nav on a phone (user request 2026-07-10): the header keeps the brand (= Home),
     the Projects + Members section links, and the name-badge. The account-level controls —
     Profile, language, theme, Log out — move into the dropdown the badge opens (.user-menu).
     Anonymous users (no .is-auth) keep the wrapping nav + switchers. --- */
  .topnav { flex-wrap: wrap; gap: .5rem .7rem; padding: .55rem .9rem; }
  .topnav-spacer { display: none; }               /* let items pack; the wrap does the spacing */

  .is-auth .topnav > a.user-chip,
  .is-auth .topnav > .theme-menu,
  .is-auth .topnav > .lang-switch,
  .is-auth .topnav > .logout-link { display: none; }

  /* Section links drop to their own full-width row below the brand + badge (order:5 sends
     them past the default-order items, width:100% forces the wrap — user request 2026-07-10). */
  .navlinks { order: 5; width: 100%; gap: 1.4rem; padding-top: .1rem; }
  .navlinks a { padding: .3rem 0; white-space: nowrap; }
  .lang-switch button { padding: .4rem .65rem; }
  .theme-toggle { width: 38px; height: 34px; }

  /* --- The name-badge dropdown --- */
  .user-menu { display: block; position: relative; margin-left: auto; }
  .user-menu > summary.user-chip { cursor: pointer; list-style: none; white-space: nowrap; }
  .user-menu > summary.user-chip::-webkit-details-marker { display: none; }
  .user-menu[open] > summary.user-chip { background: var(--hover); }
  .chip-caret { color: var(--muted); transition: transform .15s; }
  .user-menu[open] .chip-caret { transform: rotate(180deg); }
  /* The mobile name-badge shows avatar + name only — drop the role badge (QA/Programmer…) to
     keep the header uncluttered (user request 2026-07-14). Members/invite/profile badges are
     unaffected: they aren't inside .user-chip. */
  .user-menu > summary.user-chip .role-badge { display: none; }

  .menu-panel {
    position: absolute; right: 0; top: calc(100% + 8px); z-index: 60;
    width: min(264px, calc(100vw - 1.5rem)); padding: .5rem;
    display: flex; flex-direction: column;
    background: var(--surface); border: 1px solid var(--line-2); border-radius: 12px;
    box-shadow: 0 18px 44px rgba(0,0,0,.45);
  }
  .menu-item {
    display: flex; align-items: center; gap: .65rem;
    padding: .62rem .55rem; border-radius: 8px;
    color: var(--text); text-decoration: none; font-weight: 600; font-size: .95rem;
  }
  .menu-item:hover { background: var(--hover); }
  .menu-item.logout { color: var(--muted); }
  .menu-icon { flex: none; width: 18px; height: 18px; }
  .menu-div { border-top: 1px solid var(--line); margin: .45rem .25rem; }

  /* The shared language + theme forms render inline inside the panel (not as their desktop
     dropdowns). */
  .menu-panel .lang-switch { padding: .1rem .35rem .3rem; gap: .4rem; }
  .menu-panel .lang-switch button { padding: .45rem .85rem; font-size: .8rem; }
  .menu-panel .theme-panel {
    position: static; width: auto; padding: .35rem .35rem 0;
    background: none; border: none; box-shadow: none;
  }
  .menu-panel .theme-section-label { margin-bottom: .45rem; }
  .menu-panel .swatches + .theme-section-label { margin-top: .7rem; }

  /* --- Page header (title + action buttons) stacks; actions wrap. --- */
  .members-head { align-items: flex-start; }
  /* full width so the ⋯ can sit at the far right (margin-left:auto on .head-menu). */
  .head-actions { display: flex; width: 100%; flex-wrap: wrap; gap: .4rem; align-items: stretch; }

  /* --- In-project (container) header, phone (user request 2026-07-14). The lock badge + ⋯ kebab
     sit to the RIGHT of the project name and drop BELOW it (still right-aligned) when the name is
     long; New note / New section leave the header for the section headings. Scoped to
     .container-page so the projects / members / teams headers keep their full-width action row. --- */
  .container-page .head-add { display: none; }               /* moved to the section headings */
  /* column-gap is .4rem, not the .5rem it was: the lock and the ⋯ used to be siblings inside
     .head-actions (gap .4rem) and are now separated by THIS gap instead, so .4rem keeps the phone
     spacing byte-identical to before the lock moved. The name→lock gap is unaffected either way —
     #lock-control's margin-left:auto absorbs whatever's left over. */
  .container-page .members-head { align-items: center; gap: .3rem .4rem; }
  /* The title cluster is flattened here (desktop pairs the lock with the name inside it) so the
     name, the lock and the ⋯ are one flex line again — the lock keeps hugging the right next to
     the kebab, as before. NB the h1 is no longer a DOM child of .members-head, so this must not
     use a `>` combinator. */
  .container-page .head-title { display: contents; }
  .container-page #container-h1 { flex: 0 1 auto; min-width: 0; }

  /* The container page's headings are the browser defaults (2em / 1.5em) — far too heavy on a
     phone (user request 2026-07-15). The title drops to the app's page-title size (matching
     .members h1) and the section headings drop below it so the hierarchy still reads. Margins are
     em-based, so they scale down with the font. Desktop keeps the defaults. */
  .container-page #container-h1 { font-size: 1.5rem; letter-spacing: -.3px; }
  .container-page .section-head h2 { font-size: 1.15rem; }
  /* The lock now carries the right-push that .head-actions used to (it precedes the kebab), so
     [lock][⋯] still travel together to the right edge — including when the lock renders empty
     (non-manager on an unlocked project): a zero-width item with margin-left:auto still absorbs
     the free space, leaving the kebab hard right. */
  .container-page #lock-control { margin-left: auto; }
  .container-page .head-actions {                            /* just the [⋯], after the lock */
    width: auto; flex: 0 0 auto; flex-wrap: nowrap; margin-left: 0; align-items: center;
  }
  .container-page .head-menu { margin-left: 0; }             /* grouped with the lock badge, not far-right */
  /* Regular-size ⋯ (don't stretch to the lock button's height — the New note/New section buttons it
     used to match have moved to the section headings). User request 2026-07-14. */
  .container-page .head-menu > label.btn-kebab { align-self: center; height: 30px; }

  /* New note / New section move to the right of their section heading (the .head-add-inline copy). */
  .container-page .section-head {
    display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  }
  .container-page .section-head .head-add-inline { display: inline-block; }

  /* --- Comfortable tap targets on the main buttons. --- */
  .btn, .btn-ghost, .mode-btn, .member-edit button { padding-top: .55rem; padding-bottom: .55rem; }

  /* --- List rows wrap so nothing is pushed off-screen. The per-row actions collapse behind a
     ⋯ kebab menu at the row's end (user request 2026-07-10): the inline buttons are hidden and
     the .row-menu dropdown takes over. --- */
  /* Phones always use the ⋯ kebab (its appearance is defined in the base; here we only flip
     visibility). The desktop opt-in (body.kebab-actions) does the same flip at any width. */
  .project-list li { flex-wrap: wrap; }
  .row-actions { display: none; }
  .row-menu { display: block; }
  .member-edit { flex-wrap: wrap; }

  /* Two-column rows (user request 2026-07-14): column 1 stacks the name over its badges, column 2
     is the ⋯ kebab spanning both rows. Applies to the "Your projects" list (#mine-list: team /
     Locked / Owner / "!" badges) and the in-project Notes list (#notes: type / priority / state /
     assignee). Scoped by id so the teams / sections / joinable lists (also .project-list) keep
     their single-line wrap. */
  #mine-list > li, #notes > li {
    display: grid; grid-template-columns: 1fr auto; align-items: center;
    column-gap: .6rem; row-gap: .3rem;
  }
  #mine-list > li > .project-name, #notes > li > .project-name { grid-column: 1; grid-row: 1; min-width: 0; }
  #mine-list .row-badges, #notes .row-badges {
    grid-column: 1; grid-row: 2;
    display: flex; flex-wrap: wrap; align-items: center; gap: .35rem;
  }
  #mine-list > li > .row-menu, #notes > li > .row-menu { grid-column: 2; grid-row: 1 / span 2; align-self: stretch; margin-left: 0; }

  /* Personal-sort drag handle on phones (2026-07-17, user-approved): add a LEFT column for the
     handle, spanning both rows (mirrors the ⋯ kebab on the right), so the tuned name-over-badges
     stack is untouched. Drag starts immediately from the handle (app.js SortableJS forceFallback;
     no press-delay). Applies to the two-column lists #mine-list AND #notes. These override the
     shared placements above at equal specificity by source order. */
  #mine-list > li, #notes > li { grid-template-columns: auto 1fr auto; }
  #mine-list > li > .drag-handle, #notes > li > .drag-handle {
    grid-column: 1; grid-row: 1 / span 2; align-self: center;
  }
  #mine-list > li > .project-name, #notes > li > .project-name { grid-column: 2; }
  #mine-list .row-badges, #notes .row-badges { grid-column: 2; }
  #mine-list > li > .row-menu, #notes > li > .row-menu { grid-column: 3; }
  /* Make the ⋯ button fill the two-row height so it reads as one control spanning both rows, and
     scale up the dots to match that taller target (user request 2026-07-14). */
  #mine-list .row-menu .btn-kebab, #notes .row-menu .btn-kebab { height: 100%; }
  #mine-list .row-menu .btn-kebab .icon, #notes .row-menu .btn-kebab .icon { width: 26px; height: 26px; }

  /* Members list: the same two columns (user request 2026-07-14), but the role badge stays on the
     FIRST row with the name; row 2 holds authority / permission badges / "you". Column 2 is the
     Edit button, centered across both rows. Scoped to display rows (:has(.member-main)) so the
     inline edit form keeps its own flex flow. */
  #member-rows > li:has(.member-main) {
    display: grid; grid-template-columns: 1fr auto; align-items: center;
    column-gap: .6rem; row-gap: .3rem;
  }
  #member-rows .member-main {
    grid-column: 1; grid-row: 1;
    display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
  }
  #member-rows .member-name { margin-right: 0; }  /* drop the desktop right-push */
  #member-rows .member-badges {
    grid-column: 1; grid-row: 2;
    display: flex; flex-wrap: wrap; align-items: center; gap: .35rem;
  }
  #member-rows .member-badges:not(:has(*)) { display: none; }  /* no row-2 content → single line */
  #member-rows > li > .member-actions {
    grid-column: 2; grid-row: 1 / span 2; align-self: center;
    display: flex; flex-direction: column; gap: .35rem;
  }
  #member-rows .member-actions .btn-ghost { margin-left: 0; }

  /* Personal-sort handle on the members list (2026-07-17): a LEFT column spanning both rows (like
     #mine-list/#notes), so the name-over-badges stack is untouched and Edit stays on the right. Only
     display rows (:has(.member-main)) become the 3-column grid; the inline edit-form li is untouched. */
  #member-rows > li:has(.member-main) { grid-template-columns: auto 1fr auto; }
  #member-rows > li > .drag-handle { grid-column: 1; grid-row: 1 / span 2; align-self: center; }
  #member-rows .member-main { grid-column: 2; }
  #member-rows .member-badges { grid-column: 2; }
  #member-rows > li > .member-actions { grid-column: 3; }

  /* Container-detail header: New note + New section stay inline; Lock/Members/Edit/Delete fold
     into a ⋯ dropdown (user request 2026-07-10). Single instance, checkbox-toggled so it opens
     with no JS; app.js only adds outside-click / after-pick close. */
  /* inline-flex + margin-left:auto pushes the ⋯ to the row's right edge. 1px top/bottom padding
     on the wrapper (symmetric ⇒ stays centered) insets the stretched button so it's ~2px shorter
     than the buttons (user request). */
  .head-menu { display: inline-flex; position: relative; margin-left: auto; box-sizing: border-box; padding: 1px 0; }
  .head-menu-panel { display: none; }
  .head-menu > label.btn-kebab { display: inline-flex; height: auto; align-self: stretch; }
  .head-menu-cb:checked ~ .head-menu-panel {
    display: flex; flex-direction: column; gap: .35rem;
    position: absolute; right: 0; top: calc(100% + 6px); z-index: 60;
    min-width: 194px; padding: .45rem;
    background: var(--surface); border: 1px solid var(--line-2); border-radius: 10px;
    box-shadow: 0 14px 36px rgba(0,0,0,.4);
  }
  /* The management controls stack full-width as menu rows inside the dropdown. They are flex
     containers now (see the shared `.btn, .btn-ghost` rule), so their labels are held left by the
     default justify-content:flex-start — `text-align` alone no longer does it. */
  .head-menu-panel .btn-ghost { width: 100%; text-align: left; }

  /* --- Profile: label/value grid becomes a single column. --- */
  .profile-fields { grid-template-columns: 1fr; gap: .1rem .5rem; }
  .profile-fields dt { margin-top: .7rem; }
  .profile-fields dt:first-of-type { margin-top: 0; }

  /* --- Theme panel can't run off a narrow screen. --- */
  .theme-panel { width: min(216px, calc(100vw - 1.5rem)); }
}

/* ═══ Personal Dashboard (2026-07-20 shell slice; design/dashboard.mockup.html v3) ═══════════
   Desktop-only three-pane screen — full-bleed under the top-nav via #app:has(.dash-panes)
   escaping the shared #app max-width/padding. Reuses the app's own badge classes (the badge and
   badge-c-/badge-prio- palette chips) — only the pane/tree/pill layout below is new. */
/* Height (2026-07-25): the panes fill whatever the top-nav leaves, DERIVED — never a magic
   number. This was `calc(100vh - 47px)` while the real offset is 57px (22px nav content + 1.5rem
   padding + 1px border), so the grid overran the viewport by exactly 10px at every window size:
   that was both the stray page scroll AND the chat compose box sitting below the fold. Making the
   body a flex column means the nav measures itself, so a nav that grows (wrapping on a narrow
   window, a bigger font) can never push the panes off-screen again. */
body:has(.dash-panes) {
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh;          /* dvh where supported: excludes mobile browser chrome */
  overflow: hidden;        /* the panes scroll internally; the PAGE never does */
}
body:has(.dash-panes) .topnav { flex: none; }
#app:has(.dash-panes) {
  max-width: none; padding: 0; margin: 0;
  flex: 1 1 auto;
  min-height: 0;           /* without this a flex child refuses to shrink below its content */
}

.dash-panes {
  display: grid;
  grid-template-columns: 330px minmax(0, 1fr) 400px;  /* rail widened 300 -> 330 (2026-07-25) */
  height: 100%;
  min-height: 0;           /* no floor: the panes always fit, each scrolling on its own */
}
/* Grid/flex children default to min-*: auto, which blocks shrinking — so on a short window the
   panes would grow past the viewport instead of scrolling internally. */
.dash-rail, .dash-mid, .dash-chat { overflow-y: auto; min-height: 0; min-width: 0; }

.dash-rail {
  background: var(--bg-sidebar);
  border-right: 1px solid var(--border-2);
  padding: .8rem .6rem;
  display: flex;
  flex-direction: column;
  gap: .1rem;
}
.dash-rail-label {
  font-size: 10.5px; letter-spacing: .6px; color: var(--text-muted); font-weight: 700;
  padding: .2rem .5rem .45rem;
}
.dash-node-project, .dash-node-section, .dash-node-note {
  display: flex; align-items: center; gap: .45rem; border-radius: 8px; cursor: pointer;
  color: var(--text-2);
}
.dash-node-project:hover, .dash-node-section:hover, .dash-node-note:hover { background: var(--hover); }
/* Indentation (2026-07-25) is driven by NESTING, not per-row padding. It used to be a fixed
   padding-left per row type — 1.35rem for any section at any depth, 2.5rem for any note — so a
   nested section never stepped right, and a project's own notes (2.5rem) sat DEEPER than that
   project's sections (1.35rem), reading as if they belonged to a section. Now each
   .dash-node-kids adds one step, so depth compounds on its own however deep the tree goes. */
.dash-node-project { padding: .45rem .5rem; font-size: 13.5px; font-weight: 700; }
.dash-node-section { padding: .32rem .5rem; font-size: 12.5px; font-weight: 600; color: var(--text-3); }
/* A note row is TWO lines since 2026-07-25 (title, then its chips), so it stacks rather than
   sitting in a row. The dot is gone — the state badge carries state now — but its 14px slot has
   to be paid back as padding, or a note's text would start 14px+gap left of a SIBLING section's
   name and the shared level would stop lining up (the same alignment the dot was widened for). */
.dash-node-note {
  padding: .28rem .5rem .28rem calc(.5rem + 14px + .45rem);
  font-size: 12px; color: var(--text-3);
  flex-direction: column; align-items: stretch; gap: .18rem;
  position: relative;  /* anchors the unseen eye — see below */
}
/* The EYE (2026-07-28, the user's QA point 5) reclaims the 14px slot the old status dot left
   behind — the same slot the padding above pays back to keep note titles aligned with sibling
   section names. So it costs the row NO layout change and nothing shifts when the state flips.
   It has to be absolute: the row is a COLUMN flex (title over chips), so an in-flow eye would sit
   on its own line above the title rather than beside it.
   Accent when unread (matching .dash-new — in this rail, brand colour means "unseen"), dim when
   read. It renders on EVERY row: it is a toggle, so it has to be there to be clicked. */
.dash-note-eye {
  position: absolute; left: .5rem; top: .3rem;
  width: 14px; height: 14px; padding: 0; border: 0; background: none;
  color: var(--brand); cursor: pointer; line-height: 0;
}
.dash-note-eye svg { width: 14px; height: 14px; display: block; }
/* Read = present but receding. Kept faint enough not to compete with the unread ones down the
   rail, but it lifts on hover so it reads as clickable rather than as a dead marker. */
.dash-note-eye.is-read { color: var(--text-muted); opacity: .45; }
.dash-note-eye.is-read:hover, .dash-note-eye.is-read:focus-visible { opacity: 1; }
.dash-note-eye:hover { transform: scale(1.15); }
.dash-note-eye:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 3px; }
@media (prefers-reduced-motion: reduce) { .dash-note-eye:hover { transform: none; } }
.dash-note-chips { display: flex; flex-wrap: wrap; gap: .25rem; }
/* Tree chips are a size down from the app's standard badge — two of them plus up to 65px of
   indent has to survive a 300px rail. */
.dash-note-chips .badge { font-size: .58rem; padding: 0 5px; letter-spacing: .3px; }
.dash-node-name, .dash-node-note-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* `hidden` must beat .dash-new's own display (2026-07-25 live-tree): the pill now renders ALWAYS
   so the poller has an OOB target, and carries its zero state as [hidden] rather than by being
   absent from the template. Without this the chip would show a bare "0". */
.dash-new[hidden] { display: none; }
/* The kids block splits into sections + notes (2026-07-25 live-tree) so reordering notes never
   touches nested sections. Both are plain blocks — the indent/guide line stays on .dash-node-kids,
   so nesting-based depth (commit 1cdeeba) is unaffected. */
.dash-node-kids { display: none; }
.dash-node.is-open > .dash-node-kids {
  display: block;
  /* One indent step = margin + padding (12px). The border sits between them, giving a thin
     guide line down each open level — var(--line) is defined per theme, so it matches in both
     dark and light without a second rule. */
  margin-left: 7px;
  padding-left: 5px;
  border-left: 1px solid var(--line);
}
.dash-twisty { width: 14px; color: var(--text-muted); font-size: 10px; flex: none; text-align: center; }
/* A note has a dot where a section has a twisty. Padding it out to the twisty's 14px keeps a
   note's title aligned with the name of a SIBLING section at the same depth — without this they
   sit 7px apart and the shared level stops being readable. */
.dash-dot {
  width: 7px; height: 7px; border-radius: 50%; flex: none;
  margin-inline: 3.5px;
}
.dash-dot-c-purple  { background: oklch(var(--badge-l) 0.13 310); }
.dash-dot-c-blue    { background: oklch(var(--badge-l) 0.13 250); }
.dash-dot-c-honey   { background: oklch(var(--badge-l) 0.13 80); }
.dash-dot-c-emerald { background: oklch(var(--badge-l) 0.13 160); }
.dash-dot-c-coral   { background: oklch(var(--badge-l) 0.13 35); }
.dash-dot-c-teal    { background: oklch(var(--badge-l) 0.13 190); }
.dash-dot-c-violet  { background: oklch(var(--badge-l) 0.13 290); }
/* Read-marker (2026-07-25, piece ③): the ACCENT pill is now the UNSEEN count and renders only
   when > 0; the total moved to the always-dim .dash-count beside it, so you keep both signals
   ("2 need my attention, 5 are mine") and the tree never goes blank once you're caught up.
   The node name flexes so the two pills sit right without competing `margin-left:auto`. */
.dash-node-name { flex: 1 1 auto; }
.dash-new, .dash-count {
  margin-left: 6px; font-size: 10.5px; font-weight: 800;
  border-radius: 999px; padding: 0 8px; line-height: 1.5; flex: none;
}
.dash-new { background: var(--brand); color: var(--on-brand); }
.dash-count { background: var(--bg-panel-2); color: var(--text-muted); border: 1px solid var(--border-2); }
.dash-node-note-title.is-new, .dash-lrow-title.is-new { font-weight: 800; color: var(--text); }

/* Hover tooltip for a tree note's description (2026-07-25). position:fixed rather than absolute
   inside the row: the rail is overflow-y:auto, so an in-flow popup would be clipped at the rail's
   edges. app.js sets left/top via CSSOM (which CSP's style-src does not govern — that covers
   parsed style= attributes and <style> blocks) and toggles .is-on after a 2s dwell. */
.dash-tip {
  position: fixed; z-index: 60; max-width: 280px;
  background: var(--bg-panel-2); color: var(--text-2);
  border: 1px solid var(--border-2); border-radius: 8px;
  padding: .45rem .6rem; font-size: 11.5px; line-height: 1.45;
  box-shadow: 0 6px 20px rgba(0,0,0,.28);
  opacity: 0; visibility: hidden; transition: opacity .12s ease;
  pointer-events: none;                 /* never eats the hover that summoned it */
}
.dash-tip.is-on { opacity: 1; visibility: visible; }
.dash-tip.is-muted { color: var(--text-muted); font-style: italic; }

.dash-filters {
  display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .8rem;
  border-top: 1px solid var(--line); padding-top: .7rem;
}
.dash-filters-label { flex-basis: 100%; padding: 0 .5rem .3rem; }
.dash-pill {
  background: var(--bg-panel-2); border: 1px solid var(--border-2); color: var(--text-3);
  border-radius: 999px; padding: .26rem .65rem; font-size: 11.5px; font-weight: 600; cursor: pointer;
}
.dash-pill.active { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }

.dash-mid { background: var(--bg); padding: 1.1rem 1.4rem; }
.dash-mid-empty { color: var(--text-muted); font-size: 13px; padding: 2rem 0; text-align: center; }
.dash-crumb { color: var(--text-muted); font-size: 12px; margin-bottom: .5rem; }
.dash-crumb a { color: var(--text-3); text-decoration: none; cursor: pointer; }
.dash-crumb a:hover { text-decoration: underline; }
.dash-crumb-sep { margin: 0 .3rem; }
.dash-back {
  display: inline-block; background: none; border: 1px solid var(--line-2); color: var(--text-3);
  border-radius: 7px; font-size: 12px; padding: .2rem .6rem; cursor: pointer; margin-bottom: .7rem;
}
/* Full-height flex column so an empty attachments block can grow to fill the middle pane. */
.dash-note { display: flex; flex-direction: column; min-height: 100%; }
.dash-note-title { font-size: 18px; font-weight: 700; margin: 0 0 .5rem; }
/* `align-items: center` matters since 2026-08-02: the people circles now share this row with
   the badges and are twice their height, so without it the badges hang off the top. */
.dash-note-chips { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; margin-bottom: .4rem; }
/* Title left, badges right, on one row (2026-07-31, QA point 7 round 2, item 7). `flex-wrap` lets
   the chips drop under the title on a narrow pane rather than squeezing the title to nothing. */
.dash-note-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: .75rem; flex-wrap: wrap;
}
.dash-note-head .dash-note-title { margin: 0; flex: 1 1 12ch; min-width: 0; }
.dash-note-head .dash-note-chips { margin-bottom: 0; flex: 0 0 auto; }
/* A note badge that CHANGES the note (2026-07-31, QA point 7 round 2, item 4). <details> is the
   house dropdown pattern (.row-menu, .head-menu) — no JS, so the nonce-based CSP stays clean. */
.badge-menu { position: relative; display: inline-block; }
.badge-menu > summary { cursor: pointer; list-style: none; user-select: none; }
.badge-menu > summary::-webkit-details-marker { display: none; }
.badge-menu-caret { margin-left: .22rem; font-size: .85em; opacity: .8; }
.badge-menu-panel {
  position: absolute; z-index: 40; top: calc(100% + 4px); right: 0; min-width: 9rem;
  display: flex; flex-direction: column; gap: .15rem; padding: .25rem;
  background: var(--bg-panel-2); border: 1px solid var(--border-2); border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, .18);
}
.badge-menu-item {
  background: none; border: 0; padding: .2rem .3rem; text-align: left; cursor: pointer;
  border-radius: 5px;
}
.badge-menu-item:hover { background: var(--hover); }
/* Inside the badge row since 2026-08-02 (the user asked for the circles in front of the other
   badges), so it carries no margins of its own any more — the row's gap spaces it. */
.dash-note-people { display: inline-flex; align-items: center; gap: .35rem; margin: 0; }
.dash-note-people-arrow { color: var(--text-muted); font-size: .8rem; }
/* One member as a circle of initials (2026-07-31, QA point 7 round 2, item 5). Tinted from the
   member's ROLE via the shared badge palette, so it matches the role badges beside it.
   ⚠️ The fill MUST be composed with oklch(), fixed 2026-08-02 (QA round 3). `badge-c-<name>` sets
   `--badge-h`/`--badge-ch` to the hue and chroma NUMBERS (240, .04) — not colours. This rule used
   to say `background: var(--badge-h); color: var(--badge-ch)`, i.e. `background: 240`, which is
   invalid, so the browser dropped BOTH declarations: a correctly shaped circle with no fill and
   default-coloured initials, which reads as "the circles are missing". Recipe copied from
   .dash-msg-avatar, the app's other filled initials circle. */
.member-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.55rem; height: 1.55rem; border-radius: 50%; flex: 0 0 auto;
  font-size: .62rem; font-weight: 700; letter-spacing: .3px;
  background: oklch(var(--badge-l) var(--badge-ch) var(--badge-h));
  color: var(--on-brand);
}
/* Full pane width (2026-08-02, the user's QA round 3) — the 72ch prose measure it used to carry
   left the right of the pane empty, and the user asked for the width instead.
   `overflow-wrap: anywhere` is the other half of the same report: the box did not CONTAIN long
   text. A note body is user input, so it can be one unbroken 300-character token (a URL, a stack
   frame, a pasted path), and normal wrapping only breaks at spaces — without this such a line
   runs straight out of the rounded box. `anywhere` rather than `break-word` so the long token is
   also allowed to shrink the box's min-content width instead of forcing the pane wider. */
.dash-note-body {
  background: var(--bg-detail); border: 1px solid var(--border-2); border-radius: 10px;
  padding: .9rem 1.05rem; font-size: 13.5px; color: var(--text-2); margin-bottom: 1.1rem;
  overflow-wrap: anywhere; min-width: 0;
}
.dash-note-stamp {
  display: flex; align-items: center; gap: .4rem;
  font-size: 11px; color: var(--text-muted); margin-bottom: .35rem; letter-spacing: .5px;
}
/* Author-only inline edit of the note body (2026-07-31, QA point 7 round 2, item 6). */
.dash-note-desc-edit {
  background: none; border: 0; padding: 0 .1rem; cursor: pointer; line-height: 1;
  color: var(--text-muted); display: inline-flex; align-items: center;
}
.dash-note-desc-edit:hover { color: var(--text); }
.dash-note-desc-input {
  width: 100%; resize: vertical; font: inherit; color: var(--text-2);
  background: var(--bg-panel-2); border: 1px solid var(--border-2); border-radius: 7px;
  padding: .5rem .6rem;
}
.dash-note-desc-actions { display: flex; gap: .4rem; margin-top: .5rem; }

.dash-filterbar { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: .9rem; }
.dash-filterbar select {
  background: var(--bg-panel-2); color: var(--text-2); border: 1px solid var(--border-2);
  border-radius: 8px; padding: .3rem .55rem; font: inherit; font-size: 12.5px; cursor: pointer;
}
.dash-mid-h2 { font-size: 14.5px; margin: 0 0 .4rem; }
.dash-mid-empty-note { color: var(--text-muted); font-size: 12.5px; }
/* Full pane width (2026-08-02, QA point 7 round 3). The row used to carry `max-width: 84ch` from
   the 2026-07-20 dashboard shell — a prose readability cap, written before the row grew a path,
   badges and a description line. Those make it a DATA row, where the cap just left ~15% of the
   pane empty on the right.
   TWO lines, not three (2026-08-02, item 3.2): the path, then everything else on one line. The row
   is a COLUMN now — it used to be a wrapping row where `flex-basis: 100%` on the path and the
   description made line breaks emerge from the wrap, and a wrapping container cannot hold a line
   that must not wrap. */
.dash-lrow {
  display: flex; flex-direction: column; gap: .15rem;
  /* 25% off the original .55rem/.3rem, the user's call on 2026-08-02. Kept as exact quarters
     rather than rounded to .4/.22, so the ratio to every other row in the app stays legible. */
  border-top: 1px solid var(--line); padding: .4125rem .225rem; cursor: pointer; border-radius: 7px;
}
.dash-lrow:hover { background: var(--hover); }
/* Path left, the assignee's circle at the right end (2026-08-02, QA round 3). It carries no
   `margin-left: auto` — the path GROWS instead, for the same reason .dash-lrow-chips doesn't: an
   auto margin would eat the free space before flex-grow ran. Right-aligned here and on the line
   below, so the circle sits directly over the badges. */
.dash-lrow-pathrow { display: flex; align-items: center; gap: .55rem; min-width: 0; }
/* Smaller here than anywhere else (2026-08-02, QA round 3, the user's call). The pane-wide 1.55rem
   circle is taller than the 11px path text it shares a line with, so it cost the row 8px of the
   height 3.2 had just saved. Sized to the BADGES directly below it instead, so the two
   right-aligned columns read as one. */
.dash-lrow .member-avatar { width: 1.25rem; height: 1.25rem; font-size: .5rem; letter-spacing: 0; }
.dash-lrow-path {
  color: var(--text-muted); font-size: 11px;
  flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The single line. `nowrap` is the whole point; `min-width: 0` lets the children below actually
   shrink instead of forcing the row wider than the pane.
   `overflow: hidden` is the floor, not the mechanism: the description absorbs all the pressure
   down to a ~1200px viewport (measured — a 416px pane, description squeezed 384px → 84px), but
   below that it is already at zero and the badges cannot shrink, so the line would spill OVER the
   discussion pane. This clips it at the row's own edge instead. */
.dash-lrow-main {
  display: flex; align-items: center; gap: .55rem; flex-wrap: nowrap; min-width: 0;
  overflow: hidden;
}
/* Shrinks only AFTER the description has collapsed, so a long title can't push the badges out of
   view — but it still ellipsizes rather than overflowing. */
.dash-lrow-title {
  font-size: 13.5px; font-weight: 600; flex: 0 1 auto; min-width: 4ch;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Badges and the clip keep their intrinsic width at every pane size; the badges finish the line.
   ⚠️ NO `margin-left: auto` here to push them right — an auto margin swallows all positive free
   space BEFORE flex-grow is applied, which would starve `.dash-lrow-desc` (basis 0) to nothing and
   make every description invisible. The description's own `flex-grow` is what right-aligns these,
   which is why it renders even when the note has no text. */
.dash-lrow-chips { display: flex; gap: .35rem; flex: 0 0 auto; }
.dash-lrow .note-clip { flex: 0 0 auto; margin-left: 0; }
/* The note's own text on its own line (2026-07-31, QA point 7 round 2, item 10). `truncatechars`
   caps what SHIPS; `text-overflow` handles "not enough space" at whatever width the pane ends up,
   which server-side truncation alone can't know. */
/* The eye rides list rows too (2026-07-31, item 9); it keeps the tree row's COLOURS but not its
   positioning. ⚠️ `position: static` is load-bearing and was missing until 2026-08-02: the base
   `.dash-note-eye` is `position: absolute` (it is pinned into the tree row's indent), and an
   out-of-flow element ignores `flex`, so this override did nothing on its own. `.dash-lrow` is not
   a positioned ancestor either, so every list eye escaped its row and stacked in the corner of
   whatever ancestor was positioned. Measured, not read: a Playwright probe put all ten eyes at
   top=4.8px while their rows sat 146px+ down the page. */
.dash-lrow .dash-note-eye { position: static; flex: 0 0 auto; margin-right: .1rem; }
/* `flex: 1 1 0` — takes the leftover space and nothing more, so it is the ONLY elastic part of the
   line and the first thing to give way as the pane narrows; everything else keeps its intrinsic
   width. Doubles as the spacer that pushes the badges to the right edge (see .dash-lrow-chips). */
.dash-lrow-desc {
  flex: 1 1 0; min-width: 0; font-size: 13px; color: var(--text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.dash-chat {
  background: var(--bg-detail); border-left: 1px solid var(--border-2);
  display: flex; flex-direction: column;
  position: relative;   /* the "New messages" jump anchors to the pane, not the thread */
}
/* Floats just above the composer, centred. Outside the thread on purpose: inside it, it would
   scroll away with the messages — which is exactly when you need it. */
.dash-jump {
  position: absolute; left: 50%; transform: translateX(-50%); z-index: 5;
  bottom: 4.2rem; cursor: pointer;
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .28rem .7rem; border-radius: 999px; font-size: 11.5px; font-weight: 600;
  background: var(--brand); color: var(--on-brand); border: 0;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .28);
}
.dash-jump[hidden] { display: none; }
.dash-jump:hover { filter: brightness(1.06); }
.dash-chat-head { padding: .8rem 1rem; border-bottom: 1px solid var(--line); flex: none; }
.dash-chat-label { font-size: 10.5px; letter-spacing: .6px; color: var(--text-muted); font-weight: 700; }
.dash-chat-empty { color: var(--text-muted); font-size: 12.5px; text-align: center; margin: auto; padding: 1rem; }

/* ═══ Dashboard discussion pane (2026-07-22, v1 backend piece ②) ══════════════════════════════
   Bubbles reuse the badge hue vars: `badge-c-<key>` on the <li> sets --badge-h/--badge-ch, which
   inherit to the bubble/avatar below — so all 25 palette colours work with no per-colour rule. */
.dash-chat-title { font-size: 12.5px; color: var(--text-3); margin-top: .2rem; }
/* min-height:0 lets the thread shrink so the compose box below it is never pushed out of the
   pane on a short window (flex children won't shrink past their content without it). */
.dash-thread {
  list-style: none; margin: 0; padding: .8rem 1rem;
  overflow-y: auto; flex: 1 1 auto; min-height: 0;
}
.dash-msg { display: flex; gap: .55rem; margin-bottom: .9rem; }
.dash-msg-avatar {
  width: 26px; height: 26px; border-radius: 50%; flex: none; display: grid; place-items: center;
  font-size: 11px; font-weight: 800; color: var(--on-brand);
  background: oklch(var(--badge-l) var(--badge-ch) var(--badge-h));
}
.dash-msg-main { min-width: 0; }
.dash-msg-meta {
  display: flex; align-items: center; gap: .4rem; flex-wrap: wrap;
  font-size: 11px; color: var(--text-muted); margin-bottom: .2rem;
}
.dash-msg-meta b { color: var(--text-2); font-size: 12px; }
.dash-msg-time { color: var(--text-muted); }
.dash-msg-edited { font-style: italic; }
.dash-msg-act { background: none; border: 0; color: var(--text-muted); cursor: pointer; font-size: 12px; padding: 0 .1rem; }
.dash-msg-act:hover { color: var(--text-2); }
.dash-msg-bubble {
  font-size: 13px; color: var(--text-2); line-height: 1.4;
  border-left: 3px solid oklch(var(--badge-l) var(--badge-ch) var(--badge-h));
  background: oklch(var(--badge-l) var(--badge-ch) var(--badge-h) / 0.09);
  border-radius: 0 8px 8px 0; padding: .4rem .6rem; word-wrap: break-word;
}
.dash-msg-empty { list-style: none; color: var(--text-muted); font-size: 12.5px; padding: .5rem; }
.dash-msg-editacts { display: flex; gap: .4rem; margin-top: .35rem; }
.dash-compose {
  display: flex; gap: .5rem; align-items: flex-end; padding: .7rem 1rem;
  border-top: 1px solid var(--line); flex: none;
}
/* The field wraps the textarea so the emoji toggle can sit INSIDE it (2026-08-02, QA point 13,
   the user's placement), and takes over the textarea's flex role in the row. */
.dash-compose-field { flex: 1 1 auto; position: relative; display: flex; min-width: 0; }
.dash-compose-input {
  flex: 1 1 auto; resize: vertical; min-height: 2.1rem; max-height: 8rem;
  /* room for the toggle, so typed text never runs underneath it */
  padding-right: 2rem;
  background: var(--bg-panel-2); border: 1px solid var(--border-2); border-radius: 8px;
  color: var(--text-2); padding: .45rem .6rem; font: inherit; font-size: 13px;
}
.dash-compose-send { flex: none; }

/* ── Emoji picker (2026-08-02, QA point 13) ──────────────────────────────────────────────
   A <details>, the house dropdown pattern — it opens with no JS at all, so the nonce-based CSP
   stays clean; app.js only inserts the character and closes the menu. */
.emoji-menu { position: absolute; right: .3rem; bottom: .3rem; }
.emoji-toggle {
  list-style: none; cursor: pointer; user-select: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.5rem; height: 1.5rem; border-radius: 6px;
  font-size: .95rem; line-height: 1; opacity: .55;
}
.emoji-toggle::-webkit-details-marker { display: none; }
.emoji-toggle:hover, .emoji-menu[open] .emoji-toggle { opacity: 1; background: var(--hover); }
/* Opens UPWARD: the composer sits at the bottom of the pane, so a panel below would fall off
   the screen. Right-aligned to its toggle for the same reason. */
/* ⚠️ Sized against the chat pane, which is a FIXED 400px grid column (.dash-panes), so these
   numbers are stable at every viewport width. Measured: anchored at `right: 0` the panel's right
   edge lands 307px into that pane, so 307px is the widest it can be before the LEFT edge escapes —
   and the pane is `overflow-y: auto`, which makes overflow-x compute to auto and CLIP it rather
   than let it spill. `right: -1rem` borrows the gap over the Send button (the panel floats above
   the compose row, so overhanging it is free), buying enough room for 10 columns with a
   comfortable margin on both sides. */
.emoji-panel {
  position: absolute; z-index: 60; bottom: calc(100% + 6px); right: -1rem;
  padding: .4rem; border-radius: 10px; width: 19rem;
  background: var(--bg-panel-2, var(--surface)); border: 1px solid var(--border-2, var(--line));
  box-shadow: 0 10px 30px rgba(0, 0, 0, .28);
}
.emoji-loading { display: block; padding: 1rem; font-size: .78rem; color: var(--text-muted); }

/* ── Tabs, with NO JavaScript (2026-08-02) ───────────────────────────────────────────────────
   Radio inputs + sibling selectors. The radios are hidden but must stay in the DOM and BEFORE
   both the tabbar and the pages for `~` to reach them.
   ⚠️ Matched by POSITION (nth-of-type), never by key, so EMOJI_GROUPS can be reordered or renamed
   without touching this — but the number of groups must not exceed the rules below. */
.emoji-tab-radio { position: absolute; opacity: 0; pointer-events: none; }
.emoji-tabbar {
  display: flex; gap: .1rem; margin-bottom: .35rem; padding-bottom: .35rem;
  border-bottom: 1px solid var(--line); overflow-x: auto;
}
.emoji-tab {
  flex: 0 0 auto; cursor: pointer; border-radius: 6px; padding: .15rem .25rem;
  font-size: .9rem; line-height: 1.4; opacity: .5;
}
.emoji-tab:hover { opacity: .85; background: var(--hover); }
.emoji-page { display: none; grid-template-columns: repeat(10, 1.6rem); gap: .1rem; }
.emoji-pages { max-height: 12.5rem; overflow-y: auto; }
.emoji-tab-radio:nth-of-type(1):checked ~ .emoji-pages > .emoji-page:nth-child(1),
.emoji-tab-radio:nth-of-type(2):checked ~ .emoji-pages > .emoji-page:nth-child(2),
.emoji-tab-radio:nth-of-type(3):checked ~ .emoji-pages > .emoji-page:nth-child(3),
.emoji-tab-radio:nth-of-type(4):checked ~ .emoji-pages > .emoji-page:nth-child(4),
.emoji-tab-radio:nth-of-type(5):checked ~ .emoji-pages > .emoji-page:nth-child(5),
.emoji-tab-radio:nth-of-type(6):checked ~ .emoji-pages > .emoji-page:nth-child(6),
.emoji-tab-radio:nth-of-type(7):checked ~ .emoji-pages > .emoji-page:nth-child(7),
.emoji-tab-radio:nth-of-type(8):checked ~ .emoji-pages > .emoji-page:nth-child(8),
.emoji-tab-radio:nth-of-type(9):checked ~ .emoji-pages > .emoji-page:nth-child(9),
.emoji-tab-radio:nth-of-type(10):checked ~ .emoji-pages > .emoji-page:nth-child(10),
.emoji-tab-radio:nth-of-type(11):checked ~ .emoji-pages > .emoji-page:nth-child(11),
.emoji-tab-radio:nth-of-type(12):checked ~ .emoji-pages > .emoji-page:nth-child(12) {
  display: grid;
}
.emoji-tab-radio:nth-of-type(1):checked ~ .emoji-tabbar > .emoji-tab:nth-child(1),
.emoji-tab-radio:nth-of-type(2):checked ~ .emoji-tabbar > .emoji-tab:nth-child(2),
.emoji-tab-radio:nth-of-type(3):checked ~ .emoji-tabbar > .emoji-tab:nth-child(3),
.emoji-tab-radio:nth-of-type(4):checked ~ .emoji-tabbar > .emoji-tab:nth-child(4),
.emoji-tab-radio:nth-of-type(5):checked ~ .emoji-tabbar > .emoji-tab:nth-child(5),
.emoji-tab-radio:nth-of-type(6):checked ~ .emoji-tabbar > .emoji-tab:nth-child(6),
.emoji-tab-radio:nth-of-type(7):checked ~ .emoji-tabbar > .emoji-tab:nth-child(7),
.emoji-tab-radio:nth-of-type(8):checked ~ .emoji-tabbar > .emoji-tab:nth-child(8),
.emoji-tab-radio:nth-of-type(9):checked ~ .emoji-tabbar > .emoji-tab:nth-child(9),
.emoji-tab-radio:nth-of-type(10):checked ~ .emoji-tabbar > .emoji-tab:nth-child(10),
.emoji-tab-radio:nth-of-type(11):checked ~ .emoji-tabbar > .emoji-tab:nth-child(11),
.emoji-tab-radio:nth-of-type(12):checked ~ .emoji-tabbar > .emoji-tab:nth-child(12) {
  opacity: 1; background: var(--hover);
}
.emoji-item {
  background: none; border: 0; padding: 0; cursor: pointer; border-radius: 5px;
  width: 1.6rem; height: 1.6rem; font-size: .95rem; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
}
.emoji-item:hover { background: var(--hover); }

/* In-dashboard navigation links (2026-07-20): section names, note titles and breadcrumb nodes
   in the middle pane load fragments via hx-get (no href), so they need an explicit pointer. */
.crumb-link { cursor: pointer; }
.crumb-root { color: var(--text-muted); }

@media (max-width: 640px) {
  .dashboard-link { display: none; }  /* dashboard is desktop-only (user decision, mockup v3) */
}

/* ── Note attachments (dashboard middle pane + the note Edit modal) ─────────────────────── */
/* A flex COLUMN since 2026-08-02 (the user's point 11 round 2): the tile grid below grows to
   fill whatever height is left, so the whole block reads as the drop zone it always was. */
.attachments {
  margin-top: 1.25rem; border-radius: .6rem; outline-offset: 4px;
  display: flex; flex-direction: column; min-height: 0;
}
/* In the dashboard pane it takes the leftover height — but `1 1 auto` with `min-height: 0`
   means a LONG note body wins: the body is sized by its content, this yields. That is the
   'adjust itself if we have a longer note text' half of the ask. The Edit modal has no such
   column, so it simply falls back to content height there. */
.dash-note .attachments { flex: 1 1 auto; min-height: 7rem; }
/* Focusable for paste-scoping: a subtle ring when focused so it's clear where a paste lands. */
.attachments:focus-visible { outline: 2px solid var(--accent); }
.attachments:focus { outline: 2px solid var(--accent); }
/* Drag-over highlight (the only affordance at rest — no permanent drop box). */
.attachments.att-dragover {
  outline: 2px dashed var(--accent); outline-offset: 4px; background: var(--hover);
}
.attachments.att-dragover * { pointer-events: none; }  /* let the drop land on the section */
.att-head {
  font-size: .72rem; letter-spacing: .08em; color: var(--text-3, var(--muted));
  margin: 0 0 .5rem;
}
.att-list {
  list-style: none; margin: 0;
  display: flex; flex-wrap: wrap; gap: .6rem;
  /* Grows into the space the section was given; `align-content: flex-start` keeps the tiles
     packed at the top instead of spreading down the empty area. */
  flex: 1 1 auto; align-content: flex-start; overflow-y: auto; min-height: 0;
  /* ⚠️ This padding is FUNCTIONAL, not decoration. The tiles' pencil and ✕ sit at top/right
     -.4rem — deliberately OUTSIDE the tile — and `overflow-y: auto` makes overflow-x compute to
     auto too, so this list is a CLIP BOX. With padding 0 the top row's buttons were cut off by a
     measured 6px. z-index cannot help: clipping is not a stacking question. The bar above gives
     back the .5rem in margin, so nothing moves. */
  padding: .5rem .5rem 0 0;
}
/* The 'Add files' row, now ABOVE the tiles (QA point 14, folded in here). */
.att-bar {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  /* .1rem + the list's .5rem top padding = the .6rem gap this used to have on its own. */
  margin: 0 0 .1rem; flex: none;
}
.att-bar .att-hint { margin: 0; }
.att-tile { position: relative; width: 7.5rem; }
/* A <button> since 2026-08-02 (it opens the viewer rather than navigating), so the browser's
   default button chrome has to be reset back to a plain tile. */
.att-tile-link {
  display: flex; flex-direction: column; gap: .3rem;
  text-decoration: none; color: inherit; cursor: pointer;
  background: none; border: 0; padding: 0; font: inherit; text-align: left; width: 100%;
}
.att-thumb {
  width: 7.5rem; height: 5.5rem; object-fit: cover;
  border-radius: .4rem; border: 1px solid var(--line);
}
.att-icon {
  width: 7.5rem; height: 5.5rem; display: grid; place-items: center;
  font-size: 2rem; border-radius: .4rem;
  background: var(--surface); border: 1px solid var(--line);
}
.att-name { font-size: .72rem; word-break: break-word; color: var(--text-2, var(--text)); }
/* The ✕ glyph is scaled up WITHOUT touching the 1.4rem button (2026-08-02, the user's ask): it
   was drawing 7.8px wide inside a 22.4px circle, which is a lot of empty button. `display: grid` +
   `place-items` keeps it optically centred as it grows — a larger glyph on the default inline
   layout drifts off the baseline instead. */
.att-del {
  position: absolute; top: -.4rem; right: -.4rem;
  width: 1.4rem; height: 1.4rem; border: 1px solid var(--line);
  border-radius: 50%; cursor: pointer; line-height: 1;
  display: grid; place-items: center; padding: 0;
  font-size: 1.15rem;
  background: var(--bg-card, var(--surface)); color: var(--text);
}
/* The pencil rides beside the ✕. It is UNGATED — any project member may rename (the user's
   QA point 11 ruling) — whereas .att-del only renders for those who may delete, so the two
   cannot be positioned as a fixed pair. */
.att-rename-open {
  position: absolute; top: -.4rem; right: 1.3rem;
  width: 1.4rem; height: 1.4rem; border: 1px solid var(--line);
  border-radius: 50%; cursor: pointer; line-height: 0;
  display: grid; place-items: center;
  background: var(--bg-card, var(--surface)); color: var(--text-muted);
}
.att-rename-open:hover { color: var(--text); }
/* Both tile actions appear ON HOVER only (2026-08-02, the user's point 11 round 2) — at rest a
   grid of files should read as files, not as a row of controls.
   ⚠️ `:focus-within` is not optional: these are the ONLY route to rename/delete, so hiding them
   from the keyboard would remove the feature for anyone tabbing. `visibility` rather than
   `display` keeps them focusable in the tab order at all times.
   ⚠️ And on a touch screen there is no hover at all, so they stay permanently visible there —
   the note Edit modal is reachable on a phone even though the dashboard is not. */
.att-tile .att-rename-open, .att-tile .att-del {
  opacity: 0; transition: opacity .12s ease;
}
.att-tile:hover .att-rename-open, .att-tile:hover .att-del,
.att-tile:focus-within .att-rename-open, .att-tile:focus-within .att-del {
  opacity: 1;
}
@media (hover: none) {
  .att-tile .att-rename-open, .att-tile .att-del { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .att-tile .att-rename-open, .att-tile .att-del { transition: none; }
}
/* The confirm dialog's body copy. */
.att-confirm-text { margin: 0; font-size: .9rem; color: var(--text-2, var(--text)); }
/* With no delete button there is no ✕ to sit left of, so the pencil takes its corner. */
.att-tile:not(:has(.att-del)) .att-rename-open { right: -.4rem; }
.att-rename { display: flex; flex-direction: column; gap: .35rem; }
.att-rename-input {
  width: 100%; font: inherit; font-size: .78rem; color: var(--text-2, var(--text));
  background: var(--bg-panel-2, var(--surface)); border: 1px solid var(--border-2, var(--line));
  border-radius: 6px; padding: .3rem .4rem;
}
.att-rename-actions { display: flex; gap: .3rem; }
.att-rename-actions .btn, .att-rename-actions .btn-ghost { font-size: .7rem; padding: .18rem .45rem; }

/* ── The viewer's contents ─────────────────────────────────────────────────────────────── */
.att-viewer-title {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.att-viewer-body {
  display: flex; align-items: center; justify-content: center;
  min-height: min(60vh, 420px); max-height: calc(100vh - 16rem); overflow: auto;
  background: var(--bg-detail, var(--surface)); border: 1px solid var(--line); border-radius: 10px;
}
/* Fits the box without ever exceeding its own natural size, so a small image is not blown up. */
.att-viewer-img { max-width: 100%; max-height: calc(100vh - 16rem); object-fit: contain; }
.att-viewer-frame { width: 100%; height: calc(100vh - 16rem); border: 0; }
/* An office document's derived preview (2026-08-02): the embedded thumbnail beside a short
   text excerpt. Stacks on a narrow dialog. */
.att-viewer-doc {
  display: flex; gap: 1.1rem; align-items: flex-start; flex-wrap: wrap;
  padding: 1.1rem; width: 100%; max-height: calc(100vh - 16rem); overflow-y: auto;
}
.att-viewer-doc-thumb {
  max-width: 260px; max-height: 340px; flex: 0 0 auto;
  border-radius: 8px; border: 1px solid var(--line);
}
.att-viewer-excerpt { flex: 1 1 20ch; min-width: 0; }
.att-viewer-excerpt-head {
  margin: 0 0 .4rem; font-size: 10.5px; letter-spacing: .6px; font-weight: 700;
  color: var(--text-muted); text-transform: uppercase;
}
/* <pre> keeps the extracted line breaks, but the excerpt is prose from a document, so it has
   to wrap rather than scroll sideways — and a long unbroken token must not widen the dialog. */
.att-viewer-excerpt-body {
  margin: 0; font: inherit; font-size: 12.5px; color: var(--text-2, var(--text));
  white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.5;
}
.att-viewer-none {
  display: flex; flex-direction: column; align-items: center; gap: .5rem;
  padding: 3rem 1rem; text-align: center; color: var(--text-3, var(--muted));
}
.att-viewer-none-icon { font-size: 2.6rem; opacity: .55; }
.att-viewer-none-title { font-size: .92rem; font-weight: 600; color: var(--text-2, var(--text)); }
.att-viewer-none-hint { font-size: .76rem; }
.att-viewer-meta { font-size: .78rem; color: var(--text-3, var(--muted)); }

.att-add { cursor: pointer; }
.att-hint { font-size: .72rem; color: var(--text-3, var(--muted)); margin: .4rem 0 0; }

/* Empty state: a big centered drop zone. In the dashboard pane the block grows to fill the
   remaining height (.dash-note is a full-height flex column); in the Edit modal it falls back to
   its min-height. */
.dash-note .attachments.attachments-empty { flex: 1 1 auto; min-height: 0; }
.attachments.attachments-empty { display: flex; flex-direction: column; }
.att-empty-zone {
  flex: 1 1 auto; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: .55rem;
  min-height: 200px; padding: 2rem 1rem; text-align: center; cursor: pointer;
  border: 1px dashed var(--line); border-radius: .6rem; color: var(--text-3, var(--muted));
}
.att-empty-zone:hover { background: var(--hover); }
.att-empty-icon { width: 52px; height: 52px; opacity: .5; }
.att-empty-title { font-size: .9rem; font-weight: 600; color: var(--text-2, var(--text)); }
.att-empty-hint { font-size: .74rem; color: var(--text-muted); max-width: 34ch; }
.note-clip { font-size: .78rem; color: var(--text-3, var(--muted)); margin-left: .4rem; }
/* Attachment-rejection dialog (#att-error) — reuses .modal-card; this is just its message list. */
#att-error .att-error-list {
  list-style: none; margin: 0 0 1rem; padding: 0;
  font-size: .88rem; color: var(--text-2, var(--text));
}
#att-error .att-error-list li {
  padding: .5rem .7rem; border-radius: .4rem;
  color: #ff9a9a; background: rgba(255,122,122,.12); border: 1px solid #ff7a7a;
}
#att-error .att-error-list li + li { margin-top: .4rem; }
