/* ═══════════════════════════════════════════════════════════════════
   STRUCTURE — layout, spacing and behaviour
   ═══════════════════════════════════════════════════════════════════
   RULES OF THIS FILE
   1. Every class styled here EXISTS in the real app. Verified
      mechanically against workbench/_real/ — if a selector here has
      no match there, that's a bug, not a new idea.
   2. Every class has exactly ONE definition. No rule may override
      another for a specific workspace. If you ever want to write
      "#invoiceBoardView .something", stop — that pattern is the
      disease this file exists to cure.
   3. NO raw colour values here. Everything resolves to a token in
      the theme file, so a theme can change any of it without ever
      touching this file.
   4. Every block is marked LOCKED or FREE.
        LOCKED — identical on every workspace, always.
        FREE   — the workspace supplies the value (colour, count,
                 contents); the shape stays fixed.
   ═══════════════════════════════════════════════════════════════════ */




* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

/* ── SHELL ──────────────────────────────────────────── LOCKED ── */
.app-shell { display: flex; height: 100%; overflow: hidden; }
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }


/* ═══ SIDEBAR ═══════════════════════════════════════ LOCKED ═══
   Identical on every workspace. Never varies, ever. */
.sidebar {
  width: 220px; flex-shrink: 0; position: relative;
  background: var(--sidebar-bg);
  display: flex; flex-direction: column;
  padding: 24px 0;
}
/* Rounded square with a chevron — sits top-right when open, and
   drops under the mark when collapsed so it never crowds the logo. */
/* Small, quiet, and optically centred. An SVG rather than a text
   glyph — "‹" carries its own side bearings and never sits centred
   in a box no matter how you align it. */
.sidebar-collapse-btn {
  /* 26px puts the button's centre at 38px — the same optical centre
     as the logo (24px sidebar padding + half the 28.5px mark). */
  position: absolute; top: 26px; right: 14px; z-index: 5;
  width: 24px; height: 24px; border-radius: 7px;
  border: none; cursor: pointer; padding: 0;
  background: var(--wash-light); color: var(--sidebar-text);
  display: flex; align-items: center; justify-content: center;
  transition: background 0.15s, color 0.15s, transform 0.18s ease;
}
.sidebar-collapse-btn svg { display: block; }
.sidebar-collapse-btn:hover { background: var(--wash-light-3); color: var(--sidebar-text-active); }
/* Collapsed: same button, mirrored — no second glyph to keep in sync. */
body.sidebar-mini .sidebar-collapse-btn { transform: rotate(180deg); }

/* ── COLLAPSED NAV ─────────────────────────────────── LOCKED ──
   body.sidebar-mini shrinks the nav to a 64px icon rail. The mini
   icons are already in the markup; everything with a label hides
   and the icon takes its place. Desktop only — on smaller screens
   the nav is a drawer, so there's nothing to collapse. */
@media (min-width: 1024px) {
  body.sidebar-mini .sidebar { width: 64px; }
  body.sidebar-mini .logo { padding: 0 0 20px; }
  body.sidebar-mini .logo-full { display: none; }
  body.sidebar-mini .logo-mini { display: block; }
  body.sidebar-mini .nav-link {
    font-size: 0; justify-content: center; gap: 0;
    display: flex; padding: 12px 0;
  }
  body.sidebar-mini .nav-mini-icon { opacity: 1; }
  body.sidebar-mini .nav-chevron,
  body.sidebar-mini .nav-sublist,
  body.sidebar-mini .nav-section-label,
  body.sidebar-mini .sidebar-tip,
  body.sidebar-mini .sidebar-user-name,
  body.sidebar-mini .theme-select,
  body.sidebar-mini .nav-badge { display: none; }
  /* Sign out survives the icon rail as a door-with-arrow icon — without
     it, signing out required expanding the sidebar first. */
  body.sidebar-mini .logout-btn { display: flex; justify-content: center; padding: 10px 0 0; width: 100%; }
  body.sidebar-mini .logout-btn .logout-txt { display: none; }
  body.sidebar-mini .logout-btn .logout-ic { display: inline-flex; }
  /* Shared-collections nav (guest sidebar) is all text — in the icon
     rail it wraps into a mess, so it hides entirely until expanded. */
  body.sidebar-mini #pdSharedNav { display: none; }
  body.sidebar-mini .sidebar-user { padding: 14px 0 0; }
  body.sidebar-mini .sidebar-user-row { justify-content: center; }
  /* Chevron moves below the mark, centred in the rail. The button
     comes FIRST in the markup, so flex order puts it after the logo
     without touching the DOM. */
  body.sidebar-mini .sidebar-collapse-btn {
    position: static; margin: 12px auto 4px; top: auto; right: auto;
    order: 2;
  }
  body.sidebar-mini .logo { order: 1; }
  body.sidebar-mini nav { order: 3; }
  body.sidebar-mini .sidebar-user { order: 4; }
  body.sidebar-mini .logo { padding: 4px 0 0; border-bottom: none; }
  body.sidebar-mini .logo-mini { width: 34px; }
}
/* smooth, and only the width — nothing reflows sideways */
.sidebar { transition: width 0.18s ease; }

.logo {
  color: var(--sidebar-text-active);
  padding: 0 20px 26px;
  /* no divider — the wordmark and the nav read as one block */
}
/* Sizing lives here, not inline — so the collapsed state can simply
   hide one and show the other, with no !important anywhere. */
.logo-full { display: block; width: 100%; max-width: 140px; height: auto; }
.logo-mini { display: none; width: 30px; height: auto; margin: 0 auto; }

.sidebar nav { padding: 16px 0; flex: 1; overflow-y: auto; }
.nav-link {
  display: flex; align-items: center; gap: 13px;
  padding: 11px 20px;
  color: var(--sidebar-text); text-decoration: none;
  font-size: 14.5px; cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
/* The icon is in the markup already — it belongs beside the label,
   not only when collapsed. (.nav-mini-icon is the app's name for it.) */
.nav-mini-icon { display: inline-flex; flex-shrink: 0; opacity: 0.9; }
.nav-link:hover { background: var(--sidebar-hover); color: var(--sidebar-text-active); }
.nav-link.active { background: var(--sidebar-hover); color: var(--sidebar-text-active); }

.nav-section-label {
  font-size: 10px; font-weight: 700; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--sidebar-text);
  padding: 20px 20px 8px; opacity: 0.65;
}
.nav-group { display: flex; flex-direction: column; }
.nav-row { display: flex; align-items: center; }
/* The chevron sits FIRST in the markup; order moves it to the right
   without touching the DOM. It points down while the list is open. */
.nav-row .nav-link { flex: 1; order: 1; min-width: 0; }
.nav-chevron {
  order: 2; flex-shrink: 0;
  background: none; border: none; cursor: pointer; padding: 0;
  width: 34px; align-self: stretch;
  display: flex; align-items: center; justify-content: center;
  color: var(--sidebar-text); opacity: 0.7;
  transform: rotate(90deg);
  transition: transform 0.18s ease, opacity 0.15s;
}
.nav-chevron[aria-expanded="false"] { transform: rotate(0deg); }
.nav-chevron:hover { color: var(--sidebar-text-active); }
.nav-sublist { display: flex; flex-direction: column; }
/* One row per board, nested under "Boards". Built by app JS. */
.nav-sublist-item {
  display: block; padding: 8px 20px 8px 22px;
  color: var(--sidebar-text); text-decoration: none;
  font-size: 13px; cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: background 0.15s, color 0.15s;
}
.nav-sublist-item:hover { background: var(--sidebar-hover); color: var(--sidebar-text-active); }
.nav-sublist-item.active { background: var(--sidebar-hover); color: var(--sidebar-text-active); }
/* Each board carries its own colour, so you can find one by eye. */
.nav-sublist-item { display: flex; align-items: center; gap: 10px; }
.nav-sublist-dot {
  width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
  background: var(--sidebar-text);
}
.nav-sublist-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.nav-badge {
  margin-left: auto; font-size: 10.5px; font-weight: 700;
  background: var(--danger); color: var(--on-accent);
  border-radius: 999px; padding: 1px 7px; min-width: 20px; text-align: center;
}

/* Dismissible hint card in the sidebar — LOCKED */
.sidebar-tip {
  margin: 10px 16px; padding: 10px 12px;
  background: var(--wash-light-2);
  border: 1px solid var(--sidebar-border);
  border-radius: var(--radius);
  display: flex; gap: 8px; align-items: flex-start;
  font-size: 11.5px; line-height: 1.45; color: var(--sidebar-text);
  position: relative;
}
.sidebar-tip-icon { flex-shrink: 0; opacity: 0.8; }
.sidebar-tip-text { flex: 1; min-width: 0; }
.sidebar-tip-close {
  background: none; border: none; cursor: pointer; padding: 0;
  color: var(--sidebar-text); opacity: 0.6; font-size: 13px; line-height: 1;
}
.sidebar-tip-close:hover { opacity: 1; color: var(--sidebar-text-active); }

/* Sidebar footer — avatar, name, theme picker, sign out. LOCKED */
.sidebar-user {
  padding: 14px 16px 0; margin-top: auto;
  border-top: 1px solid var(--sidebar-border);
  display: flex; flex-direction: column; gap: 8px;
}
.sidebar-user-row { display: flex; align-items: center; gap: 9px; cursor: pointer; }
.sidebar-avatar {
  width: 30px; height: 30px; border-radius: 50%; flex-shrink: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background: var(--sidebar-hover); color: var(--sidebar-text-active);
  font-size: 11px; font-weight: 700;
}
.sidebar-avatar img { width: 100%; height: 100%; object-fit: cover; }
.sidebar-user-name {
  font-size: 13px; color: var(--sidebar-text-active);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.theme-select {
  font: inherit; font-size: 12px; cursor: pointer;
  background: var(--wash-light-2); color: var(--sidebar-text);
  border: 1px solid var(--sidebar-border); border-radius: var(--radius);
  padding: 5px 8px;
}
.theme-select:hover { color: var(--sidebar-text-active); }
.logout-btn {
  font: inherit; font-size: 12px; cursor: pointer; text-align: left;
  background: none; border: none; color: var(--sidebar-text); padding: 4px 0 0;
}
/* Expanded sidebar keeps the plain text link; the door icon is the
   collapsed rail's. */
.logout-btn .logout-ic { display: none; }
.logout-btn .logout-ic svg { display: block; }
.logout-btn:hover { color: var(--sidebar-text-active); }


/* ═══ TOPBAR ════════════════════════════════════════ LOCKED ═══
   Height, padding, background and title treatment are locked for
   every workspace. FREE: which buttons sit in .board-members-bar,
   and whether a subtitle appears beside the title. */
.topbar {
  display: flex; align-items: center; gap: 14px;
  padding: 16px 24px; flex-shrink: 0;
  background: var(--bg); border-bottom: none;
}
.hamburger-btn {
  display: none; align-items: center; justify-content: center;
  width: 36px; height: 36px; flex-shrink: 0;
  background: none; border: none; border-radius: var(--radius);
  color: var(--ink); font-size: 20px; cursor: pointer;
}
.hamburger-btn:hover { background: var(--wash-ink); }

/* Arrow only — the word "Boards" added nothing an arrow doesn't say.
   Sized as a proper icon button so it has a real hit area instead of
   being a stray glyph. */
.back-btn {
  background: none; border: none; cursor: pointer;
  color: var(--ink-muted); font-family: inherit;
  font-size: 18px; line-height: 1;
  width: 30px; height: 30px; padding: 0; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius);
  transition: background 0.12s, color 0.12s;
}
.back-btn:hover { background: var(--wash-ink); color: var(--ink); }

/* One title rule for all three workspaces. .topbar-title is the plain
   span used by task/PD; .wp-inv-title is invoice's <h1>, which
   exists as its own element only because a subtitle sits beside it. */
.topbar-title, .wp-inv-title {
  font-family: var(--font-heading); font-weight: 400;
  font-size: 26px; line-height: 1.2; color: var(--ink);
}
.wp-inv-heading { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; min-width: 200px; }
.wp-inv-sub { font-size: 14px; color: var(--ink-label); }

.board-members-bar { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.board-member-stack { display: flex; align-items: center; }
/* Mobile overflow menu. Hidden on desktop, where the ghost buttons
   sit in the header directly. */
.topbar-more {
  display: none;
  background: var(--surface); border: 1px solid var(--hairline-strong);
  border-radius: var(--radius); cursor: pointer;
  width: 34px; height: 34px; padding: 0;
  color: var(--ink-muted); font-size: 16px; line-height: 1;
  align-items: center; justify-content: center;
}
.topbar-more:hover { border-color: var(--wash-ink-strong); color: var(--ink); }
/* Overlapping member avatars in the topbar. Same anatomy as the
   card avatars (.avatar-sm), one size up. */
.board-member-avatar {
  width: 30px; height: 30px; border-radius: 50%; flex-shrink: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700; color: var(--on-accent);
  border: 2px solid var(--bg); margin-left: -8px; cursor: pointer;
  transition: transform 0.15s;
}
.board-member-stack .board-member-avatar:first-child { margin-left: 0; }
.board-member-avatar:hover { transform: translateY(-2px); z-index: 1; }
.board-member-avatar img { width: 100%; height: 100%; object-fit: cover; }

.topbar-search {
  position: relative; display: flex; align-items: center; gap: 6px;
  background: var(--surface); border: 1px solid var(--hairline-strong);
  border-radius: var(--radius); padding: 5px 10px; color: var(--ink-icon);
}
.topbar-search input {
  border: none; background: none; outline: none;
  font-family: inherit; font-size: 12.5px; color: var(--ink);
  width: 130px; transition: width 0.18s ease;
}
.topbar-search input:focus { width: 220px; }
.search-results {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 3000;
  width: 340px; max-height: 60vh; overflow-y: auto; padding: 6px;
  background: var(--surface); border: 1px solid var(--hairline-strong);
  border-radius: var(--radius); box-shadow: var(--shadow-popup);
}

.notify-bell-btn {
  position: relative; background: none;
  border: 1px solid var(--hairline-strong); border-radius: 50%;
  width: 34px; height: 34px; padding: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-muted);
}
.notify-bell-btn:hover { background: var(--panel-footer); color: var(--ink); }
.notify-bell-dot {
  position: absolute; top: 6px; right: 6px;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--danger); border: 2px solid var(--bg);
}


/* ═══ BUTTONS ═══════════════════════════════════════ LOCKED ═══
   One button system. Every workspace uses these, no exceptions. */
.wp-btn {
  font: inherit; font-size: 12.5px; font-weight: 500; cursor: pointer;
  border-radius: var(--radius); padding: 9px 16px;
  border: 1px solid transparent; white-space: nowrap;
  transition: background 0.12s, border-color 0.12s;
}
.wp-btn-primary { background: var(--accent); color: var(--on-accent); box-shadow: var(--shadow-btn); }
.wp-btn-primary:hover { background: var(--accent-hover); }
.wp-btn-ghost {
  background: var(--surface); border-color: var(--hairline-strong);
  color: var(--ink-muted); padding: 7px 13px; font-size: 12px;
}
.wp-btn-ghost:hover { border-color: var(--wash-ink-strong); }
/* The PD tag filter: icon button, lit while any tag is isolated. */
#pdTagFilterBtn { padding: 7px 10px; }
#pdTagFilterBtn.is-on {
  background: var(--accent); color: var(--on-accent);
  border-color: var(--accent);
}
/* Tag pills on a product card, and the room's Tags cell. */
.pd-card-tags { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 12px 6px; }
.pd-card-tag {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 10.5px; line-height: 1; padding: 3px 7px; border-radius: 999px;
  background: var(--wash-ink, rgba(0,0,0,.06)); color: var(--ink-muted, #6b6a63);
  max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pd-card-tag-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; display: inline-block; }
.pd-room-tags { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
/* The PD "Submitted for review" filter, lit while active. */
#pdReviewFilterBtn.is-on {
  background: var(--accent); color: var(--on-accent);
  border-color: var(--accent);
}


/* ═══ COLUMNS ═══════════════════════════════════════ LOCKED ═══
   Page padding, gap and column shape are locked.
   FREE: how many columns, and each column's tint. */
.canvas {
  flex: 1; display: flex; flex-direction: row;
  gap: 16px;
  /* No top padding — the header already supplies 16px of breathing
     room below itself. 24px here on top of that read as a gap. */
  padding: 0 24px 24px;
  overflow-x: hidden; overflow-y: hidden; min-height: 0;
}
/* Invoice's columns are the SAME columns — modifier adds nothing. */

/* ═══ COLUMN ════════════════════════════════════════ LOCKED ═══
   VOCABULARY, because these two were used interchangeably and it made
   several comments in this file wrong:

     WORKSPACE  the template — Task, Invoices, Product Development.
                There are three, and they define the column shape.
     BOARD      a named instance of a workspace. "ARMR" is a board that
                is a Task workspace; "Grunk" is a board that is a Product
                Development workspace; "Fashion Invoices" and "Contract
                Work Invoices" are two boards of the Invoice workspace.

   Columns belong to the WORKSPACE. Boards differ in content, never in
   column shape — which is why nothing in this file is keyed to a board.

   Every column in every workspace is the same object: a header, a
   body, and optionally a footer. What differs is its ROLE, and a
   workspace is the same three roles every time:

     .col-project   what the workspace is ABOUT — the parent.
                    Task: Projects · Invoices: Clients · PD: Collections.
                    The only column with an eye, an add button, a footer
                    and groups.

     .col-task      its children, created here and not yet in flight.
                    Task: Tasks · Invoices: Draft · PD: Design.
                    Exactly one, always second. It is where the create
                    button lives, on every workspace.

     .col-active    in flight. Any number — Active1, Active2, Active3.
                    Task: Active · Invoices: Sent · PD: Tech Pack, Sample.

     .col-complete  finished. Exactly one, always LAST.
                    Task: Completed · Invoices: Paid · PD: Production.
                    A role of its own rather than "the last active
                    column", because it already behaves differently
                    everywhere: its cards dim, and PD collapses it.

   Write role rules, not position rules. ".col-project .column-footer"
   says why the footer is there; ".column:nth-child(1) .column-footer"
   says only where it is, and breaks the day a column is reordered or
   a fourth workspace has a different shape.

   The ONE exception is the tint below, which is genuinely positional —
   it is about the eye moving across the board, not about meaning. */
.column {
  flex: 1 1 0; min-width: 0; min-height: 0;
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-col);
}

/* ── WHAT EACH ROLE CARRIES ───────────────────── LOCKED ──
   These say out loud what the markup is allowed to contain, so a new
   board cannot quietly grow a second footer or put groups in a stage.
   The bodies behave identically in all three — that is deliberate and
   is why there is no per-role .column-body rule here. */

/* Only the project column has a footer, and Create Group lives in it. */
.col-task .column-footer,
.col-active .column-footer,
.col-complete .column-footer { display: none; }

/* Only the project column has groups. The panels are painted onto an
   underlay inside its body (see GROUPS below). */
.col-task .proj-underlay,
.col-active .proj-underlay,
.col-complete .proj-underlay { display: none; }


/* ── THE COMPLETE COLUMN ───────────────────────── LOCKED ──
   Finished work reads as finished, on every workspace, without the app
   having to remember to mark it.

   This used to be `.card.is-done, .card.is-paid` — two per-card state
   classes for one idea, and app.html emits NEITHER of them (0 matches).
   So the rule was correct and unreachable, and completed tasks were
   never actually dimmed. Keying off the COLUMN cannot fail that way:
   the column is always there, and a card dragged in or out changes
   appearance because of where it now is. */
/* SETTLED — the same recipe for a finished card and a hidden one, because
   they are the same idea: still here, out of play, not competing.

   Four properties, and the white background is load-bearing. An earlier
   attempt used a washed-back translucent surface, which let the column
   tint mix in — so "finished" rendered a different colour on every
   workspace and the card's look depended on what sat behind it. Another
   used an opaque grey, which reads as a different KIND of card rather
   than the same card at rest.

   The card stays white. `opacity` does the dimming, the flat shadow drops
   it out of the air, and the accent mutes with it. */
.col-complete .card,
.card.is-hidden {
  background: var(--surface);
  border-color: var(--hairline);
  opacity: 0.6;
  box-shadow: var(--shadow-card-flat);
  /* The accent is set inline per card, so mute it rather than replace it:
     mixed toward the card's own surface it desaturates and lightens
     together, which is what a muted variant of the colour looks like. */
  border-top-color: color-mix(in srgb, var(--card-accent, var(--card-accent-default)) 60%, var(--surface));
}
.col-complete .card:hover,
.card.is-hidden:hover {
  opacity: 0.85;
  box-shadow: var(--shadow-card-flat);
  transform: none;
}

/* Collapsible, on every workspace. Product Development already did this
   for its Production column via #pdColShipped.pd-collapsed — seven
   ID-scoped rules that no other workspace could reach. Same behaviour,
   keyed to the role, so Completed and Paid collapse too.

   And keyed to a TRAIT: .can-collapse, set by columns.json ("collapsible")
   on any non-owner column. Email folds Someday (in flight, parked) as
   well as Delete (terminal) — the first workspace with two rails. The
   terminal column still collapses without the trait, as before. */
.col-complete.is-collapsed,
.can-collapse.is-collapsed {
  flex: 0 0 46px; min-width: 46px; cursor: pointer;
}
.col-complete.is-collapsed .column-header,
.col-complete.is-collapsed .column-body,
.can-collapse.is-collapsed .column-header,
.can-collapse.is-collapsed .column-body { display: none; }
.col-complete.is-collapsed:hover,
.can-collapse.is-collapsed:hover { background: var(--wash-ink); }

/* The rail shown in its place: count above a vertical label. */
.col-complete .complete-rail,
.can-collapse .complete-rail { display: none; }
.col-complete.is-collapsed .complete-rail,
.can-collapse.is-collapsed .complete-rail {
  display: flex; flex-direction: column; align-items: center;
  gap: 10px; padding: 14px 0; height: 100%;
}
/* ── COLUMN TINTS ─────────────────────────────── LOCKED ──
   The app used to assign these with 11 ID selectors (#colProjects,
   #invColTasks, #pdColDesign …) — three workspaces spelling out the same
   five colours in the same order, eleven times.

   A tint is the ONE thing here that is genuinely positional. It is about
   the eye moving across the board, not about what a column means, so
   position is the right thing to key off — and it is the only place in
   this file where nth-child is the right answer rather than a smell.

   The five CYCLE. Written as plain nth-child(1..5) they stopped at five,
   and a sixth column fell through to bare --surface, visibly broken
   beside its neighbours — a stylesheet quietly asserting "at most five
   columns" while columns.json is free to hold more. 5n+1 means column 6
   takes tint 1, column 7 takes tint 2, and the rhythm continues for any
   width.

   Colours live in the theme files; this file only says where they go. */
.column:nth-child(5n+1) { background: var(--col-1); }
.column:nth-child(5n+2) { background: var(--col-2); }
.column:nth-child(5n+3) { background: var(--col-3); }
.column:nth-child(5n+4) { background: var(--col-4); }
.column:nth-child(5n+5) { background: var(--col-5); }

/* Layout only. Type lives on .column-title now that it's an element —
   before, the header had to set the font for everything inside it and
   each child opted back out. */
.column-header {
  display: flex; align-items: center; gap: 8px;
  flex-shrink: 0;
  border-bottom: 1px solid var(--hairline-strong);
  border-radius: var(--radius) var(--radius) 0 0;
  /* Fixed height, for the same reason the card footer has one: a header
     is a SLOT, and its height must not depend on which controls that
     particular column happens to carry. Projects and Tasks have an add
     button (and Projects an eye), so they came out 57px while Active and
     Completed — title only — were 49px. Now one value everywhere.
     Vertical padding is gone: the height and centring do that job, and
     with both you get to argue about which one wins.
     35px leaves 6.5px above and below the 22px icon buttons. */
  height: 35px; box-sizing: border-box;
  padding: 0 18px;
}
.col-header-icon { flex-shrink: 0; color: var(--ink-icon); }
.column-body {
  flex: 1; display: flex; flex-direction: column;
  gap: 8px; padding: 10px;
  overflow-y: auto; min-height: 0;
}

/* Small controls in a column header — LOCKED */
/* Both actions sit at the far right, eye first. The markup order is
   (+, eye) because the app floated them right, and float reverses —
   flex order restores the same result without the float. */
/* 22px, the same as every control in a card footer. One icon-button
   size across the board, so a header and a footer read as the same
   system rather than two that happen to sit near each other. */
.col-add-btn, .col-eye-btn {
  background: none; border: none; cursor: pointer;
  color: var(--ink-icon);
  width: 22px; height: 22px; padding: 0; flex-shrink: 0;
  border-radius: var(--radius); line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
}
.col-eye-btn { order: 2; }
.col-add-btn { order: 3; margin-right: -4px; }
/* The header value (invoice total, PD count) stays left of them. */
.column-value { order: 1; }
/* The title, finally an element of its own — so it can be sized,
   truncated, and ordered independently of the header's other parts. */
.column-title {
  font-size: 12.5px; font-weight: 800;
  letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--ink);            /* Deep Ink — the label leads */
  line-height: normal;
  /* The label never gives up space — it's what tells you which column
     you're in. When things get tight the VALUE truncates instead. */
  /* flex-grow so the title claims the free space: everything after it
     then pins right, whether the column has a value, an eye, an add
     button, or none of them. grow 1 / shrink 0 keeps the guarantee
     above — the label fills space but never gives any up. */
  flex: 1 0 auto; white-space: nowrap;
}
.col-add-btn { font-size: 16px; }
.col-eye-btn svg { width: 15px; height: 15px; }
.col-add-btn:hover, .col-eye-btn:hover { background: var(--wash-ink); color: var(--ink); }

/* Right-aligned value in a column header — FREE: invoice puts money
   totals here, PD a count, task nothing. Shape is locked. */
/* The value (invoice total, PD count) sits back from the label —
   14px, muted, normal weight. It reads as data, not as a heading. */
.column-value {
  /* Inert as long as every header has a .column-title — the title's
     flex-grow takes the free space first, since flex resolves lengths
     before distributing to auto margins. Kept as the fallback: when
     seven headers still carried a bare text node there was nothing to
     grow, and this was the only thing holding the total on the right
     edge. Removing it moved Sent and Paid 170px inward. */
  margin-left: auto;
  font-size: 14px; font-weight: 400;
  letter-spacing: 0; color: var(--ink-label);
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap;
}

/* Footer link at the bottom of a column — LOCKED */
.column-footer {
  display: flex; align-items: center; gap: 6px;
  padding: 11px 16px; flex-shrink: 0;
  border-top: 1px solid var(--hairline-strong);
  color: var(--ink-label); font-size: 12.5px;
  text-decoration: none; cursor: pointer;
}
.column-footer:hover { color: var(--ink); }
.plus { font-size: 15px; line-height: 1; }

/* PD's collapsed "Shipped" archive rail — FREE, PD-only feature.
   No other workspace has an archive column, so nothing to unify. */
.rail-label {
  writing-mode: vertical-rl; text-orientation: mixed;
  font-size: 11px; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-label);
}
.rail-count {
  font-size: 10.5px; font-weight: 700;
  min-width: 20px; height: 20px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--panel-footer); color: var(--ink-secondary);
}


/* ═══ CARD ══════════════════════════════════════════ LOCKED ═══
   Shape, border, shadow, padding and the accent bar are locked.
   FREE: the accent colour (--card-accent) and what goes inside. */
.card {
  position: relative; flex-shrink: 0;
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-top: 3px solid var(--card-accent, var(--card-accent-default));
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  cursor: pointer;
  transition: box-shadow 0.15s, transform 0.22s cubic-bezier(0.25,1,0.5,1), opacity 0.15s;
}
@media (hover: hover) {
  .card:hover { box-shadow: var(--shadow-card-hover); transform: translateY(-1px); }
}
/* ── THE THREE REGIONS ──
   Every card on every workspace is a header, a body and a footer, in
   that order, as SIBLINGS. Two of the three are locked; the middle one
   is the workspace's own.

     .card-header   LOCKED   the title row — the card's name, and the
                             ⋯ menu. Nothing else has ever belonged here.
     .card-body     FREE     whatever this workspace needs to say about
                             this card. PD puts its stage badges here.
                             Task and Invoices leave it out entirely.
     .card-footer   LOCKED   the parent, one line of detail, and the
                             controls that move the card.

   The names rotated to get here, and the rotation had to happen in one
   move: what is now .card-header was called .card-body — it holds the
   title and the menu, which is a header by any reading — while PD's
   middle region was .pd-card-badges, a name that could only ever
   describe one workspace's contents. Renaming either alone would have
   left two different meanings of .card-body live at the same time. */
.card-header {
  flex: none; padding: 12px 14px;
  display: flex; align-items: flex-start; justify-content: space-between;
}

/* The free middle. It has no look of its own on purpose — it is a
   region, not a component, and what a workspace puts in it brings its
   own styling. It supplies only the inset and the rhythm, so a second
   workspace filling it lands in the same place as the first. */
.card-body {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: 6px; padding: 0 14px 12px;
  font-size: 12.5px;
}
/* A card that leaves the body out — Task and Invoices today — must not
   pay for it. The header carried `flex: 1` when it was the only region
   above the footer; with a real sibling below it that would eat the
   body's space, so the growth belongs to the body when there is one. */
.card:not(:has(.card-body)) .card-header { flex: 1; }
.card-footer {
  display: flex; align-items: center; gap: 6px;
  padding: 5px 10px; flex-shrink: 0;
  border-top: 1px solid var(--hairline);
  font-size: 12px; color: var(--ink-label);
  /* Fixed height, because the footer is a SLOT and a slot's height must
     not depend on what happens to be in it. The Projects column carries
     an eye (22px) and an isolate toggle; Tasks, Active and Completed
     carry only move arrows (20px). That made column 1's footer 33px and
     the other three 31px, so cards never lined up across the board.
     33px = the shared 22px control height + 5px padding top and bottom
     + the 1px top border. */
  min-height: 33px; box-sizing: border-box;
}
/* The text side of the body. It was an anonymous <div style="flex:1;
   min-width:0"> in every renderer — layout written next to the markup,
   where no stylesheet and no theme can reach it. min-width:0 is what
   lets a long unbroken title ellipsis instead of pushing the ⋯ button
   off the card, so it is structural, not decoration. */
.card-main { flex: 1; min-width: 0; }

/* The move arrows, likewise. They sit hard right of whatever else the
   footer carries — an assignee, a pill, nothing at all — so the group
   claims the free space rather than each card guessing. */
.card-moves { margin-left: auto; display: flex; gap: 1px; }

/* A PARENT is a heading; a CHILD is content. The two were both Times
   19px, so a task read as important as the project holding it and a
   column of them read as a wall of headings. Keyed to the column's
   ROLE, so a fourth workspace inherits the distinction without knowing
   it exists. */
.col-task .card-title,
.col-active .card-title,
.col-complete .card-title {
  font-family: var(--font-body);
  font-size: 15px; letter-spacing: 0.01em;
}

.card-title {
  font-family: var(--font-heading); font-weight: 400;
  font-size: 19px; color: var(--ink); overflow-wrap: anywhere;
  /* Titles wrap to three lines on a task card. `normal` leaves the
     leading to the font, and Times sets it looser than the rest of the
     app — the one place in this sheet where a value was left to chance. */
  line-height: 1.25;
}

/* ── THE CHILD CARD'S CONSTANT ──
   Every card in a col-task / col-active / col-complete column is a
   CHILD of something in the project column. Two things are therefore
   always true of it, on every workspace:

     the body is a TITLE ROW    the child's own name, nothing else
     the footer NAMES ITS PARENT  the project · collection · client

   The parent used to sit in the body as a .card-desc under the title,
   which read as a description of the child rather than as the thing it
   belongs to — and it pushed the title up against the card's top edge
   while leaving the footer holding only arrows. The footer is where
   belonging goes: it is the card's edge, shared with the controls that
   move the card between its parent's stages.

   .card-parent and .card-assignee are the same object at different
   distances — both are quiet, truncating labels on the footer's left.
   The parent yields first when the footer is tight, because a card's
   own footer text (who has it, when it's due) is more urgent than a
   name the card's accent colour is already telling you. */
.card-parent,
.card-assignee {
  font-size: 11px; color: var(--ink-label);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.card-parent   { flex: 0 1 auto; max-width: 60%; }
.card-assignee { flex: 0 0 auto; max-width: 55%; }
/* ── THUMBNAIL ──
   First thing in the header, left of the title. Product Development
   had this as .pd-card-thumb, styled only in the legacy sheet with raw
   hex fallbacks; it is the same object on a task card, so it is one
   class now.

   `contain` rather than `cover`, with padding inside the border: these
   are product shots and flat artwork on white, and cropping a garment
   to fill a square is how you lose the neckline. The card is not a
   gallery — it is telling you which thing this is.

   A card with no image gets no thumbnail and no gap, which is what
   makes this optional without a setting anywhere. */
.card-thumb {
  width: 62px; height: 62px; flex: none;
  border-radius: var(--radius);
  object-fit: contain; padding: 3px;
  border: 1px solid var(--hairline-strong);
  background: var(--panel);
  box-sizing: border-box;
}

/* THE THUMBNAIL SPANS THE TITLE AND THE BODY.
   It used to sit inside .card-header, which made it part of the title
   ROW — so the body dropped below the whole header and started back at
   the card's left edge, under the picture rather than beside it. The
   thumbnail is not part of the title; it belongs to the card.

   As a direct child of .card it can span both rows. `1 / -2` means from
   the first row to the line before the last, so it reaches down past the
   body and stops above the footer — and it does that whether or not this
   workspace fills the body, without either case being special-cased.

   A card with no thumbnail keeps the plain column stack, so nothing pays
   for a grid it does not use. */
.card:has(> .card-thumb) {
  display: grid;
  grid-template-columns: auto 1fr;
  /* auto 1fr auto, not three autos. A thumbnail taller than the title and
     status line together leaves slack, and grid hands slack to the FIRST
     auto row — which put 67px between a 19px title and its status line,
     and made the header 86px tall to hold 19px of text. Naming the body
     row 1fr sends the slack there, so the title stays against the top. */
  grid-template-rows: auto 1fr auto;
  column-gap: 10px;
}
.card > .card-thumb {
  grid-column: 1; grid-row: 1 / -2;
  /* A plain square, top-aligned. Stretching it with a min-height made it
     SIZE the rows instead of sitting in them — the card grew to fit the
     picture rather than the picture fitting the card. */
  align-self: start; height: 62px;
  margin: 12px 0 12px 14px;
}
.card:has(> .card-thumb) > .card-header { grid-column: 2; padding-left: 0; }
.card:has(> .card-thumb) > .card-body   { grid-column: 2; padding-left: 0; align-self: start; }
.card:has(> .card-thumb) > .card-footer { grid-column: 1 / -1; }

/* ── THE STATUS ROW ──
   The card's state as a SENTENCE — "84 days late · due Sep 1" — rather
   than a row of pills. PD had five of them competing for the same
   glance, and the state is the only part that is ever urgent, so it is
   the only part that carries colour. The separator and the date stay
   quiet. Lives in .card-body, which is why that region is free.

   Tones are the SAME five meanings the pills use, so nothing has to be
   invented for a fourth workspace: late is bad, soon is warn, ok is
   good, done is settled. */
.card-state { font-weight: 600; }
.card-state.is-late { color: var(--tone-bad-ink); }
.card-state.is-soon { color: var(--tone-warn-ink); }
.card-state.is-ok   { color: var(--tone-good-ink); }
.card-state.is-done { color: var(--ink-label); }
.card-sep { color: var(--hairline-strong); }
.card-due { color: var(--ink-label); font-weight: 400; }

.card-desc {
  font-size: 12px; color: var(--ink-label);
  margin-top: 2px; overflow-wrap: anywhere;
}
.card-meta { display: flex; align-items: center; gap: 6px; margin-top: 10px; }

/* ── CARD STATES ──────────────────────────────── LOCKED ──
   Runtime conditions the app applies. Same meaning, same look, on
   every workspace — one rule each, no per-workspace variants. */

/* Unread activity — dark outline plus a dot on the title. */
.card.has-unread { border-color: var(--ink); box-shadow: 0 0 0 1.5px var(--ink); }
.card.has-unread .card-title::before {
  content: ''; display: inline-block;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--danger); margin-right: 8px;
  vertical-align: middle; position: relative; top: -1px;
}

/* Selected / isolated. The accent bar SURVIVES selection: `border-color`
   is a shorthand and was silently wiping border-top-color, so selecting a
   card erased the one thing that says which project or client it belongs
   to. Re-assert the top edge after the shorthand. */
.card.is-active {
  background: var(--accent-tint);
  border-color: var(--accent-tint-border);
  border-top-color: var(--card-accent, var(--card-accent-default));
}

/* .is-hidden shares the SETTLED recipe above with .col-complete .card —
   same meaning, so the same treatment rather than a second opacity value
   (it was 0.5 here against 0.72 there, for one idea). */

/* Finished cards are styled by their COLUMN — see THE COMPLETE COLUMN
   above. `.card.is-done` / `.card.is-paid` used to live here; app.html
   emits neither, so the rule never fired and completed cards were not
   actually dimmed on any workspace. */

/* Member avatars — LOCKED */
.avatar-stack { display: flex; }
.avatar-sm {
  width: 22px; height: 22px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 9px; font-weight: 700; color: var(--on-accent);
  border: 2px solid var(--surface); margin-left: -6px;
}
.avatar-stack .avatar-sm:first-child { margin-left: 0; }
.card-msg-count { font-size: 11px; color: var(--ink-label); margin-left: 4px; }

/* Card icon buttons — LOCKED. One size, one hover, everywhere. */
/* Every control in a card footer is the same height. They were three
   different ones — the eye 22px, the isolate toggle 13px, the move
   arrows 20px — sitting in a single row, which is why the footer's
   height changed from column to column and cards never lined up.
   Width still varies: the eye carries a count beside its icon. */
.card-eye, .card-iso, .card-menu-btn, .card-move-btn {
  background: none; border: none; cursor: pointer;
  color: var(--ink-icon); padding: 2px 4px; border-radius: 4px;
  font-family: inherit; font-size: 12px; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  height: 22px; box-sizing: border-box; flex-shrink: 0;
}
.card-menu-btn { font-size: 15px; flex-shrink: 0; margin-left: 8px; }
.card-eye svg { width: 16px; height: 16px; }
.card-eye:hover, .card-iso:hover, .card-menu-btn:hover,
.card-move-btn:hover:not(:disabled) { background: var(--wash-ink); color: var(--ink); }
.card-move-btn:disabled { opacity: 0.45; cursor: default; }
/* Directional modifiers carry no styling of their own — they exist
   so the app's JS can target each arrow. Listed so this file is a
   complete record of the card's real anatomy. */
.card-move-up, .card-move-down, .card-move-left, .card-move-right { }

/* ═══ STATUS PILL ═══════════════════════════════════ LOCKED ═══
   ONE pill. Three boards had three: .card-workflow-pill (Task),
   .inv-badge (Invoices) and .pd-badge (Product Dev) — same object,
   three shapes, and every tone a raw hex no theme could reach.

   The shape below is the invoice treatment, because that is the one
   that was reviewed and approved: uppercase, tracked, 700. It reads as
   a label rather than a word that happens to be in a box.

   Tones are named by MEANING, so a fourth workspace knows what to use
   without inventing a sixth colour:
     neutral  nothing has happened yet     draft · backlog · no status
     info     in flight, waiting on others sent · in review
     good     finished, nothing owed       paid · ok · done
     warn     needs attention, not late    revision requested · at risk
     bad      past due                     overdue · late
   FREE: the five tone pairs. LOCKED: the shape. */
.card-workflow-pill, .card-sched-pill, .inv-badge, .pd-badge {
  display: inline-block;
  font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.04em;
  padding: 3px 8px; border-radius: 999px;
  white-space: nowrap; flex-shrink: 0;
  background: var(--tone-neutral-bg); color: var(--tone-neutral-ink);
}

/* Each board's own state words, mapped onto the five tones. This block
   is the whole vocabulary — adding a state means adding it here, not
   inventing another colour next to the markup that needs it.

   The bare .inv-badge / .pd-badge default IS the neutral tone, so no
   modifier is needed for it. (.inv-badge.draft and .inv-badge.processing
   had rules in the legacy sheet but the app never emits either — it only
   ever sets paid, sent or overdue. verify.py caught them.) */

.card-workflow-pill.review,
.inv-badge.sent                                  { background: var(--tone-info-bg);    color: var(--tone-info-ink); }

.inv-badge.paid,
.pd-badge.ok                                     { background: var(--tone-good-bg);    color: var(--tone-good-ink); }

.card-workflow-pill.revision,
/* .inv-badge.warn had no entry — the tones were written per workspace as
   they came up, so an invoice badge could be neutral, info, good or bad
   but never warn. Nothing said so; it just rendered grey. The five tones
   are supposed to be workspace-agnostic. */
.inv-badge.warn,
.pd-badge.warn                                   { background: var(--tone-warn-bg);    color: var(--tone-warn-ink); }

/* A scheduled task is waiting, not late — until its day arrives and
   Active is still full, which is the one thing the user needs to see.
   It was a fourth pill shape (10px/600, its own radius, a raw #b45309)
   sitting in the same footer row as the workflow pill. Same object. */
.card-sched-pill.is-due                          { background: var(--tone-warn-bg);    color: var(--tone-warn-ink); }

.inv-badge.overdue,
.pd-badge.late,
.pd-badge.home.slip                              { background: var(--tone-bad-bg);     color: var(--tone-bad-ink); }

/* .home is an outline, not a fill — it states a date rather than a
   status, so it must not compete with the pills that do. */
.pd-badge.home { background: transparent; border: 1px solid var(--hairline-strong); font-weight: 600; }
.pd-badge.home.slip { background: transparent; border-color: var(--tone-bad-ink); }


/* ═══ RESPONSIVE ════════════════════════════════════ LOCKED ═══
   Three sizes, one set of rules for every workspace.

     phone      <=640px    one column per swipe
     tablet     641-1023   1.5 columns — the half tells you to swipe
     desktop    >=1024     every column visible, sidebar back

   iPad portrait is 768px wide, which used to land in the phone rule
   and gave one absurdly wide column. Tablet landscape is 1024px and
   lands on desktop, which is right. */

/* ── phone + tablet portrait: no sidebar, compact header, swipe ── */
@media (max-width: 1023px) {
  /* Hidden until the hamburger opens it — display:none alone made the
     hamburger a no-op (it toggled a class on an element that could
     never render). Off-canvas panel, backdrop behind it. */
  .sidebar { display: none; }
  .sidebar.sidebar-open {
    display: flex; position: fixed; top: 0; left: 0;
    height: 100dvh; width: 240px; z-index: 1000;
    box-shadow: 0 0 40px rgba(0,0,0,0.35);
  }
  .hamburger-btn { display: flex; }
  /* One row, always. This header was taking 204px — a quarter of a
     phone screen — because it wrapped. Keep what's actually needed:
     the menu, where you are, and the one action. */
  .topbar { padding: 10px 14px; gap: 10px; flex-wrap: nowrap; }
  .topbar-title, .wp-inv-title {
    font-size: 19px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
  }
  .topbar-search,
  /* .back-btn stays. It was in this list, on the reasoning that a phone
     topbar should carry the menu, where you are, and one action — and
     that was wrong. The hamburger OPENS THE SIDEBAR; it does not leave
     the board. Hiding the back arrow left a phone with no way out of a
     board except opening the sidebar and finding Boards, which is not
     what anyone reaches for. A regression I introduced; the app never
     hid it before. */
  .board-member-stack,
  .notify-bell-btn,
  .wp-inv-sub,
  .wp-btn-ghost { display: none; }
  /* …except the review filter — it's a view control, not desktop chrome. */
  #pdReviewFilterBtn { display: inline-flex; padding: 6px 10px; font-size: 11px; }
  /* the ghost actions aren't dropped — they move in here */
  .topbar-more { display: flex; }
  /* 24px of glyph is not a tap target. 44px is the smallest thing a
     thumb hits reliably, and this is now the only way off the board. */
  .back-btn { display: inline-flex; align-items: center; justify-content: center;
              width: 44px; height: 44px; margin: 0 -6px 0 -8px; flex: none;
              font-size: 22px; }
  /* Let the title give up space so the action button is never clipped.
     .wp-inv-heading's 200px floor is a desktop rule; here it pushed
     the blue button off screen. */
  .wp-inv-heading { min-width: 0; flex: 1; }
  .topbar-title { flex: 1; }
  .topbar > * { min-width: 0; }
  .board-members-bar { flex-shrink: 0; margin-left: auto; }
  .wp-btn-primary { padding: 8px 12px; font-size: 12px; }

  .canvas {
    /* no top padding — the header already gives breathing room */
    overflow-x: auto; scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
    align-items: stretch;
  }
  .canvas::-webkit-scrollbar { display: none; }
  .column { scroll-snap-align: start; }
}

/* ── phone: one column fills the screen ── */
@media (max-width: 640px) {
  .canvas { padding: 0 12px 12px; gap: 10px; }
  .column { flex: 0 0 calc(100vw - 24px); }
  .column-header { padding: 16px 18px; font-size: 13px; }
}

/* ── tablet portrait: 1.5 columns ──
   A full column plus half of the next. The half is the point: it
   shows there's more to swipe to, and stops one column sprawling
   across the whole iPad. Width solves 1c + gap + 0.5c = available. */
@media (min-width: 641px) and (max-width: 1023px) {
  .canvas { padding: 0 16px 16px; gap: 12px; }
  .column { flex: 0 0 calc((100vw - 44px) / 1.5); }
  .topbar { padding: 12px 16px; }
  .topbar-title, .wp-inv-title { font-size: 24px; }
}


/* ═══ MODALS ════════════════════════════════════════ LOCKED ═══
   The invoice modals are the REFERENCE, not the thing to change.
   That shell was designed deliberately — 6px radius, panel ground,
   surface-coloured header, soft border — and everything else adopts
   it. Task and PD previously had three different shells: 12px radius
   (.modal/.task-modal), 16px hardcoded (.pd-room), and a fourth for
   small popups. Now there is one.

   Values below are lifted verbatim from the invoice editor
   (_legacy.css: #invOverlay .inv-modal:has(.wpi-editor), .wpi-head,
   .invc-close). Nothing here is a new idea. */

/* ── The backdrop ── */
.inv-overlay, .modal-overlay, .task-overlay, .pd-room-overlay, .cb-overlay {
  position: fixed; inset: 0; z-index: 3000;
  /* .cb-overlay is raised below — it is always the SECOND sheet, opened
     on top of one of the others. */
  background: var(--overlay);
  display: none;                       /* the app toggles this */
  align-items: center; justify-content: center;
  padding: 24px 16px; overflow-y: auto;
}

/* ── The shell — one shape for every modal in the app ── */
.inv-modal, .modal, .task-modal, .pd-room, .cb-popup {
  background: var(--panel);
  border: 1px solid var(--hairline);
  /* The top edge carries the project's colour, same mechanism the
     cards use. 3px, matching them — the app currently hardcodes 4px
     inline at five sites (see inlineAccents in renames.json). */
  border-top: 3px solid var(--card-accent, var(--hairline));
  border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
  display: flex; flex-direction: column;
  overflow: hidden; position: relative;
  width: 100%; margin: auto;
}
/* Only the width differs, because the contents differ. */
.inv-modal    { max-width: 860px; }
/* 1000px, from the Warm Paper block at _legacy.css:2731 — NOT the 620px
   at _legacy.css:2560, which is the stale pre-Warm-Paper line that the
   Warm Paper rule already overrides. Copying the first match instead of
   the winning one shrank the reference modal by 380px. */
.invc-modal   { max-width: 1000px; }
.modal        { max-width: 1041px; height: 88vh; }
.task-modal   { max-width: 1041px; height: 88vh; }
/* 1040, not 760. The timeline is a fixed 520px, so at 760 it left the
   title about 200px and the two-column body could not hold a 272px
   sidebar beside a readable thread. */
.pd-room      { max-width: 1040px; height: min(860px, calc(100vh - 40px)); }
.cb-popup     { max-width: 320px; }

/* ── The header — surface ground, rule beneath, generous padding ── */
.invc-head, .wpi-head, .modal-title-row, .cb-header {
  flex: none; position: relative;
  width: 100%; box-sizing: border-box;
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
  padding: 22px 24px 20px;
  display: flex; align-items: flex-start; gap: 20px;
}
.cb-header { padding: 16px 18px 14px; align-items: center; }

/* The project modal's header was pinned to HALF the modal width:
   _legacy.css has a literal `.modal .modal-title-row { width: 50% }`,
   left over from a two-column title area. Since .modal-close is
   absolutely positioned to the header's right edge, the X landed in the
   MIDDLE of a 1041px modal.

   The legacy selector is (0,2,0) and beats the shared rule's (0,1,0)
   regardless of load order, so matching its specificity is the fix —
   not reordering. The app already overrides this to 100% inside a
   mobile media query, with a comment saying the row should fill the
   modal, so desktop was simply the branch nobody revisited. */
.modal .modal-title-row,
.task-modal .modal-title-row { width: 100%; box-sizing: border-box; }

/* ── WHAT OPENS ON TOP OF A MODAL ──
   The legacy sheet had a careful ladder: modals at 100-300, and
   everything designed to open FROM INSIDE one above them at 300-700 —
   the emoji picker, the attach menu, confirmations, dropdowns, the
   lightbox, reaction and tag pickers.

   Unifying the four modal backdrops onto the invoice modal's values
   took its z-index of 3000 with them, and that jumped all four ABOVE
   that entire band. The invoice modal could afford 3000 because nothing
   opens inside it. The others cannot: every popup a person can reach
   from within a modal started rendering behind it.

   It surfaced one symptom at a time — "Submit for review does nothing",
   then "the emoji popup is behind" — because each is a separate rule
   and you only find one by trying it. So the ladder is restated here in
   full rather than patched per-report.

   3000  the modal backdrops
   3100  a sheet opened from a modal (the submit sheet, settings popups)
   3200  a popup opened from inside either of those

   Legacy's relative order inside each band is preserved; only the band
   moved. */
.cb-overlay,
/* The invoice editor's own pickers. They are .inv-overlay like the
   editor itself, so at a shared 3000 which one wins came down to DOM
   order. Same family of bug, not yet reported — raised before it is. */
#invTaskPickOverlay, #invItemPickOverlay, #invClientOverlay,
#invNewOverlay, #invSendOverlay,
.qct-overlay { z-index: 3100; }

/* .pd-sheet-overlay was in this list, copied from legacy's z-index
   ladder — the app emits it ZERO times. The guard caught it, which is
   the same rule the design brief gives Claude Design: do not style
   something the app cannot produce. */
.emoji-picker, .attach-menu, .members-overlay,
.conf-overlay, .tcreate-overlay, .pd-img-hover-menu,
.card-dropdown, .board-dropdown, .ac-dropdown,
.reaction-picker, .dm-react-picker, .tag-picker,
.profile-overlay, .members-panel-overlay, .info-popup-overlay,
.lightbox { z-index: 3200; }

.invc-head-left, .wpi-head-left  { flex: 1; min-width: 0; }
.invc-head-right, .wpi-head-right { flex: none; text-align: right; }
.invc-head-label, .wpi-head-label {
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink-label); margin-bottom: 5px;
}
.invc-head-name, .wpi-head-name, .cb-title {
  font-family: var(--font-heading); font-weight: 400;
  font-size: 22px; line-height: 1.2; color: var(--ink);
  overflow-wrap: anywhere;
}
.cb-title { font-size: 16px; }
.invc-head-meta { font-size: 12.5px; color: var(--ink-label); margin-top: 4px; }
.invc-head-balance, .wpi-head-total {
  font-family: var(--font-heading); font-weight: 400;
  font-size: 22px; color: var(--ink); white-space: nowrap;
}

/* ── The close button — the invoice one, everywhere ── */
.invc-close, .modal-close, .cb-close, .inv-x {
  position: absolute; top: 18px; right: 18px;
  width: 28px; height: 28px; padding: 0;
  border: none; background: none; cursor: pointer;
  border-radius: var(--radius);
  color: var(--ink-icon); font-size: 15px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.12s, color 0.12s;
}
.invc-close:hover, .modal-close:hover, .cb-close:hover, .inv-x:hover {
  background: var(--wash-ink); color: var(--ink);
}

/* ── Bodies and footers ── */
/* .invc-body is deliberately NOT in this list. It is a three-track grid
   — minmax(0,1fr) / 1px / minmax(0,1fr) — whose .invc-pane children pad
   themselves and whose middle track IS the divider. Padding the grid
   itself insets the divider so it no longer runs the full height, and
   double-pads the panes. */
.invc-body {
  flex: 1; min-height: 0; overflow-y: auto;
}
.wpi-body, .inv-body, .modal-two-col {
  flex: 1; min-height: 0; overflow-y: auto; padding: 20px 24px;
}
.wpi-editor { display: flex; flex-direction: column; min-height: 0; flex: 1; }
.wpi-foot, .modal-actions {
  flex: none; display: flex; align-items: center; gap: 8px;
  justify-content: flex-end;
  padding: 14px 24px;
  border-top: 1px solid var(--rule);
  background: var(--panel-footer);
}
.modal-title-row .modal-breadcrumb { flex: 1; min-width: 0; font-size: 12.5px; color: var(--ink-label); }
.modal-back, .modal-add-task-btn {
  background: none; border: none; cursor: pointer;
  color: var(--ink-icon); padding: 4px 8px; border-radius: var(--radius);
  font-family: inherit; font-size: 12.5px;
}
.modal-back:hover, .modal-add-task-btn:hover { background: var(--wash-ink); color: var(--ink); }
.modal-left-top, .modal-members, .discussion, .tasks-section { min-width: 0; }

/* ── Modal buttons — the app's own button system ── */
.btn {
  font: inherit; font-size: 12.5px; font-weight: 500; cursor: pointer;
  border-radius: var(--radius); padding: 9px 16px;
  border: 1px solid transparent; white-space: nowrap;
}
.btn-submit { background: var(--accent); color: var(--on-accent); box-shadow: var(--shadow-btn); }
.btn-submit:hover { background: var(--accent-hover); }
.btn-cancel { background: var(--surface); border-color: var(--hairline-strong); color: var(--ink-muted); }
.btn-cancel:hover { border-color: var(--wash-ink-strong); }


/* ═══ PROJECT GROUPS ════════════════════════════════ LOCKED ═══
   Folders in the Projects column, on all three workspaces.

   ARCHITECTURE — do not change this without reading it.
   Groups are FLAT. The header, its member cards, the "+ Add project"
   row and an invisible end-marker are all direct SIBLINGS inside
   .column-body. Nothing nests. That's deliberate: the drag engine
   reorders exactly one list, so a card can be dropped into a group,
   out of it, between panels, or at the top of the column with no
   special cases.

   The panel behind a group is NOT a container. It is a rectangle
   painted onto .proj-underlay by _paintGroupPanels(), measured from
   the header's offsetTop down to the end marker. The drag engine never
   touches it, which is why dragging stays simple.

   Turning this into a nested div would look identical and break
   dragging. It has been tried. */

/* The underlay sits behind everything and takes no clicks. */
.proj-underlay {
  position: absolute; inset: 0;
  pointer-events: none; z-index: 0;
}
.proj-underlay .grp-rect {
  position: absolute;
  background: var(--group-panel);
  border: 1px solid var(--group-panel-border);
  border-radius: 10px;
}
/* Blocks sit above the painted panel. */
.column-body > *:not(.proj-underlay) { position: relative; z-index: 1; }

/* ── The group header row ── */
.projects-group-header {
  position: relative; width: 100%;
  display: flex; align-items: center; gap: 6px;
  text-align: left; background: none; border: none; cursor: pointer;
  font-family: inherit; font-size: 11px; font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-muted);
  padding: 8px 10px 6px; margin-top: 14px;
  overflow: hidden;
}
.projects-group-header:hover { color: var(--ink); }
/* Collapsed, a group is a standalone pill and needs even padding. */
.projects-group-header[data-collapsed="1"] {
  padding-top: 14px; padding-bottom: 14px; margin-bottom: 16px;
}

.grp-drag-handle {
  cursor: grab; flex-shrink: 0;
  color: var(--ink-label); font-size: 12px; line-height: 1;
  padding: 0 2px; user-select: none; touch-action: none;
}
.grp-drag-handle:active { cursor: grabbing; }
.chev { font-size: 9px; flex-shrink: 0; }
/* Name yields first; the count and controls hold their width. */
.grp-name {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 40px; flex-shrink: 1;
}
.grp-count { font-weight: 600; color: var(--ink-label); flex-shrink: 0; }

.grp-eye, .grp-iso, .grp-menu {
  background: none; border: none; cursor: pointer;
  color: var(--ink-icon); padding: 2px 4px; flex-shrink: 0;
  border-radius: 4px; line-height: 1;
  display: inline-flex; align-items: center;
}
.grp-eye { margin-left: auto; }
.grp-menu { font-size: 15px; }
.grp-eye svg, .grp-iso svg { width: 15px; height: 15px; }
.grp-eye:hover, .grp-iso:hover, .grp-menu:hover { background: var(--wash-ink); color: var(--ink); }
/* Toggled on — same accent both controls use elsewhere. */
.grp-eye.is-hidden, .grp-iso.is-active { color: var(--accent); opacity: 1; }
.grp-menu-disabled { opacity: 0.35; cursor: default; pointer-events: none; }

/* Member cards sit inset on the painted panel. */
.card.grp-member { margin: 0 8px 2px; }

/* Per-group "+ Add project" — also the group's drag boundary: dropping
   BELOW it files the card outside the group, above it files it last. */
.group-add-row {
  position: relative;
  display: flex; align-items: center; gap: 6px;
  padding: 0 10px 6px; font-size: 12px;
  color: var(--ink-muted); cursor: pointer;
  /* The panel is painted ~4px past this row, so without extra room the
     next block sits tighter below a panel than above one. */
  margin-bottom: 16px;
}
.group-add-row:hover { color: var(--accent); }


/* ═══ CARD MENU ═════════════════════════════════════ LOCKED ═══
   The three-dot popover on a project card. One implementation serves
   the task board and Product Development — see buildCardColourPicker
   in app.html; they previously had identical colour grids written out
   twice.

   Every colour here is a token. The SWATCH colours are the exception
   and are set inline, because they are data: the user picks one and it
   is stored on the project. */
.card-dropdown {
  /* FIXED, not absolute. _placeCardDropdown positions this from
     getBoundingClientRect(), which is viewport-relative — with `absolute`
     the same numbers are read as document-relative and the menu lands in
     the wrong place the moment the page is scrolled. The legacy rule was
     `position: fixed` and it was right. */
  /* z-index lives in the layer ladder above, not here. It was 500 in
     this rule, which sits LATER in this same file than the ladder — so
     this one quietly won and the factory menu still opened behind the
     Product Room, even after the inline 340 was removed. Two rules for
     one property in one file is how that hides. */
  position: fixed;
  width: 224px; padding: 6px;
  background: var(--surface);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-popup);
  color: var(--ink);
}

/* Section label — same type as a column title, one step smaller. */
.card-menu__label {
  padding: 6px 8px 4px;
  font-size: 10.5px; font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-label);
}

/* Swatch grid. The last swatch is "no colour", so clearing a colour is
   picking one rather than a separate action below the grid. */
.card-menu__swatches {
  display: grid; grid-template-columns: repeat(6, 1fr); gap: 4px;
  padding: 2px 6px 8px;
}
.swatch {
  width: 22px; height: 22px; padding: 0; border: 0; border-radius: var(--radius);
  background: none; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.swatch:hover { background: var(--wash-ink); }
.swatch.is-selected { box-shadow: inset 0 0 0 1px var(--accent); }
.swatch i { width: 12px; height: 12px; border-radius: 50%; display: block; }
.swatch--none i { border: 1px dashed var(--ink-icon); }

/* Hex field — mono, because a hex is a value to read character by
   character rather than a word. */
.card-menu__hex {
  display: flex; align-items: center; gap: 6px;
  margin: 0 6px 8px; height: 28px; padding: 0 4px 0 8px;
  background: var(--panel);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
}
.card-menu__hex:focus-within { border-color: var(--accent); }
.card-menu__hex-dot { flex: none; width: 12px; height: 12px; border-radius: 50%; }
.card-menu__hex-dot.is-none { border: 1px dashed var(--ink-icon); }
.card-menu__hex-input {
  flex: 1; min-width: 0; border: 0; background: none; outline: 0; padding: 0;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 12px; letter-spacing: 0.02em; color: var(--ink);
}
.card-menu__pipette {
  position: relative; width: 22px; height: 22px; border-radius: var(--radius);
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-icon); cursor: pointer;
}
.card-menu__pipette:hover { background: var(--wash-ink); color: var(--ink); }
/* The native colour input covers the icon and is invisible — the icon is
   the affordance, the input is the mechanism. */
.card-menu__pipette input[type="color"] {
  position: absolute; inset: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer;
}

/* Items */
.card-dropdown-divider {
  height: 1px; border: 0; background: var(--hairline); margin: 6px 0;
}
.card-dropdown-action {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 7px 8px; border: 0; border-radius: var(--radius); background: none;
  font: inherit; font-size: 13px; color: inherit; text-align: left; cursor: pointer;
}
.card-dropdown-action:hover { background: var(--wash-ink); }
.card-dropdown-action.is-danger, .card-dropdown-del.is-danger { color: var(--danger); }
.card-dropdown-action.is-danger:hover,
.card-dropdown-del.is-danger:hover { background: var(--tone-bad-bg); }

/* The trigger, while its menu is open. */
.card-menu-btn.is-open { background: var(--wash-ink); color: var(--ink); }
/* The card lifts to its hover shadow, and stops clipping the popover. */
.card.has-menu-open { box-shadow: var(--shadow-card-hover); overflow: visible; }


/* ═══ PROJECT MODAL INTERIOR ════════════════════════ LOCKED ═══
   Applies the app's existing "white sheet on panel ground" relationship
   inside the project modal — the same relationship cards already have
   with columns. Layout and regions are unchanged; this only adds
   surfaces, edges and spacing.

   Scoped to .modal and .task-modal — the two project modals (the task
   board's #modal and Product Development's inside #pdProjOverlay) and
   the task modal. All three are built from the SAME class vocabulary:
   .modal-title-row, .modal-two-col, .modal-left-top, .modal-members,
   .discussion, .disc-tabs, .disc-panel, .messages, .message-composer.
   The only reason the task modal rendered flat white was that its shell
   carries .task-modal rather than .modal, so none of this reached it —
   not because it was built differently. One rule set serves all three.

   PD keeps its wider tasks column: _legacy.css:2207 sets 330px at
   (1,1,0), which outranks the 258px below. The task modal has no tasks
   pane, and _legacy.css:3128 makes its grid a single column at (0,2,0),
   which outranks the two-column rule below — so it stays one column and
   the sheets simply run full width.

   SHEETS are what you read or author. GROUND is the chrome framing them.

     sheet    identity (title · date · description · members)
              message thread
              composer
              tasks pane
     ground   modal shell · breadcrumb bar · gutters · tab strip

   Four sheets. Members sits inside the identity sheet; individual
   messages and tasks are transparent rows within their sheet — never a
   sheet inside a sheet.

   ONE DEVIATION from the spec, deliberate: it puts the breadcrumb bar on
   ground. Here it stays --surface, because .modal-title-row shares its
   rule with .invc-head — the invoice client window, which is signed off
   and must not move. Divergence there would cost more than it buys. */

/* The body is a GRID, not a stack — .discussion and .tasks-section sit
   side by side (_legacy.css:1873, with .modal adding a third row at
   :3088). Overriding it with flex-column collapsed the thread to 14px
   and pushed Tasks to full width, so the grid is restated here rather
   than replaced.

   row-gap is 0 so the two halves of the identity sheet TOUCH and read as
   one sheet; the separation below it is a margin on the lower half. */
/* DESKTOP ONLY. _legacy.css:5712 stacks this into a flex column under
   768px, and a media query adds no specificity — so an unscoped grid
   here loads later and wins on a phone too, which put a 258px tasks
   pane beside a ~100px thread on a 375px screen. */
@media (min-width: 769px) {
  .modal-two-col {
    display: grid;
    grid-template-columns: 1fr 258px;
    grid-template-rows: auto auto 1fr;
    column-gap: 16px; row-gap: 0;
    flex: 1; min-height: 0; overflow: hidden;
  }
  .modal .modal-left-top,
  .modal .modal-members { grid-column: 1 / -1; }
  .modal .modal-members,
  .task-modal .modal-members { margin-bottom: 16px; }
}

/* On a phone the same four sheets stack, with the thread taking the
   space left over. */
@media (max-width: 768px) {
  .modal .modal-members { margin-bottom: 12px; }
  .modal .discussion { flex: 1; min-height: 0; }
  .modal .tasks-section { flex: none; max-height: 45vh; }
}

/* ── Identity sheet ──
   .modal-left-top and .modal-members are SIBLINGS in the markup, not one
   wrapper, so the sheet is drawn in two halves that meet: the top loses
   its bottom corners, the bottom loses its top ones and its top border
   becomes the divider between them. No markup change needed. */
.modal .modal-left-top,
.modal .modal-members,
.task-modal .modal-left-top,
.task-modal .modal-members {
  background: var(--surface);
  border: 1px solid var(--hairline);
  box-shadow: var(--shadow-sheet);
  padding: 17px 22px 18px;
}
.modal .modal-left-top,
.task-modal .modal-left-top {
  border-radius: var(--radius) var(--radius) 0 0;
  border-bottom: 0;
}
.modal .modal-members,
.task-modal .modal-members {
  border-radius: 0 0 var(--radius) var(--radius);
  padding: 13px 22px 15px;
  /* The task modal's lower half carried `border-bottom: 1px solid
     var(--border-light)` (_legacy.css:3163) from when this was a flat
     stack of bands and every band needed an edge. On a sheet that line
     lands 1px inside the bottom border and reads as a seam. */
  border-bottom: 1px solid var(--hairline);
}

/* The task modal's identity sheet is drawn from the INSIDE. Its top half
   is itself a 1fr/1fr grid — title left, description right, divided by a
   border on .task-title-col (_legacy.css:3141) — so the sheet keeps
   padding: 0 and the two halves carry the inset instead. Their old
   paddings were asymmetric (28px on the outside, 16px against the
   divider), which was right for a full-width band and wrong for a sheet:
   the text sat off-centre inside it. */
.task-modal .modal-left-top { padding: 0; }
.task-modal .task-title-col,
.task-modal #mTaskDescBody { padding: 17px 22px 18px; }

/* ── Task window header: one line, two columns ──
   Title | members + start date. The description is not in the header
   at all on desktop (it stays reachable on mobile via the info sheet),
   and the strip is a single line so the thread below gets the height:
   the old two-row header was ~145px, this is ~50. The identity sheet is
   still its two markup halves (.modal-left-top / .modal-members) — they
   sit side by side and read as one sheet. */
@media (min-width: 769px) {
  .task-modal .modal-two-col {
    /* Not `1fr auto`: an auto track takes the members row's full
       max-content width and squeezed the title to 82px. Two fr tracks
       share the width and the chips wrap when they must. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr);
    grid-template-rows: auto 1fr;
  }
  .task-modal .modal-left-top {
    grid-column: 1; grid-row: 1;
    grid-template-columns: 1fr;                 /* legacy splits it 1fr 1fr for the description */
    border-radius: var(--radius) 0 0 var(--radius);
    border-bottom: 1px solid var(--hairline); border-right: 0;
    margin-bottom: 12px;
  }
  .task-modal #mTaskDescBody { display: none; }
  .task-modal .task-title-col {
    padding: 9px 16px;
    display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 12px;
  }
  .task-modal .task-title-col .project-title-edit { font-size: 16px; min-height: 0; padding: 2px 6px; }
  .task-modal .task-title-col .project-created-date { margin: 0; }
  .task-modal .task-title-col .tag-row { margin: 0; }
  .task-modal .modal-members {
    grid-column: 2; grid-row: 1;
    border-radius: 0 var(--radius) var(--radius) 0;
    border-left: 1px solid var(--hairline);
    margin-bottom: 12px; padding: 8px 16px;
    display: flex; flex-direction: row; align-items: center; flex-wrap: wrap; gap: 6px 14px;
  }
  .task-modal .modal-members .m-desktop-label { display: none; }   /* the chips say it */
  .task-modal .modal-members .m-section-body { flex: none; display: flex; align-items: center; gap: 6px; }
  .task-modal .task-sched-row { margin: 0; }
  .task-modal .discussion { grid-column: 1 / -1; grid-row: 2; }
}

/* On a phone the members sheet is hidden (_legacy.css:5999, !important —
   its content lives in the info popup). The start date now lives in that
   sheet, and scheduling must stay reachable, so the sheet shows with
   everything but the date row hidden. */
@media (max-width: 768px) {
  .task-modal .modal-members { display: block !important; padding: 0 20px 6px; background: none; border: 0; box-shadow: none; }
  .task-modal .modal-members > :not(.task-sched-row) { display: none !important; }
}

/* ── Discussion column ──
   .disc-panel holds the thread and the composer as siblings. It becomes
   a transparent column so each can be its own sheet with a gap between,
   rather than one region split by a divider. */
.modal .discussion,
.task-modal .discussion { display: flex; flex-direction: column; min-height: 0; }
/* Deliberately does NOT set `display`. _legacy.css:3685 already makes
   this a flex column, and :3686 hides the inactive tabs with
   `.disc-panel.hidden { display: none }` at (0,2,0). Setting display
   here — at (1,1,0) with the .modal prefix — outranked that and left
   Images, Videos, Files and Links all visible at once, stacked under
   the thread. Only the additive properties belong here. */
.modal .disc-panel,
.task-modal .disc-panel {
  gap: 16px;
  background: none; padding: 0;
}
.modal .messages,
.task-modal .messages {
  flex: 1; min-height: 0; overflow-y: auto;
  /* Recessed wash behind white message sheets — the same read as the
     Product Development rooms, instead of rows on a white panel. */
  background: var(--panel-footer);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: 12px;
}
.modal .messages .message,
.task-modal .messages .message {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sheet);
  margin-bottom: 10px;
}
.modal .message-composer,
.task-modal .message-composer {
  flex: none;
  /* _legacy.css:3214 gives every composer `margin: 0 16px 16px` — right
     when it was a band inside one panel, wrong now that it is a sheet
     sitting beside the thread sheet: it came out 32px narrower and
     16px short of the bottom, on top of the 16px gap. Two stacked
     sheets that do not share an edge read as a mistake. The spacing
     below is the disc-panel's gap, not a margin. */
  margin: 0;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sheet);
  padding: 14px 16px 12px;
}

/* ── Tasks sheet ── */
/* #pdProjOverlay is named so it can outrank _legacy.css:2208, which
   sets this pane to --bg-app at (1,1,0) — Product Development's products
   pane came out rgb(249,248,246) while every other sheet was white.
   Same specificity, later file, so this wins. */
.modal .tasks-section,
#pdProjOverlay .tasks-section {
  display: flex; flex-direction: column; min-height: 0;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sheet);
  overflow: hidden;
}
.modal .tasks-section .discussion-label { padding: 14px 18px 12px; }
.modal .task-list { flex: 1; min-height: 0; overflow-y: auto; }

/* Rows inside a sheet are transparent and separated by a hairline —
   a row is not a sheet of its own. */
.modal .task-list > * + * { border-top: 1px solid var(--hairline); }
/* Messages are sheets with their own borders now — no row hairlines. */
.modal .messages > * + *,
.task-modal .messages > * + * { border-top: 0; }

/* The tab strip is ground, and reads as the edge the sheets sit under. */
.modal .disc-tabs,
.task-modal .disc-tabs { background: none; border-bottom: 1px solid var(--rule); }


/* The mobile chat header and the Project info bar exist in the markup at
   all widths, so they need a DESKTOP DEFAULT — without it they render raw
   above the breadcrumb, which is exactly what happened: a bare chevron, a
   "G" mark and "Grunk 1 member · 24 tasks" stacked in the top-left corner.
   Adding markup means deciding what it does at every width, not only the
   one it was written for. */
.chat-head, .info-bar { display: none; }


/* ═══ PROJECT MODAL — MOBILE CHAT ═══════════════════ LOCKED ═══
   On a phone this is not a modal. It is a chat screen — WhatsApp,
   Messages — filling the viewport with no backdrop and nothing floating.

   Three things on screen, top to bottom: header, thread, chat bar.
   Everything else — description, created date, members, tab strip, task
   list, formatting toolbar — moves behind a tap on the header, into the
   Project info screen. That screen is the SAME DOM in a different state
   (.info-open on the modal), so nothing is moved and moved back.

   The desktop relationship inverts here on purpose. On desktop the thread
   is one white sheet holding transparent rows; a single sheet filling a
   375px screen stops meaning anything, so the rows become the sheets and
   the ground shows between them. That is what makes it read as a
   conversation rather than a document. */
@media (max-width: 768px) {

  /* ── 16px, or iOS zooms the page ──
     Safari zooms the whole document when a focused field is smaller than
     16px, and there is no way to zoom back out by hand. On a phone that
     looked like the window "moving weird" and then scrolling sideways —
     the zoom makes the layout wider than the visual viewport, so it
     scrolls to follow the caret.

     Every composer and every text field in a modal, not just the one it
     was reported on: they all share this input and they would all do it. */
  .message-input,
  .modal input[type="text"], .modal textarea,
  .task-modal input[type="text"], .task-modal textarea,
  .pd-room input[type="text"], .pd-room textarea,
  .cb-popup input[type="text"], .cb-popup textarea { font-size: 16px; }

  /* ── Full screen. No backdrop, no float, no radius. ── */
  .modal-overlay { padding: 0; background: none; }
  .modal {
    width: 100%; max-width: none;
    height: 100dvh; max-height: none;
    border: 0; border-radius: 0; box-shadow: none;
    background: var(--panel);
    display: flex; flex-direction: column;
  }
  /* The task window is a chat too — same full-bleed screen, not a
     floating card over a dimmed board. */
  .task-overlay { padding: 0; background: none; }
  .task-modal {
    width: 100%; max-width: none;
    height: 100dvh; max-height: none;
    border: 0; border-radius: 0; box-shadow: none;
    background: var(--panel);
    display: flex; flex-direction: column;
  }

  /* ── 1. Header ── */
  .modal .modal-title-row { display: none; }
  .task-modal .modal-title-row { display: none; }
  .modal .chat-head,
  .task-modal .chat-head {
    flex: none;
    display: flex; align-items: center; gap: 10px;
    height: 58px; padding: 0 12px 0 8px;
    background: var(--surface);
    border-bottom: 1px solid var(--rule);
    box-shadow: 0 1px 2px var(--hairline);
  }
  .chat-head__back, .chat-head__act {
    flex: none; width: 36px; height: 36px;
    display: grid; place-items: center;
    background: none; border: 0; border-radius: var(--radius);
    color: var(--ink-icon); cursor: pointer;
  }
  .chat-head__back:hover, .chat-head__act:hover { background: var(--wash-ink); color: var(--ink); }
  .chat-head__mark {
    flex: none; width: 38px; height: 38px; border-radius: var(--radius);
    background: var(--accent); color: var(--surface);
    display: grid; place-items: center;
    font-family: var(--font-heading); font-size: 17px; line-height: 1;
  }
  /* The whole text block is the tap target for Project info. */
  .chat-head__text {
    flex: 1; min-width: 0; text-align: left;
    background: none; border: 0; padding: 0; cursor: pointer;
    display: flex; flex-direction: column; gap: 2px;
  }
  .chat-head__title {
    font-family: var(--font-heading); font-size: 18px; line-height: 1.15;
    color: var(--ink);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .chat-head__sub {
    font-size: 11.5px; color: var(--ink-label);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }

  /* ── 2. Thread — the only thing that scrolls ── */
  .modal .modal-two-col {
    flex: 1; min-height: 0;
    display: flex; flex-direction: column;
    overflow: hidden;
  }
  .modal .discussion { flex: 1; min-height: 0; display: flex; flex-direction: column; }
  /* _legacy.css:5920 painted this #d8dbe0 — a COOL grey in a warm palette,
     through ID selectors that outrank any class rule. Same intent, the
     palette's own ground; the ids are named so this wins. */
  .modal .disc-panel,
  #discPanel, #tmDiscPanel { background: var(--panel); }
  .modal .disc-panel { flex: 1; min-height: 0; gap: 0; padding: 0; }

  .modal .messages {
    flex: 1; min-height: 0; overflow-y: auto;
    background: transparent; border: 0; border-radius: 0; box-shadow: none;
    display: flex; flex-direction: column; gap: 8px;
    padding: 12px 12px 14px;
  }
  .modal .messages > * + * { border-top: 0; }

  .modal .messages .message { align-items: flex-end; gap: 8px; max-width: 86%; padding: 0; }
  .modal .messages .message-body {
    min-width: 0;
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: var(--radius) var(--radius) var(--radius) 2px;
    box-shadow: var(--shadow-sheet);
    padding: 8px 12px 7px;
  }
  .modal .messages .message-author { color: var(--accent); font-size: 11.5px; font-weight: 600; }
  .modal .messages .message-time { display: block; text-align: right; font-size: 10.5px; }
  /* Meta goes UNDER the content, chat-app style. As a header it made an
     image-only post read as an anonymous card with a timestamp banner. */
  .modal .messages .message-body { display: flex; flex-direction: column; }
  .modal .messages .message-meta {
    order: 99; margin-top: 2px;
    display: flex; align-items: baseline; gap: 6px; justify-content: flex-end;
  }
  /* Author still leads for other people's messages. */
  .modal .messages .message-meta .message-author { margin-right: auto; }
  /* Yours carry a small "You" so the sender is never ambiguous. */
  .modal .messages .message.is-mine .message-meta::before {
    content: 'You';
    font-size: 10.5px; font-weight: 700; letter-spacing: .05em;
    color: var(--ink-label); margin-right: auto;
  }

  /* Yours: right, tinted, no avatar, no repeated name, mirrored corner. */
  .modal .messages .message.is-mine { align-self: flex-end; flex-direction: row-reverse; max-width: 78%; }
  /* ── The COLLECTION window follows the Product Room, not WhatsApp ──
     Its threads read like the product threads: every message a white
     sheet with the avatar at its bottom-left and the author named — own
     messages included, no right-aligned "mine" treatment. */
  #pdProjOverlay .messages .message.is-mine {
    align-self: flex-start; flex-direction: row; max-width: 92%;
  }
  #pdProjOverlay .messages .message { max-width: 92%; }
  #pdProjOverlay .messages .message.is-mine .avatar,
  #pdProjOverlay .messages .message.is-mine .avatar-sm { display: flex; }
  #pdProjOverlay .messages .message.is-mine .message-author { display: inline; }
  #pdProjOverlay .messages .message.is-mine .message-meta::before { content: none; }
  #pdProjOverlay .messages .message.is-mine .message-body {
    background: var(--surface);
    border-color: var(--hairline);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sheet);
  }
  #pdProjOverlay .messages .message-body {
    border-radius: var(--radius);
    box-shadow: var(--shadow-sheet);
  }
  /* Card anatomy exactly like the Product Room: author + time at the
     TOP of the sheet, actions inline at its top-right — not the
     chat-app bottom meta the other windows use. */
  #pdProjOverlay .messages .message-meta {
    order: 0; justify-content: flex-start; margin: 0 0 4px;
  }
  #pdProjOverlay .messages .message-meta .message-author { margin-right: 0; }
  #pdProjOverlay .messages .message-time { text-align: left; }
  #pdProjOverlay .messages .msg-actions {
    top: 6px; right: 6px;
    background: none; border: 0; box-shadow: none;
    border-radius: 0; padding: 0;
  }
  /* Chat bar exactly like the Product Room: +, pill input, mic/send. */
  #pdProjOverlay .message-input {
    background: var(--panel); border: 1px solid var(--hairline);
    border-radius: 999px; padding: 10px 14px; min-height: 40px;
  }
  #pdProjOverlay #pdpjEmojiBtn { display: none; }
  /* Tap-to-reveal actions (touch keeps :hover after a tap): as a small
     floating pill above the bubble, not bare icons over the image. */
  .modal .messages .msg-actions {
    top: -36px; right: 0;
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: 999px;
    padding: 3px 8px;
    box-shadow: var(--shadow-popup);
  }
  .modal .messages .message.is-mine .avatar,
  .modal .messages .message.is-mine .avatar-sm,
  .modal .messages .message.is-mine .message-author { display: none; }
  .modal .messages .message.is-mine .message-body {
    background: var(--accent-tint);
    border-color: var(--accent-tint-border);
    border-radius: var(--radius) var(--radius) 2px var(--radius);
    box-shadow: none;
  }

  /* ── 3. Chat bar — one row, pinned ── */
  .modal .message-composer {
    flex: none;
    background: var(--surface);
    border: 0; border-top: 1px solid var(--rule);
    border-radius: 0; box-shadow: none;
    /* Clear the iPhone home indicator in standalone mode. */
    padding: 8px 10px calc(12px + env(safe-area-inset-bottom, 0px)) 6px;
  }
  /* One row only: no formatting buttons on a phone. */
  .modal .composer-toolbar { display: none !important; }

  /* Product Development's composer was never given the mobile chat-bar
     treatment. _legacy.css:5796 builds it from ID lists — #toggleToolbarBtn
     and #tmToggleToolbarBtn, #emojiBtn and #tmEmojiBtn, and so on — which
     cover the project modal and the task modal and stop there. PD's twins
     are named the same way with a pdpj prefix, so its chat bar kept the
     full desktop lineup: Aa, @, and a boxed input.

     Mirrored here rather than extended there, so all three composers are
     described in one place. The real fix is semantic classes on these
     buttons instead of a growing list of ids — noted, not done. */
  #pdpjToggleToolbarBtn,
  #pdpjMentionBtn,
  #pdpjDictateBtn,
  #pdpjEnterToggle { display: none !important; }
  /* Emoji sits inside the input pill, right edge, as it does elsewhere. */
  #pdpjEmojiBtn {
    position: absolute; right: 50px; bottom: 4px;
    width: 32px; height: 32px; border-radius: 50%;
    color: var(--ink-icon); opacity: 0.75; z-index: 2;
  }
  #pdpjAttachBtn { flex-shrink: 0; width: 38px; height: 38px; border-radius: 50%; order: 1; }
  .modal .message-input {
    min-height: 40px;
    background: var(--panel);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    font-size: 14px;
  }
  .modal .send-btn {
    width: 40px; height: 40px; flex: none;
    border-radius: var(--radius);
    background: var(--accent); color: var(--surface);
  }

  /* ── Hidden on the conversation screen ── */
  /* #pdProjOverlay is named because MY OWN desktop sheet rule
     (#pdProjOverlay .tasks-section, added so the products pane would be
     white) is (1,1,0) and outranks a plain .modal rule — so Product
     Development kept its products list on the chat screen while the task
     board hid its tasks. Specificity has to be matched, not assumed. */
  .modal .modal-left-top,
  .modal .modal-members,
  .modal .disc-tabs,
  .modal .tasks-section,
  #pdProjOverlay .tasks-section,
  .modal .info-bar { display: none; }
  /* The task window is a chat screen like .modal above. Its chat-head
     (#tmChatHead) carries the name, project and status; everything
     hidden here comes back in the Group Info popup (tap the title or ⋮).
     display:none needs !important only where legacy's mobile block sets
     a display of its own on the same element. */
  .task-modal .modal-left-top { display: none !important; }
  .task-modal .modal-members { display: none !important; }
  .task-modal .disc-tabs,
  .task-modal .task-sched-row,
  .task-modal .info-bar { display: none; }
  .task-modal .modal-two-col {
    flex: 1; min-height: 0;
    display: flex; flex-direction: column;
    overflow: hidden; padding: 0; gap: 0;
  }
  .task-modal .discussion { flex: 1; min-height: 0; }
  /* Thread runs edge to edge like the Product Room — no inset card. */
  .task-modal .disc-panel { margin: 0; border-radius: 0; border: 0; box-shadow: none; }
  /* Workflow buttons sit between the head and the thread (JS moves them
     into .modal-two-col on mobile) — style as a slim action bar. */
  .task-modal .modal-two-col > .workflow-actions {
    flex: none; padding: 8px 12px;
    background: var(--surface); border-bottom: 1px solid var(--rule);
  }

  /* ── Project info: the same DOM, a different state ── */
  .modal.info-open .chat-head,
  .modal.info-open .discussion,
  .modal.info-open .message-composer { display: none; }
  .modal.info-open .info-bar {
    flex: none;
    display: flex; align-items: center; gap: 8px;
    height: 58px; padding: 0 12px 0 8px;
    background: var(--surface);
    border-bottom: 1px solid var(--rule);
  }
  .info-bar__back {
    flex: none; width: 36px; height: 36px;
    display: grid; place-items: center;
    background: none; border: 0; border-radius: var(--radius);
    color: var(--ink-icon); cursor: pointer;
  }
  .info-bar__title { font-size: 15px; font-weight: 600; color: var(--ink); }
  .modal.info-open .modal-two-col { overflow-y: auto; padding: 12px; gap: 12px; }
  /* !important is required, not preferred: _legacy.css already hides the
     description, created date and members on mobile with !important —
     which is the right call for the CHAT screen, and exactly what the
     brief asks for. The info screen is where they come back, so
     un-hiding them has to match that weight. */
  .modal.info-open .modal-left-top { display: block !important; }
  .modal.info-open .modal-members,
  .modal.info-open .tasks-section,
  #pdProjOverlay .modal.info-open .tasks-section { display: flex !important; }
  .modal.info-open .project-created-date { display: block !important; }
  .modal.info-open #mProjDescBody { display: block !important; }
  .modal.info-open .tasks-section { flex: none; max-height: none; }
}


/* ═══ PRODUCT ROOM ══════════════════════════════════ LOCKED ═══
   The fourth window of its kind, and the last one still built the old
   way: structure.css owned only its shell, and all 47 of its interior
   rules lived in the legacy sheet. That is why it felt like a different
   app after the other three became sheets on ground.

   From product-modal.md, with its token names mapped onto ours:
     --ink-2 → --ink-secondary   --ink-3 → --ink-label
     --ink-4 → --ink-icon        --navy  → --accent
     --overdue → --tone-bad-ink  --sheet-shadow → --shadow-sheet

   STAGE COLOUR IS DATA, not CSS. PD_STAGES carries color / wash / head /
   fg per stage and the renderer writes them inline, which is right: a
   stage is configurable and a board may define its own. Nothing here
   hardcodes a stage colour — the rules below only say where it goes. */

.pd-room { background: var(--panel); }
/* 44px, per the spec — and SCOPED, because .modal-title-row is shared
   with the invoice client window, which is signed off. Unscoped this
   would have shortened that window's header too. */
.pd-room .modal-title-row {
  height: 44px; min-height: 44px; box-sizing: border-box;
  padding: 0 16px 0 24px;
  display: flex; align-items: center; gap: 16px;
  border-bottom: 1px solid var(--rule);
}
/* The gear and the close were ON TOP OF EACH OTHER. .modal-close is
   absolutely positioned to top:18 right:18 for every other modal, while
   the gear is an ordinary flex child at the row's right edge — at the
   old 74px header they missed each other, and at 44px they land in the
   same 28px square.

   product-modal.md puts both in one group: `.actions { margin-left:
   auto; display: flex; gap: 2px }`. So here the close stops being
   absolute and simply queues up after the gear. SCOPED to .pd-room:
   every other modal still relies on the absolute placement. */
.pd-room .modal-breadcrumb { flex: 1; min-width: 0; }
.pd-room .pd-room-icon-btn,
.pd-room .modal-close {
  position: static; flex: none; margin: 0;
  width: 28px; height: 28px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius);
}
.pd-room .pd-room-icon-btn + .modal-close { margin-left: 2px; }

/* ── Header: identity left, timeline right ── */
.pd-room-head {
  display: flex; align-items: center; justify-content: space-between;
  /* Tighter than the spec's 16/14. The header was 117px for a 49px
     title block: the TIMELINE is the tall element at 86px, and centring
     the short thing against the tall one put 35px of air above the title
     and 34px below the subline. Cutting padding alone would not have
     fixed it — the centring gap would remain — so the timeline's own
     stack is tightened below as well. */
  gap: 32px; padding: 10px 24px 10px; flex: none;
  border-bottom: 1px solid var(--hairline-strong);
}
.pd-room-head .pd-thumb-box {
  width: 44px; height: 44px; flex: none;
  border-radius: var(--radius); border: 1px dashed var(--hairline-strong);
  /* `background: none` wiped the sizing. The renderer sets
     style.backgroundImage inline, and legacy carried the `center/contain
     no-repeat` that made it fit — my shorthand reset it to `auto`, so the
     photo was applied but rendered at its natural size. Keep the
     positioning; the image still arrives inline. */
  background: none center / contain no-repeat;
  display: grid; place-items: center;
  color: var(--ink-icon); font-size: 8.5px; line-height: 1.15;
  cursor: pointer; overflow: hidden;
}
.pd-room-head .pd-thumb-box:hover { border-color: var(--accent); color: var(--accent); }
.pd-room-head .pd-thumb-box.has-photo { border-style: solid; border-color: var(--hairline); }
.pd-head-text { min-width: 0; margin-left: 14px; margin-right: auto; }
.pd-value-title {
  font-family: var(--font-heading); font-size: 26px; line-height: 1.15;
  color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  outline: none;
}
/* The current stage, said once. The spec is explicit that stage state
   lives in three places only — this line, the timeline, and the sticky
   bar of the open section — so there is no "current stage" card. */
.pd-head-stage { display: flex; align-items: center; gap: 7px; font-size: 12.5px; margin-top: 3px; }
.pd-head-stage .name { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; }
.pd-head-stage .name::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.pd-head-stage .sep { color: var(--hairline-strong); }
.pd-head-stage .due { color: var(--ink-label); }

/* ── Timeline ──
   The app already emits .pd-tstage > .rail > .seg/.dot/.seg + .tname +
   .tsub, and draws the connecting line as segments either side of each
   node rather than one absolute track. That works and is already wired
   to scroll the thread, so it is styled rather than replaced. */
.pd-timeline {
  display: grid; grid-template-columns: repeat(4, 1fr);
  width: 520px; flex: none;
  /* Legacy gave this `padding: 12px 6px 10px` and a bottom border, from
     when it was its own full-width band BELOW the header. It sits inside
     the header now, so both are leftovers — and together they were 23px
     of the 26px of air above the title. */
  padding: 0; border-bottom: 0;
}
.pd-tstage {
  background: none; border: 0; cursor: pointer; padding: 0;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  font: inherit; line-height: 1.2;
}
.pd-tstage .rail { display: flex; align-items: center; width: 100%; }
/* `.pd-tstage .rail .seg`, not `.pd-tstage .seg` — _legacy.css:6237 uses
   the three-class form at (0,3,0) and sets height: 2px, so the shorter
   selector below never won and the connecting line stayed twice the
   weight the design asks for. product-modal.md is explicit: a 1px track
   in `hsl(24 10% 10% / 0.14)`, and the completed run 1px in the first
   stage's colour. At 2px it reads as a progress BAR rather than a
   thread between four points. */
.pd-tstage .rail .seg { flex: 1; height: 1px; background: var(--hairline-strong); }
.pd-tstage .rail .seg.hide { visibility: hidden; background: transparent; }
.pd-tstage .dot {
  width: 22px; height: 22px; flex: none; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 11px; font-weight: 600;
  background: var(--surface); border: 1px solid var(--hairline-strong);
  color: var(--ink-label);
}
.pd-tstage.done .dot    { color: var(--on-accent); border-color: transparent; }
.pd-tstage.current .dot { border-width: 1.5px; font-weight: 700; }
.pd-tstage .tname { font-size: 12.5px; font-weight: 500; color: var(--ink-secondary); line-height: 1.2; }
.pd-tstage.current .tname { font-weight: 600; }
.pd-tstage .tsub  { font-size: 11px; color: var(--ink-label); margin-top: -2px; }

/* ── Body: thread left, sidebar right ── */
.pd-room-body {
  flex: 1; min-height: 0; display: grid;
  grid-template-columns: 1fr 272px; gap: 20px;
  /* 10px on top, not 16. The tab strip also carried an inline
     `padding: 10px 18px` from the markup, so the gap above the tabs was
     26px of two paddings stacked. The inline is gone — the strip is
     styled below — and this is the only one left. */
  padding: 10px 24px 24px;
}
.pd-room-main { min-height: 0; display: flex; flex-direction: column; gap: 12px; }
.pd-room-main .disc-tabs {
  flex: none; background: none; gap: 6px;
  padding: 0 0 8px; border-bottom: 1px solid var(--rule);
}

/* The thread is ONE sheet; the stage sections are washed bands inside
   it, so the sheet's edge stays unbroken as you scroll through stages. */
.pd-room .pd-thread {
  flex: 1; min-height: 0; overflow-y: auto; padding: 0;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sheet);
}
.pd-room .message-composer {
  flex: none; margin: 0;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sheet);
}

/* ── Stage sections ──
   Wash and bar colour arrive inline from PD_STAGES. The last section
   gets min-height:100% so the current stage's wash fills the rest of
   the sheet rather than stopping in mid-air. */
.pd-sec:last-child { min-height: 100%; }
.pd-sec-head {
  position: sticky; top: 0; z-index: 2;
  display: flex; align-items: center; gap: 8px; min-height: 40px;
  padding: 7px 12px 7px 16px;
  font-size: 12.5px; font-weight: 600;
  border-bottom: 1px solid var(--hairline);
}
.pd-sec-head .sdot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.pd-sec-head .swhen { font-size: 11.5px; font-weight: 400; color: var(--ink-label); }
.pd-sec-head .sactions { margin-left: auto; display: flex; align-items: center; gap: 6px; }

/* Messages on the desktop thread are white sheets on the stage wash —
   the same read as the mobile bubbles. Without this they fell through
   to legacy .message's --bg-subtle, a cream card on a cream wash, and
   the whole room read as one undifferentiated tint. */
.pd-room .pd-sec-body .message {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sheet);
}

/* ── Sidebar: a tray holding three sheets ──
   Recessed rather than flat, so three white cards read as sitting IN
   something rather than floating beside the thread. */
.pd-room-side {
  min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: 10px;
  background: var(--panel-footer);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: 10px;
}
.pd-room-side .pd-cell {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sheet);
  padding: 13px 16px;
}
.pd-room-side .pd-cell-head { display: flex; align-items: center; gap: 8px; }
.pd-room-side .pd-mini-label {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; font-weight: 700; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--ink);
}
.pd-room-side .pd-edit-link {
  margin-left: auto; background: none; border: 0; cursor: pointer;
  font: inherit; font-size: 11.5px; color: var(--ink-label); padding: 0;
}
.pd-room-side .pd-edit-link:hover { color: var(--accent); }
.pd-room-side .pd-value-big {
  font-family: var(--font-heading); font-size: 24px; line-height: 1.15;
  color: var(--ink); margin-top: 6px;
}
.pd-room-side .pd-due-left { font-size: 12px; color: var(--ink-label); margin-top: 4px; }
.pd-room-side .pd-half-content { display: block; font-size: 13px; margin-top: 8px; }

/* ── PRODUCT ROOM — MOBILE CHAT ──
   The same inversion as the project windows: on a phone this is not a
   modal, it is a chat screen. The desktop thread is ONE white sheet
   holding washed stage bands; a single sheet filling 375px stops meaning
   anything, so here the stage wash becomes the ground and the messages
   become the sheets.

   On screen, top to bottom: the white header with the stepper inside it,
   the thread, the chat bar. The tabs, the three sidebar cards and the
   desktop header go behind the header — that info sheet is not designed
   yet, so for now the ⋯ opens the settings gear it already had. */
@media (max-width: 768px) {

  .pd-room-overlay { padding: 0; background: none; }
  .pd-room {
    width: 100%; max-width: none;
    height: 100dvh; max-height: none;
    border: 0; border-radius: 0; box-shadow: none;
    background: var(--panel);
  }

  /* The desktop chrome steps aside — but NOT .pd-room-head, because the
     timeline lives inside it. Hiding the parent hid the stepper too.
     The row stays and loses everything except the timeline, so it reads
     as the second line of the white header block. */
  .pd-room .modal-title-row,
  .pd-room .pd-room-main .disc-tabs,
  .pd-room .pd-room-side { display: none; }
  .pd-room .pd-room-head {
    display: block; padding: 0 10px 8px; gap: 0;
    background: var(--surface); border-bottom: 1px solid var(--rule);
  }
  .pd-room .pd-room-head .pd-thumb-box,
  .pd-room .pd-head-text { display: none; }

  /* ── Header: one row, then the stepper, in one white block ── */
  .pd-room .chat-head {
    flex: none; display: block; padding: 0;
    background: var(--surface);
    border-bottom: 1px solid var(--rule);
    box-shadow: 0 1px 2px var(--hairline);
  }
  .pd-room .chat-head {
    display: flex; align-items: center; gap: 10px;
    padding: 8px 12px 6px 8px;
    border-bottom: 0; box-shadow: none;
  }
  /* The stepper: the same timeline, stripped of its dates — four nodes
     and four labels is all that fits across 375px. */
  .pd-room .pd-timeline { width: 100%; padding: 0; }
  .pd-room .pd-tstage .dot   { width: 16px; height: 16px; font-size: 9px; }
  .pd-room .pd-tstage .tname { font-size: 10.5px; text-align: center; }
  .pd-room .pd-tstage .tsub  { display: none; }

  /* ── Thread: stage sections ARE the layout, no sheet around them ── */
  .pd-room .pd-room-body {
    display: flex; flex-direction: column;
    gap: 0; padding: 0; min-height: 0;
  }
  /* flex: 1, or the composer floats. On desktop this is a GRID item and
     stretches for free; on a phone it is a flex item in a column, so
     without this it is only as tall as its content and the chat bar sits
     halfway up the screen with ground beneath it. */
  .pd-room .pd-room-main { gap: 0; flex: 1; min-height: 0; }
  .pd-room .pd-thread {
    background: none; border: 0; border-radius: 0; box-shadow: none;
    padding: 0;
  }
  .pd-room .pd-sec-body {
    display: flex; flex-direction: column; gap: 8px;
    padding: 8px 12px 12px;
  }
  /* THE STAGE BAR HAS TO WRAP. The current stage carries its actions —
     a factory button plus Approve and Revision — in a row that does not
     wrap, and three buttons after the meta text is far wider than 375px.
     That is what was dragging the whole thread sideways, not an image.
     The actions drop to a second line inside the band instead.

     product-modal.md would rather they moved INTO the submission card as
     full-width buttons on a phone. That is a change to where the
     renderer appends them, not a style, so it is a separate job — this
     stops the overflow now. */
  .pd-room .pd-sec-head {
    min-height: 34px; padding: 6px 12px; font-size: 11.5px;
    flex-wrap: wrap; row-gap: 6px;
  }
  .pd-room .pd-sec-head .swhen { flex: 1 1 100%; min-width: 0; }
  .pd-room .pd-sec-head .sactions { margin-left: 0; flex-wrap: wrap; }

  /* Nothing constrained an image inside a message — anywhere, in either
     stylesheet. A desktop screenshot pasted into a thread arrives 1400px
     wide and pushes the column out with it. */
  .pd-room .message-text img,
  .pd-room .pd-sec-body img { max-width: 100%; height: auto; }

  /* The header mark is 38px and carries the product photo when there is
     one; a dashed empty box when there is not, matching the design. */
  .pd-room .chat-head__mark {
    width: 38px; height: 38px; border-radius: var(--radius);
    display: grid; place-items: center;
    font-size: 12px; font-weight: 600; color: var(--on-accent);
  }
  .pd-room .chat-head__mark.has-photo { border: 1px solid var(--hairline); }
  .pd-room .chat-head__title { font-family: var(--font-heading); font-size: 19px; }

  /* ── Messages become bubbles ──
     White sheets on the stage wash, with the avatar OUTSIDE the bubble at
     its bottom-left and the time inside at the bottom-right. That is what
     makes a row read as something someone said rather than a table row. */
  .pd-room .pd-sec-body .message {
    display: flex; align-items: flex-end; gap: 8px;
    background: none; border: 0; box-shadow: none; padding: 0;
    max-width: 92%;
  }
  .pd-room .pd-sec-body .message-body {
    flex: 1; min-width: 0;
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sheet);
    padding: 9px 12px 7px;
  }
  .pd-room .pd-sec-body .message-author {
    color: var(--accent); font-size: 12px; font-weight: 600;
  }
  .pd-room .pd-sec-body .message-time {
    display: block; text-align: right;
    font-size: 10.5px; color: var(--ink-icon); margin-top: 3px;
  }
  .pd-room .pd-sec-body .avatar,
  .pd-room .pd-sec-body .avatar-sm { flex: none; align-self: flex-end; }

  /* Events sit centred between bubbles, not left-aligned like a row. */
  .pd-room .pd-sec-body .pd-event,
  .pd-room .pd-sec-body .pd-marker {
    align-self: center; text-align: center; max-width: 92%;
  }

  /* ── Chat bar ──
     One row: +, the input as a pill, send. The formatting tools, the
     mention button and the "Return to Send" checkbox are desktop
     affordances — a phone keyboard has its own return key. */
  .pd-room .message-composer .composer-toolbar,
  .pd-room .message-composer .fmt-btn.tmfmt,
  .pd-room .message-composer #pdToggleToolbarBtn,
  .pd-room .message-composer #pdMentionBtn,
  .pd-room .message-composer #pdEmojiBtn { display: none; }
  /* "Return to Send" is an unclassed <label> around #pdEnterToggle, so it
     has to be reached through the input. (.composer-hint was my invention
     — the guard caught it, the app emits no such class.) A phone keyboard
     has its own return key; the checkbox is a desktop affordance. */
  .pd-room .message-composer label:has(#pdEnterToggle) { display: none; }
  .pd-room .message-composer .message-input {
    background: var(--panel); border: 1px solid var(--hairline);
    border-radius: 999px; padding: 10px 14px; min-height: 40px;
  }

  /* ── Chat bar ── */
  .pd-room .message-composer {
    flex: none; margin: 0; border-radius: 0;
    border: 0; border-top: 1px solid var(--rule);
    box-shadow: none;
  }
}

/* ── "Your client sees an older version" ──
   Shown on an invoice card and in the editor when a SENT invoice has been
   edited since it was last published. Editing writes to your own copy;
   sharedInvoices/{token} — the document behind the link in the client's
   inbox — is only rewritten by Send. Without this you can change $3,000
   to $2,032.26, see the new figure on your board, and leave a Stripe
   button in their inbox still charging $3,000.

   Warn tone, not bad: nothing is broken, something is pending. */
.inv-stale-note {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; line-height: 1.35;
  padding: 4px 10px; border-radius: var(--radius);
  background: var(--tone-warn-bg); color: var(--tone-warn-ink);
}
.inv-stale-note[hidden] { display: none; }

/* ═══ SEND INVOICE ══════════════════════════════════ LOCKED ═══
   From send-invoice.md. The dialog was a form with no preview: you wrote
   an email and pressed Send without ever seeing it. The right half is
   the email now.

   Every id the existing handlers read — invSendTo / From / Subject / Msg
   / When / Summary / Go / Close — is unchanged. _invSend() and the
   scheduling logic are untouched; only the surface moved. */

#invSendOverlay .send-modal {
  width: min(960px, calc(100vw - 48px)); max-width: 960px;
  height: min(700px, 90vh);
  display: flex; flex-direction: column;
  background: var(--panel); overflow: hidden;
  /* Legacy .inv-pick-modal (0,1,0) sets 12px and a heavy shadow; this
     (1,1,0) outranks it. The spec's geometry, stated here on purpose. */
  border: 1px solid var(--rule); border-radius: var(--radius);
  box-shadow: 0 2px 4px hsl(24 10% 10% / 0.06), 0 12px 30px hsl(24 10% 10% / 0.08), 0 30px 70px hsl(24 10% 10% / 0.10);
}

/* ── Header: the invoice's identity, not the word "Send" ── */
.send-head {
  display: flex; align-items: center; gap: 16px;
  padding: 18px 24px 16px;
  background: var(--surface); border-bottom: 1px solid var(--rule);
}
.micro {
  font-size: 11px; font-weight: 600; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink-label);
}
.send-head__row { display: flex; align-items: center; gap: 10px; margin-top: 3px; }
.send-head__num { font-family: var(--font-heading); font-size: 24px; line-height: 1.15; color: var(--ink); }
.client-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
.send-head__meta { font-size: 13px; color: var(--ink-label); }
.send-head__total { margin-left: auto; text-align: right; }
.send-head__total .figure {
  font-family: var(--font-heading); font-weight: 400;
  font-size: 26px; line-height: 1.1; color: var(--accent);
}

/* ── Body: compose left, the email right ── */
.inv-body.send-body {
  flex: 1; min-height: 0; padding: 0;
  display: grid; grid-template-columns: 400px minmax(0, 1fr);
}
.send-compose {
  min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: 16px;
  padding: 22px 24px 24px;
  border-right: 1px solid var(--hairline-strong);
}
.send-compose label { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.send-compose input, .send-compose textarea {
  width: 100%; background: var(--surface);
  border: 1px solid var(--hairline-strong); border-radius: var(--radius);
  padding: 10px 12px; font: inherit; font-size: 14px; overflow-wrap: anywhere;
}
.send-compose textarea { min-height: 132px; line-height: 1.6; resize: vertical; }
.send-compose :is(input, textarea):focus-visible {
  outline: none; border-color: var(--accent-tint-border);
  box-shadow: 0 0 0 3px var(--accent-tint);
}
.field-help { font-size: 12px; color: var(--ink-label); }
.label-aside {
  font-size: 11.5px; color: var(--ink-label); margin-left: 8px;
  text-transform: none; letter-spacing: 0; font-weight: 400;
}
.send-2up { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: 14px 16px; }
/* From and Subject are read-only tiles. They state a fact; they are not
   fields you fill in, and looking like one is a small lie. */
#invSendFrom, #invSendSubject {
  background: var(--panel-footer); border-color: var(--hairline);
  font-size: 13px; color: var(--ink-secondary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default;
}

/* ── When ── */
.send-when { display: flex; flex-direction: column; gap: 6px; }
/* .send-seg, not .seg. `.seg` is ALREADY the timeline's connecting
   segment — .pd-tstage .rail .seg — and this rule's padding and border
   landed on all eight of them, turning a 1px thread into an 8px bar.
   The height rule was fine the whole time; two unrelated things simply
   shared a four-letter class name because I picked it from the spec
   without checking it was free. */
.send-seg {
  display: flex; background: var(--surface);
  border: 1px solid var(--hairline-strong); border-radius: var(--radius); padding: 3px;
}
.send-seg__opt {
  flex: 1; padding: 7px 10px; border: 0; border-radius: 4px;
  background: transparent; font: inherit; font-size: 13px; font-weight: 500;
  color: var(--ink-secondary); cursor: pointer;
}
.send-seg__opt.is-on { background: var(--accent); color: var(--on-accent); }
.send-when__date { display: flex; align-items: center; gap: 10px; margin-top: 4px; }
.send-when__date input { flex: 1; padding: 9px 12px; font-size: 13.5px; }
.send-when__date[hidden] { display: none; }

/* ── Footnotes ── */
.send-notes {
  margin-top: auto; padding-top: 8px;
  border-top: 1px solid var(--hairline-strong);
  display: flex; flex-direction: column; gap: 8px;
  font-size: 12px; line-height: 1.6; color: var(--ink-label);
}

/* ── Preview: the email on inbox ground ──
   The bar sits IN the scroll flow, not pinned above it — an earlier
   version read as sticky and clipped the envelope line under it. There
   is no separate "no published copy yet" state any more: the preview
   renders straight from the current invoice (_invSharedPayload), so it
   has something to show even before the first send. */
.send-preview {
  min-height: 0; overflow-y: auto;
  background: var(--panel-footer); padding: 22px 28px 30px;
}
.send-preview__bar { display: flex; align-items: baseline; gap: 8px; margin-bottom: 14px; }
.send-preview__bar .sub { font-size: 12px; color: var(--ink-label); }
.send-preview__email { max-width: 480px; margin: 0 auto; }
/* The envelope reads like an inbox row: sender on the left, subject on
   the right — above the card, not baked into the email itself. */
.send-preview__envelope {
  display: flex; justify-content: space-between; gap: 12px;
  font-size: 12px; color: var(--ink-label); padding: 0 4px 10px;
}
.send-preview__envelope span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.send-preview__envelope b { font-weight: 600; color: var(--ink-secondary); white-space: nowrap; }
#invSendPreviewBox iframe {
  width: 100%; border: 1px solid var(--hairline);
  border-radius: var(--radius); background: var(--surface); display: block;
  min-height: 300px; /* until the load handler sizes it to the real content */
}

/* ── Footer ── */
.inv-actions.send-foot {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 24px; background: var(--panel-footer);
  border-top: 1px solid var(--rule);
}
.send-foot__note { font-size: 12.5px; color: var(--ink-label); line-height: 1.4; }
/* Legacy .inv-btn is a 12px 7px-radius chip with a black primary. Two
   classes here so both the ghost and the primary outrank it (0,2,0). */
.send-foot .inv-btn {
  margin-left: auto; display: inline-flex; align-items: center; gap: 7px;
  font-size: 13px; font-weight: 500; padding: 8px 14px;
  border-radius: var(--radius); border: 1px solid var(--hairline-strong);
  background: var(--surface); color: var(--ink);
}
.send-foot .inv-btn + .inv-btn { margin-left: 0; }
.send-foot .inv-btn:hover { border-color: hsl(24 10% 10% / 0.2); }
.send-foot .inv-btn-primary { background: var(--accent); color: var(--on-accent); border-color: transparent; }
.send-foot .inv-btn-primary:hover { background: var(--accent-hover); }
.send-foot .inv-btn-primary:disabled { opacity: 0.6; cursor: default; }
.send-foot__icon { width: 13px; height: 13px; flex: none; }

@media (max-width: 720px) {
  /* One column. The preview goes under the compose pane rather than
     behind a button — the spec's full-screen preview screen is not
     designed yet. */
  .inv-body.send-body { grid-template-columns: 1fr; grid-template-rows: auto auto; }
  .send-compose { border-right: 0; border-bottom: 1px solid var(--hairline-strong); }
  #invSendOverlay .send-modal { width: 100%; max-width: none; height: 100dvh; border-radius: 0; }
}

/* ── Line item qty × rate ──
   The rate field was 52px wide and needed 71 to show "2032.26" plus the
   number spinner, so a prorated amount rendered as "2032" — the value
   was right, the field just cut it off, which is the worst way for a
   money field to be wrong.

   The spinner goes: nobody steps a price by 0.01 with arrows, and it was
   eating ~15px of a field that had none to spare. Then both fields are
   wide enough for a real figure. */
.inv-i-qty, .inv-i-rate {
  -moz-appearance: textfield;
  appearance: textfield;
}
.inv-i-qty::-webkit-outer-spin-button, .inv-i-rate::-webkit-outer-spin-button,
.inv-i-qty::-webkit-inner-spin-button, .inv-i-rate::-webkit-inner-spin-button {
  -webkit-appearance: none; margin: 0;
}
/* SCOPED, because _legacy.css:2816 is `.wpi-line-sub input { width: 52px }`
   at (0,1,1) and beats a bare `.inv-i-rate` at (0,1,0). A probe element
   with the class outside that row measured 92px while the real one
   measured 52 — the rule was working everywhere except where it was
   needed. Matching the specificity is the fix; flex had nothing to do
   with it, which cost me two guesses. */
.wpi-line-sub input.inv-i-qty  { width: 56px; flex: none; }
.wpi-line-sub input.inv-i-rate { width: 92px; flex: none; }

/* ── Due field in the invoice editor's field row ──
   .inv-mini is a flex row (an earlier _legacy.css rule) holding the "Due"
   label text, the mode <select>, and (only when mode is "date") a date
   <input> — all crammed into one 1fr grid column with every child also
   set to width:100%. Three flex items each demanding full width of a
   ~180px column has no good outcome; the date input was the one that
   lost, rendering half its digits clipped. Let it wrap onto its own line
   instead of fighting for the same line. */
#invDueRow {
  flex-wrap: wrap; row-gap: 4px; align-items: center;
}
#invDueRow select { flex: 1 1 auto; width: auto; min-width: 0; }
#invDueRow input[type="date"] { flex: 1 1 100%; width: auto; }

/* ═══════════════════════════════════════════════════════════════════
   PUBLIC PAGES — pay page (?inv=) and client portal (?client=)
   From public-pages.md. Always Warm Paper: these carry the sender's
   brand, so every value here is a literal, not a themed token — there
   is nothing for a board's data-theme to reach. Scoped by view id so
   they outrank the _legacy.css restyle (.pubinv-wrap … at 0,2,0).
   ═══════════════════════════════════════════════════════════════════ */
#publicInvoiceView, #publicClientView {
  --pub-ink: hsl(24 10% 10%); --pub-ink-2: hsl(24 8% 25%); --pub-ink-muted: hsl(24 8% 32%);
  --pub-ink-label: hsl(24 8% 38%); --pub-ink-soft: hsl(24 8% 42%);
  --pub-bg: hsl(34 20% 97%); --pub-panel: hsl(34 19% 95.5%); --pub-panel-footer: hsl(34 17% 93.5%); --pub-head-tint: hsl(34 16% 96%);
  --pub-navy: #1E3A5F; --pub-navy-hover: #16293F; --pub-navy-tint: hsl(215 40% 93%); --pub-navy-tint-border: hsl(215 40% 80%);
  --pub-navy-tint-hover: hsl(215 42% 89%); --pub-navy-tint-ink: #3D5F87;
  --pub-hairline: hsl(24 10% 10% / 0.06); --pub-hairline-strong: hsl(24 10% 10% / 0.08); --pub-rule: hsl(24 10% 10% / 0.1); --pub-rule-mid: hsl(24 10% 10% / 0.14);
  --pub-btn-shadow: 0 2px 6px hsl(215 52% 24% / 0.2), 0 10px 26px hsl(215 52% 24% / 0.18);
  --pub-btn-shadow-sm: 0 1px 2px hsl(215 52% 24% / 0.18), 0 5px 16px hsl(215 52% 24% / 0.16);
  background: var(--pub-bg); color: var(--pub-ink);
  font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-weight: 400;
}
#publicInvoiceView .micro, #publicClientView .micro {
  font-size: 11px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--pub-ink-label);
}
#publicInvoiceView .pubinv-wrap { max-width: 700px; margin: 0 auto; padding: 48px 20px 64px; }

/* ── 04 Pay page ── */
#publicInvoiceView .inv-sheet {
  background: #fff; border: 1px solid var(--pub-hairline); border-radius: 6px; padding: 40px 42px 38px;
  font-size: 14px; color: var(--pub-ink);
  box-shadow: 0 1px 2px hsl(24 10% 10% / 0.04), 0 10px 34px hsl(24 10% 10% / 0.06);
}
#publicInvoiceView .pubinv-nav { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-bottom: 14px; font-size: 12px; color: var(--pub-ink-label); }
/* The portal link is a small ghost button — findable without shouting.
   The sender's name reads as "…from Nam Chaudry": medium, not a title. */
#publicInvoiceView .pubinv-nav-back { font-size: 12px; font-weight: 500; color: var(--pub-ink-muted); text-decoration: none; }
#publicInvoiceView .pubinv-nav-biz { font-size: 14px; font-weight: 500; color: var(--pub-ink-2); }
/* "N other invoices are open" — the signal a slow payer actually needs,
   under the pay buttons, only when it's true. */
.pub-more {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-top: 14px; padding: 12px 14px;
  background: var(--pub-navy-tint); border: 1px solid var(--pub-navy-tint-border); border-radius: 6px;
  font-size: 13px; color: var(--pub-navy-tint-ink);
}
/* display:flex on the class beat the UA's [hidden]{display:none}, so the
   strip showed as an empty box on single-invoice clients. Explicit. */
.pub-more[hidden], .pub-more .btn-navy[hidden] { display: none; }
.pub-more__text { flex: 1; min-width: 200px; line-height: 1.5; }
.pub-more__text b { font-weight: 600; color: var(--pub-navy); }
.pub-more .btn-ghost { text-decoration: none; }
.pub-more .btn-navy { padding: 9px 14px; }
#publicInvoiceView .pubinv-banner {
  background: hsl(140 16% 95.5%); border: 1px solid hsl(140 20% 37% / 0.25); color: hsl(140 25% 25%);
  border-radius: 6px; padding: 12px 16px; font-size: 13.5px; font-weight: 400; margin-bottom: 14px;
}
#publicInvoiceView .pubinv-banner.err { background: hsl(8 26% 96%); border-color: hsl(8 45% 45% / 0.3); color: #A6412E; }

.pub-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 24px; }
.pub-logo { height: 72px; width: auto; mix-blend-mode: multiply; display: block; }
.pub-head__name { font-family: 'Times New Roman', Times, serif; font-size: 22px; line-height: 1.3; }
.pub-head__right { text-align: right; font-size: 13px; color: var(--pub-ink-soft); }
.pub-head__num { font-family: 'Times New Roman', Times, serif; font-size: 22px; line-height: 1.3; font-weight: 400; color: var(--pub-ink); }

.pub-parties { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px; margin-top: 34px; font-size: 14px; line-height: 1.6; }
.pub-parties .micro { margin-bottom: 2px; }
.pub-parties .muted { color: var(--pub-ink-soft); overflow-wrap: anywhere; }

.pub-table { border: 1px solid var(--pub-hairline); border-radius: 6px; margin-top: 32px; overflow: hidden; }
.pub-table__head, .pub-table__row { display: grid; grid-template-columns: minmax(0, 1fr) 60px 108px 116px; gap: 0 14px; padding: 10px 18px; }
.pub-table__head { background: var(--pub-head-tint); font-size: 10.5px; font-weight: 600; letter-spacing: 0.09em; text-transform: uppercase; color: var(--pub-ink-label); }
.pub-table__row { padding: 14px 18px; font-size: 14px; border-top: 1px solid var(--pub-hairline); }
.pub-table__row > div:first-child { overflow-wrap: anywhere; }
.pub-table__row > div:not(:first-child), .pub-table__head > div:not(:first-child) { text-align: right; }
.pub-table__sub { display: grid; grid-template-columns: minmax(0, 1fr) 116px; gap: 0 14px; padding: 11px 18px; font-size: 13px; color: var(--pub-ink-soft); border-top: 1px solid var(--pub-hairline); text-align: right; }

.pub-totals { display: flex; justify-content: flex-end; margin-top: 26px; }
.pub-totals__block { width: 290px; max-width: 100%; }
.pub-totals__row { display: flex; justify-content: space-between; gap: 16px; font-size: 13px; font-weight: 400; color: var(--pub-ink-label); padding: 4px 0; }
.pub-totals__due { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; border-top: 1px solid var(--pub-rule-mid); margin-top: 8px; padding-top: 10px; }
.pub-totals__due .label { font-size: 11px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: var(--pub-ink); }
.pub-totals__due .figure { font-size: 34px; line-height: 1.05; font-weight: 400; color: var(--pub-navy); white-space: nowrap; }
.pub-note { margin-top: 18px; font-size: 13px; line-height: 1.6; color: var(--pub-ink-2); white-space: pre-line; }
.pub-note--muted { margin-top: 8px; font-size: 12px; color: var(--pub-ink-label); }

#publicInvoiceView .pubinv-paywrap { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 26px; text-align: left; }
#publicInvoiceView .pubinv-paywrap.one { grid-template-columns: 1fr; }
#publicInvoiceView .pubinv-pay {
  border: 0; border-radius: 6px; text-align: center; padding: 15px 16px; font: inherit; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: 2px; min-width: 0; margin: 0;
}
#publicInvoiceView .pubinv-pay .l1 { font-size: 15px; font-weight: 500; }
#publicInvoiceView .pubinv-pay .l2 { font-size: 12px; font-weight: 400; }
#publicInvoiceView .pubinv-pay:disabled { cursor: default; opacity: 0.8; }
#pubinvPayBankBtn { background: var(--pub-navy); color: #fff; box-shadow: var(--pub-btn-shadow); }
#pubinvPayBankBtn:hover { background: var(--pub-navy-hover); }
#pubinvPayBankBtn .l2 { color: hsl(215 45% 82%); }
#pubinvPayBtn { background: var(--pub-navy-tint); border: 1px solid var(--pub-navy-tint-border); color: var(--pub-navy); }
#pubinvPayBtn:hover { background: var(--pub-navy-tint-hover); }
#pubinvPayBtn .l2 { color: var(--pub-navy-tint-ink); }

.pub-links { display: flex; justify-content: center; align-items: baseline; gap: 14px; font-size: 12px; color: var(--pub-ink-label); margin-top: 14px; }
.pub-links a, .pub-links button { font: inherit; font-size: 12px; color: var(--pub-navy); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none; }
.pub-links a:hover, .pub-links button:hover { color: var(--pub-navy-hover); text-decoration: underline; }
#publicInvoiceView .pubinv-dlhint { margin: 8px auto 0; font-size: 12px; color: var(--pub-ink-label); text-align: center; }

.pub-foot { display: flex; align-items: flex-start; gap: 8px; border-top: 1px solid var(--pub-hairline); margin-top: 30px; padding-top: 16px; font-size: 12px; line-height: 1.6; color: var(--pub-ink-label); text-align: left; }
.pub-foot svg { width: 12px; height: 12px; margin-top: 3px; flex: none; stroke: hsl(24 8% 45%); stroke-width: 1.8; }
.pub-foot b { font-weight: 600; font-size: 13.5px; color: var(--pub-ink-2); }
.pub-foot a { color: var(--pub-navy); text-decoration: none; }
.pub-foot a:hover { text-decoration: underline; }

@media (max-width: 560px) {
  #publicInvoiceView .inv-sheet { padding: 28px 22px 26px; }
  .pub-table__head, .pub-table__row { grid-template-columns: minmax(0, 1fr) 96px; }
  .pub-table .qty, .pub-table .rate { display: none; }
  #publicInvoiceView .pubinv-paywrap { grid-template-columns: 1fr; }
}

/* ── 05 Client portal ── */
#publicClientView .pubcl-wrap { max-width: 760px; margin: 0 auto; padding: 48px 20px 64px; background: none; border-radius: 0; overflow: visible; }
.pubcl-card {
  background: var(--pub-panel); border: 1px solid var(--pub-hairline); border-radius: 6px; overflow: hidden;
  box-shadow: 0 2px 4px hsl(24 10% 10% / 0.06), 0 12px 30px hsl(24 10% 10% / 0.08);
}
#publicClientView .pubcl-head {
  background: #fff; border-bottom: 1px solid var(--pub-rule); padding: 26px 28px 24px; margin: 0;
  display: flex; align-items: flex-start; gap: 24px; flex-wrap: wrap;
}
.pubcl-head__id { flex: 1; min-width: 200px; }
#publicClientView .pubcl-logo { height: 64px; max-height: none; max-width: none; width: auto; mix-blend-mode: multiply; display: block; margin: 0; }
.pubcl-title { font-family: 'Times New Roman', Times, serif; font-size: 26px; line-height: 1.2; font-weight: 400; color: var(--pub-ink); margin-top: 14px; }
.pubcl-sub { font-size: 13px; color: var(--pub-ink-label); margin-top: 4px; }
#publicClientView .pubcl-balance { text-align: right; font-size: inherit; font-weight: 400; color: inherit; }
.pubcl-balance__figure { font-size: 36px; line-height: 1.05; font-weight: 400; color: var(--pub-navy); white-space: nowrap; margin-top: 2px; }
.btn-navy {
  display: inline-flex; align-items: center; gap: 6px; background: var(--pub-navy); color: #fff; border: 0; border-radius: 6px;
  box-shadow: var(--pub-btn-shadow-sm); padding: 11px 18px; font: inherit; font-size: 12.5px; font-weight: 500; cursor: pointer; text-decoration: none;
}
.btn-navy:hover { background: var(--pub-navy-hover); }
.btn-navy:disabled { cursor: default; opacity: 0.8; }
#pubclPayAllBtn { margin-top: 10px; }

.pubcl-body { padding: 22px 28px 26px; }
#publicClientView .pubcl-sec { padding: 0; margin: 0; }
#publicClientView .pubcl-sec-head {
  display: flex; align-items: center; gap: 10px; padding-bottom: 8px; margin: 0;
  border-bottom: 1px solid var(--pub-rule); font-size: 12.5px; color: var(--pub-ink); letter-spacing: 0; text-transform: none; font-weight: 400;
}
#publicClientView .pubcl-sec-head .name { font-size: 12.5px; font-weight: 800; letter-spacing: 0.09em; text-transform: uppercase; color: var(--pub-ink); }
#pubclPaidSec .pubcl-sec-head .name { color: var(--pub-ink-label); }
#publicClientView .pubcl-sec-head .count { font-size: 12px; color: var(--pub-ink-label); }
#publicClientView .pubcl-sec-head .aside { margin-left: auto; font-size: 13px; color: var(--pub-ink-label); }
#pubclPaidSec .pubcl-sec-head { margin-top: 28px; }

#publicClientView .pubcl-row--open {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap; background: #fff; border: 1px solid var(--pub-hairline-strong); border-radius: 6px;
  box-shadow: 0 1px 2px hsl(24 10% 10% / 0.04), 0 4px 14px hsl(24 10% 10% / 0.04); padding: 15px 18px; margin-top: 10px;
}
#publicClientView .pubcl-row-main { flex: 1; min-width: 150px; }
#publicClientView .pubcl-row-num { font-size: 15px; font-weight: 400; color: var(--pub-ink); display: flex; align-items: center; gap: 7px; }
#publicClientView .pubcl-row-num .recur { width: 13px; height: 13px; stroke: hsl(24 8% 50%); flex: none; }
#publicClientView .pubcl-row-sub { font-size: 12px; color: var(--pub-ink-label); margin-top: 1px; }
#publicClientView .pubcl-row-sub .overdue { color: #A6412E; }
#publicClientView .pubcl-row--open .pubcl-row-amt { font-size: 20px; font-weight: 400; color: var(--pub-ink); white-space: nowrap; }
#publicClientView .pubcl-row--open .btn-navy { padding: 10px 18px; }
#publicClientView .pubcl-row--paid {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap; background: none; border: 0; border-radius: 0; box-shadow: none;
  border-bottom: 1px solid var(--pub-hairline-strong); padding: 12px 4px; margin: 0;
}
#publicClientView .pubcl-row--paid .pubcl-row-num { font-size: 14px; }
#publicClientView .pubcl-row--paid .pubcl-row-amt { font-size: 16px; font-weight: 400; color: var(--pub-ink-soft); white-space: nowrap; }
#publicClientView .pubcl-row--paid a { font-size: 12px; color: var(--pub-navy); text-decoration: none; }
#publicClientView .pubcl-row--paid a:hover { text-decoration: underline; }
#publicClientView .pubcl-empty {
  font-size: 13.5px; color: var(--pub-ink-label); text-align: center; padding: 28px; margin-top: 10px;
  border: 1px dashed hsl(24 10% 10% / 0.14); border-radius: 6px;
}

.pubcl-actions { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 24px; }
.btn-ghost {
  display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--pub-hairline-strong); border-radius: 6px;
  padding: 7px 13px; font: inherit; font-size: 12px; font-weight: 500; color: var(--pub-ink-muted); cursor: pointer;
}
.btn-ghost:hover { border-color: hsl(24 10% 10% / 0.2); }
.pubcl-actions .note { font-size: 12px; color: var(--pub-ink-label); }
#publicClientView .pubinv-dlhint { margin-top: 8px; font-size: 12px; color: var(--pub-ink-label); }
.pubcl-foot { border-top: 1px solid var(--pub-rule); background: var(--pub-panel-footer); padding: 13px 28px; font-size: 12px; color: var(--pub-ink-label); }
@media (max-width: 560px) { #publicClientView .pubcl-head, .pubcl-body { padding-left: 18px; padding-right: 18px; } .pubcl-foot { padding-left: 18px; padding-right: 18px; } }

/* ═══ Universal mobile chat bar (2026-09-09) ═══════════════════════════
   One treatment for EVERY .message-composer — task modal, project
   window, Product Room, collection window. WhatsApp when idle:
   [+] [pill] [mic]. Slack when engaged (focus, text, or staged
   attachments): pill full-width on top, the options row appears
   beneath it, send pinned right. Supersedes the three per-surface
   id-list passes above and in _legacy.css — their id hacks are
   explicitly neutralised here. The desktop composer is untouched. */
@media (max-width: 1023px) {
  .message-composer {
    display: flex !important; flex-wrap: wrap; align-items: center;
    gap: 6px 8px; position: relative;
    margin: 0 !important; border: 0 !important;
    border-top: 1px solid var(--rule) !important;
    border-radius: 0 !important; box-shadow: none !important;
    background: var(--surface);
    padding: 8px 10px calc(8px + env(safe-area-inset-bottom)) 10px !important;
  }
  .message-composer .composer-toolbar,
  .message-composer .reply-preview,
  .message-composer .voice-rec,
  .message-composer .attachment-strip { flex-basis: 100%; order: 0; }
  .message-composer .composer-footer { display: contents !important; }
  .message-composer .composer-actions { display: contents !important; }

  /* Neutralise the older absolute-position emoji pills. */
  .message-composer #emojiBtn,
  .message-composer #tmEmojiBtn,
  .message-composer #pdpjEmojiBtn {
    position: static !important; right: auto !important; bottom: auto !important;
    opacity: 1;
  }

  /* Uniform round buttons. */
  .message-composer .composer-actions .fmt-btn {
    width: 36px; height: 36px; border-radius: 50%; flex: none;
    align-items: center; justify-content: center; margin: 0;
  }

  /* Idle: attach + pill + mic. Everything else waits for engagement. */
  .message-composer .composer-actions .fmt-btn { display: none; }
  .message-composer .composer-actions .fmt-btn:first-child { display: flex; order: 1; }
  .message-composer .message-input {
    order: 2; flex: 1 1 0%; min-width: 0; margin: 0;
    background: var(--panel); border: 1px solid var(--hairline);
    border-radius: 999px; padding: 9px 14px; min-height: 40px;
  }
  .message-composer .composer-actions .voice-btn { display: flex; order: 3; }
  .message-composer .send-btn {
    display: none; order: 3; width: 38px; height: 38px; flex: none;
    border-radius: 50%; background: var(--accent); color: var(--surface);
    align-items: center; justify-content: center;
  }
  /* Desktop affordances a phone never needs. */
  .message-composer .composer-footer label { display: none !important; }
  .message-composer .composer-actions .dictate-btn { display: none !important; }

  /* Engaged: pill on top, options row under it, send right. */
  .message-composer.is-expanded .message-input { flex-basis: 100%; order: 1; border-radius: 18px; }
  .message-composer.is-expanded .composer-actions .fmt-btn:not(.dictate-btn) { display: flex; order: 2; }
  .message-composer.is-expanded .composer-actions .voice-btn { order: 2; }
  .message-composer.is-expanded .send-btn { display: flex; margin-left: auto; }
  /* Older passes hid these by id with !important — bring them back
     when engaged, same weapon. */
  .message-composer.is-expanded #toggleToolbarBtn,
  .message-composer.is-expanded #tmToggleToolbarBtn,
  .message-composer.is-expanded #pdToggleToolbarBtn,
  .message-composer.is-expanded #pdpjToggleToolbarBtn,
  .message-composer.is-expanded #mentionBtn,
  .message-composer.is-expanded #tmMentionBtn,
  .message-composer.is-expanded #pdMentionBtn,
  .message-composer.is-expanded #pdpjMentionBtn,
  .message-composer.is-expanded #emojiBtn,
  .message-composer.is-expanded #tmEmojiBtn,
  .message-composer.is-expanded #pdEmojiBtn,
  .message-composer.is-expanded #pdpjEmojiBtn { display: flex !important; }
  /* The Aa toolbar may open while engaged. */
  .message-composer.is-expanded .composer-toolbar:not(.hidden) { display: flex !important; }
}
