/* ============================================================================
   KH OS — components.css
   2026-08-04. Load AFTER styles.css and tokens.css.

   READ THIS FIRST — the visual delta is empty.

   I re-diffed the prototype against the re-pinned kh-os-LIVE. There are no
   remaining visual differences I can defend. Everything from the 23-row list in
   VISUAL_DELTA.md is now in the shipped CSS, and the four gaps I raised last
   round are closed on your side:

     .lrow 7-track grid            done (branch)
     .sc-health segment classes    done (branch)
     view-switcher collapse        shipped, breakpoint 1379 — verified
     dashed empty-state panel      shipped, .empty-note.empty-panel:502 — verified

   Also verified present and matching the prototype, so not respecified here:
   .x-close (32px bare ✕, X1) · .sgroup .lrows indent rail (3px --line-soft,
   14px) · .meet-time 74px centred · .meet-card.is-marker at .72 opacity ·
   .pn-cards repeat(auto-fill,minmax(268px,1fr)) · the .tt-head/.tt-row track
   list minmax(0,1fr) 170px 96px 80px 40px · .chip tier inversions ·
   .lrow.od inset bar · .lrow-add · 34px avatars with the .unassigned ＋.

   So this file does not restyle anything. Every rule below swaps a hard-coded
   literal for the matching token from tokens.css, at the identical value. That
   is the mechanism that makes tokens.css actually bite: without it, a change to
   --tag-ink or --font-head has no consumer and does nothing.

   Each rule is a like-for-like substitution. Computed output is byte-identical
   to what ships today — this should be a no-op in the browser and a real change
   in how the next edit works. If any rule renders differently, that is a bug in
   my substitution, not an intended change; tell me and I will fix it.

   Selectors are the shipped ones. No new markup, no new class names.
   ============================================================================ */


/* ── Type ────────────────────────────────────────────────────────────────────
   Replaces the literal stacks at styles.css:37-38. Same stacks. */

body{
  font-family:var(--font-ui);
  font-size:var(--ui-size);
  line-height:var(--ui-line);
}

h1,h2,h3,
.login-card h1,
.rail-title,
.login-wordmark,
.sc-name,
.greet-h1,
.bh-line{
  font-family:var(--font-head);
}


/* ── Empty panels ────────────────────────────────────────────────────────────
   X4: one dashed panel border. --line stays on .lrow-add, which is a control. */

.empty-note.empty-panel{
  border-color:var(--line-dash);
}


/* ── Done and overdue ────────────────────────────────────────────────────────
   One "done" ink and one overdue tint across list, board and table. */

.lrow.done .l-title,
.tt-row.done .l-title{
  color:var(--ink-done);
}

.lrow.od,
.tt-row.od{
  background:var(--od-tint);
}

.lrow.od{
  box-shadow:inset 3px 0 0 var(--st-stuck);
}


/* ── Chips ───────────────────────────────────────────────────────────────────
   Same tints, now addressable. */

.chip.due{
  background:var(--due-bg);
  color:var(--due-ink);
}

.chip.overdue{
  background:var(--od-chip-bg);
  color:var(--od-chip-ink);
}

.chip.tag{
  background:var(--tag-bg);
  color:var(--tag-ink);
}

.chip.tag:hover{
  background:var(--tag-bg-hover);
}

.chip.tag.t-private{
  background:var(--tier-private-bg);
  color:var(--tier-private-ink);
}

.chip.tag.t-public{
  background:var(--tier-public-bg);
  color:var(--tier-public-ink);
}


/* ── Avatars ─────────────────────────────────────────────────────────────────
   X2: one size, plus the table's documented exception. Setting .avatar from the
   token means the table override is the only place a second number appears. */

.avatar{
  width:var(--avatar);
  height:var(--avatar);
  flex-basis:var(--avatar);
}

.tt-row .avatar,
.tt-head .avatar{
  width:var(--avatar-sm);
  height:var(--avatar-sm);
  flex-basis:var(--avatar-sm);
}


/* ── Status pill ─────────────────────────────────────────────────────────────
   The pill width and the row's status track are the same measurement and should
   read from one number. The row branch now uses a 100px track; if the pill is
   meant to fill it, set --pill-w:100px in tokens.css and both follow. Left at
   104px here so this file changes nothing on its own. */

.status-pill{
  width:var(--pill-w);
  flex-basis:var(--pill-w);
}


/* ── At-risk ring ────────────────────────────────────────────────────────────
   Your rebuild softened this ring today; whatever value it landed on, put it in
   --at-risk and both consumers follow. */

.store-card.at-risk{
  border-color:var(--at-risk);
}

.bh-pill.bh-red{
  border-color:var(--at-risk);
}


/* ── Press ───────────────────────────────────────────────────────────────────
   DESIGN.md's scale(.96), from one place. */

.btn:active,
.fchip:active,
.persona:active,
.store-card:active{
  transform:var(--press);
}


/* ── Not in this file, deliberately ──────────────────────────────────────────

   Relative due words (Today / Tomorrow / 3d / 2d late / Today · 2:30pm).
   Text, not style — it belongs in the due-label function in app.js. The chip
   treatment above is unchanged either way.

   Tasks Timeline (gantt) bars. Still blocked on the data decision: there is no
   start-date field, so the prototype derives bar length from priority, which is
   not a shippable rule. Nothing to hand over until tasks get a real `start`.

   Nothing structurally new. Store cards, the row grid and the one-click
   complete control all landed on your side, so there is no new panel or column
   for me to ship as a render function this round. The Tasks renderers from last
   round are still at renderers/tasks.js if the row work wants a reference.
   ============================================================================ */


/* ══ v2.1 DOCUMENTS EDITOR ═══════════════════════════════════════════════
   Design handoff v2.1 (2026-08-06). Rules against class names already in
   styles.css:360-474. Tokens are section 4 of tokens.css.

   ⚠ `.tt-` NAMES TWO UNRELATED THINGS. Editor set: .tt-toolbar .ttb .tt-sep
   .ttsel .tt-canvas .tt-page .tt-panel .tt-comment .tt-words .tt-av.
   Tasks TABLE set: .tt-card .tt-head .tt-row .tt-task .tt-store .tt-status
   .tt-due .tt-owner .tt-grip .tt-badge (styles.css:1880-1917). Nothing here
   touches the second set — a .tt-row rule written for the editor would
   restyle every Tasks table in the app.
   ════════════════════════════════════════════════════════════════════════ */

/* ── 1. Canvas: measure and type scale ─────────────────────────────────────
   The one change a reader feels first. .tt-page is a letter sheet today
   (8.5in wide, 1in pad, 11pt/1.65); the line runs about 105 characters, which
   is why an SOP reads as a wall. 700px at 14.5/1.72 is ~75 characters. The
   sheet keeps its paper metaphor — white, shadowed, top-anchored — it just
   stops pretending to be print. */

.tt-canvas{ background:var(--doc-canvas-bg); }

.tt-page{
  width:var(--doc-measure);
  max-width:calc(100% - 32px);
  min-height:0;                       /* was 11in — a short doc drew a page of blank */
  margin:var(--doc-page-top) auto 0;
  padding:var(--doc-page-pad-t) var(--doc-page-pad-x) 0;
  border-radius:var(--doc-page-r) var(--doc-page-r) 0 0;
  box-shadow:var(--doc-page-shadow);
}

.tt-page .ProseMirror{
  min-height:0;                       /* was 9in */
  font-size:var(--doc-body);
  line-height:var(--doc-body-line);
  color:var(--doc-ink);
  text-wrap:pretty;
}
.tt-page .ProseMirror p{ margin:0 0 14px; }
.tt-page .ProseMirror h1{
  font-family:var(--font-head);
  font-size:var(--doc-h1); font-weight:700;
  letter-spacing:-.015em; line-height:1.2;
  margin:0 0 10px;
}
.tt-page .ProseMirror h2,
.tt-page .ProseMirror h3{
  font-family:var(--font-head);
  font-size:var(--doc-h2); font-weight:600;
  margin:24px 0 8px;
}
.tt-page .ProseMirror h4{ font-size:var(--t-body); font-weight:700; margin:18px 0 6px; }

/* Checklist rows get a touch target and breathing room. The taskList markup
   is TipTap's own and is already in the DOM. */
.tt-page .ProseMirror ul[data-type="taskList"] li{ gap:10px; padding:2px 0; }
.tt-page .ProseMirror ul[data-type="taskList"] li>label{ margin-top:3px; }
.tt-page .ProseMirror ul[data-type="taskList"] li[data-checked="true"]>div{
  color:var(--ink-done); text-decoration:line-through;
}


/* ── 2. Callout panels: a left rule instead of an emoji ────────────────────
   styles.css:395-399 sets ::before to ℹ️ 📝 ✅ ⚠️ and pads 40px to clear
   them. The emoji render differently on Windows, Android and macOS, they
   carry the platform's own colour, and they read as decoration. The kind is
   carried by a 3px rule in the same colour the fill already implies. Fills
   and borders below are unchanged from styles.css:391-394.

   The stroked Lucide icon that sits inline in the prototype is markup — see
   section 5. This rule set is correct with or without it. */

.tt-page .ProseMirror .tt-panel,
.hist-doc .tt-panel{
  padding:var(--panel-pad);
  border-radius:var(--panel-r);
  border-left-width:var(--panel-rule-w);
  border-left-style:solid;
}
.tt-panel::before{ content:none; }

.tt-panel[data-kind="info"]   { border-left-color:var(--panel-info-rule); }
.tt-panel[data-kind="note"]   { border-left-color:var(--panel-note-rule); }
.tt-panel[data-kind="success"]{ border-left-color:var(--panel-success-rule); }
.tt-panel[data-kind="warn"]   { border-left-color:var(--panel-warn-rule); }


/* ── 3. Toolbar: quieter, grouped by the separator ─────────────────────────
   The full redesign groups the buttons into segmented trays, which needs
   wrapper elements (section 5). What is achievable against the shipped class
   names is the skin: a lighter idle button, a real pressed state, and a
   separator that actually separates. */

.tt-toolbar{
  min-height:var(--doc-toolbar-h);
  gap:4px;
  border-radius:var(--doc-group-r);
  flex-wrap:nowrap; overflow-x:auto;   /* wrapping to a second row moved the canvas */
  scrollbar-width:none;
}
.tt-toolbar::-webkit-scrollbar{ display:none; }

.ttb{
  min-width:var(--doc-ctl-w);
  height:var(--doc-ctl-h);
  border-radius:var(--doc-ctl-r);
  color:var(--ink-soft);
  transition:background var(--m-fast) ease, color var(--m-fast) ease;
}
.ttb:hover{ background:var(--doc-group-bg); border-color:transparent; }
.ttb:active{ transform:var(--press); }
.ttb.on{
  background:#fff;
  color:var(--blue-mid);
  border-color:transparent;
  box-shadow:var(--doc-ctl-shadow);
}
.ttb:focus-visible{ outline:2px solid var(--cyan); outline-offset:1px; }

.tt-sep{ height:22px; background:var(--line-soft); margin:0 6px; }
.ttsel{ height:var(--doc-ctl-h); border-color:var(--line-soft); background:var(--doc-group-bg); }
.tt-words{ font-size:var(--doc-meta); margin-left:auto; }

.tt-av{ width:28px; height:28px; margin-left:-8px; }


/* ── 4. Comment highlight, quote echo, acknowledgment pill ─────────────────
   Literal-for-token substitution. Intended as a visual no-op — the point is
   that the highlight in the page and its echo in the rail are now one pair of
   values and cannot drift apart. */

.tt-page .ProseMirror .tt-comment{
  background:var(--cmt-hl-bg);
  border-bottom:2px solid var(--cmt-hl-rule);
}
.cmt-quote{
  background:var(--cmt-quote-bg);
  color:var(--cmt-quote-ink);
  border-left:var(--panel-rule-w) solid var(--cmt-hl-rule);
}
.cmt-card{ border-radius:12px; padding:12px; }
.cmt-card:hover{ border-color:var(--line); box-shadow:var(--shadow); }

.rc-ack{ background:var(--ok-bg); border-color:var(--ok-line); color:var(--ok-ink); }

/* The overlay comments panel keeps working unchanged. Below the rail
   breakpoint it stays an overlay by design — this only trims the width so it
   matches the docked rail rather than being 80px wider for no reason. */
.cmt-panel{ width:min(var(--doc-rail-w), 92vw); }
.hist-side{ width:min(var(--doc-rail-w), 88vw); }

/* The modal editor shell. The canvas needs the width more than the backdrop
   does. */
.doc-card{ max-width:1180px; }

/* ── 2b. Specificity corrections against the SHIPPED rules ─────────────────
   Found by running it, not by reading it. The pack's callout rules are written
   as bare `.tt-panel[data-kind]` (0,2,0) and `.tt-panel::before` (0,1,1), but
   styles.css beats both:
     :420  .tt-page .ProseMirror .tt-panel { border:1px solid transparent }  (0,3,0)
     :428  .tt-panel[data-kind="info"]::before { content:"ℹ️" }              (0,2,1)
   so the left rule rendered transparent and every emoji stayed on screen.
   These restate the same intent at the shipped specificity. */

.tt-page .ProseMirror .tt-panel[data-kind="info"],
.hist-doc .tt-panel[data-kind="info"]   { border-left-color:var(--panel-info-rule); }
.tt-page .ProseMirror .tt-panel[data-kind="note"],
.hist-doc .tt-panel[data-kind="note"]   { border-left-color:var(--panel-note-rule); }
.tt-page .ProseMirror .tt-panel[data-kind="success"],
.hist-doc .tt-panel[data-kind="success"]{ border-left-color:var(--panel-success-rule); }
.tt-page .ProseMirror .tt-panel[data-kind="warn"],
.hist-doc .tt-panel[data-kind="warn"]   { border-left-color:var(--panel-warn-rule); }

.tt-panel[data-kind="info"]::before,
.tt-panel[data-kind="note"]::before,
.tt-panel[data-kind="success"]::before,
.tt-panel[data-kind="warn"]::before{ content:none; }

/* The pack sets .tt-toolbar{flex-wrap:nowrap}, but styles.css:502 sets
   .ttg{flex-wrap:wrap} globally — the groups would still wrap inside a
   no-wrap bar. The shipped comment there ("controls never squeeze") is
   answered by the pack's overflow-x:auto instead. */
.tt-toolbar .ttg{ flex-wrap:nowrap; }


/* ============================================================================
   v2.2 — FORMS BUILDER.  Ported from design_handoff v2.2-forms-builder.
   Section 2's .fb- rules are consumed by formsbuilder.js. Extra rules for the
   controls that differ from the handoff are in section 4 at the foot.
   ============================================================================ */

/* ── 1. DELTA AGAINST SHIPPED CLASS NAMES ──────────────────────────────────
   Everything this round could do to the existing forms surfaces. Four rules.
   Listed in full rather than padded out. */

/* The forms list row uses 🧩 as its icon (app.js:17701). Every other module's
   doc-ic carries a glyph or a brand mark; the puzzle piece is the only emoji
   in a .doc-ic anywhere in the app and it renders four different ways across
   the store iPads. A stroked square is the same silhouette at every size. */
.doc-row .doc-ic.form-ic{
  font-size:0;
  background:var(--tag-bg);
  color:var(--tag-ink);
}
.doc-row .doc-ic.form-ic::before{
  content:"";
  display:block;
  width:15px;height:15px;
  border:1.75px solid currentColor;
  border-radius:3px;
  box-shadow:inset 0 -5px 0 -3.5px currentColor;
}

/* The three-button cluster at the end of a forms row (Fill out / Edit /
   Delete) wraps under 420px because it is an inline flex with no min-width on
   the row's meta column. Phone only. */
@media (max-width:760px){
  .doc-row{flex-wrap:wrap;row-gap:10px}
  .doc-row .doc-meta{flex:1 1 100%;min-width:0}
  .doc-row .doc-meta + div{flex:1 1 100%;justify-content:flex-end}
  .doc-row .btn{min-height:var(--tap-min)}
}

/* Token substitution, no visual change: the forms empty note and the modal's
   dashed borders are literals today. */
.empty-note{color:var(--muted)}


/* ── 2. THE BUILDER — new view, new class names ────────────────────────────
   Consumed by renderers/formsbuilder.js. Prefix is .fb-. Nothing below
   collides with a shipped selector; the closest neighbours are .fld and
   .fchip in the modal builder, which stay exactly as they are until the
   modal is retired.

   The renderers also write these values inline so a fragment renders on its
   own. Where both exist this sheet wins once it is linked — keep the two in
   step or delete the inline copy after the port. */

.fb{
  position:fixed;
  inset:var(--appbar-h) 0 0 0;
  display:flex;
  flex-direction:column;
  background:var(--bg);
  font-family:var(--font-ui);
}

/* Form bar — back, title, view switcher, Cancel / Publish. */
.fb-bar{
  height:var(--fb-bar-h);
  flex:0 0 var(--fb-bar-h);
  display:flex;align-items:center;gap:12px;
  padding:0 18px;
  background:var(--card);
  border-bottom:1px solid var(--line-soft);
}
.fb-bar-title{
  font-family:var(--font-head);
  font-size:var(--t-headline);font-weight:600;color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.fb-bar-sub{font-size:var(--t-caption);color:var(--muted);margin-top:1px}

.fb-switch{
  display:flex;gap:2px;padding:3px;
  background:var(--bg);
  border:1px solid var(--line-soft);
  border-radius:11px;
}
.fb-switch button{
  border:none;background:none;
  font:inherit;font-size:var(--t-caption);font-weight:700;
  color:var(--muted);
  border-radius:9px;padding:7px 15px;cursor:pointer;
  transition:background var(--m-fast) ease,color var(--m-fast) ease;
}
.fb-switch button:hover{color:var(--ink)}
.fb-switch button[aria-pressed="true"]{
  background:var(--card);color:var(--blue-mid);
  box-shadow:0 1px 3px rgba(8,26,102,.14);
}

/* Body — the three regions. */
.fb-body{flex:1;min-height:0;display:flex;align-items:stretch}

.fb-palette{
  flex:0 0 var(--fb-palette-w);
  border-right:1px solid var(--line-soft);
  background:var(--card);
  display:flex;flex-direction:column;
  overflow:auto;
}
.fb-palette-h{
  padding:16px 16px 10px;
  font-size:var(--t-caption2);font-weight:800;letter-spacing:.12em;
  text-transform:uppercase;color:var(--muted);
}
.fb-tiles{
  padding:0 12px 8px;
  display:grid;grid-template-columns:1fr 1fr;gap:var(--fb-tile-gap);
}
.fb-tile{
  display:flex;flex-direction:column;align-items:flex-start;gap:5px;
  border:1px solid var(--line-soft);
  background:var(--card);
  border-radius:var(--fb-tile-r);
  padding:var(--fb-tile-pad);
  font:inherit;text-align:left;cursor:pointer;
  transition:border-color var(--m-med) ease,background var(--m-med) ease,transform var(--m-med) ease;
}
.fb-tile:hover{
  border-color:var(--cyan);
  background:rgba(55,189,245,.06);
  transform:translateY(-1px);
}
.fb-tile:active{transform:var(--press)}
.fb-tile-ic{font-size:var(--t-body);line-height:1}
.fb-tile-l{font-size:var(--t-caption);font-weight:600;color:var(--ink);line-height:1.25}

/* Canvas. --fb-canvas-min is load-bearing: this column never goes below it.
   Everything else gives way first. */
.fb-canvas{
  flex:1;min-width:0;
  overflow:auto;
  padding:26px 0 60px;
}
.fb-sheet{
  max-width:var(--fb-canvas-max);
  min-width:0;
  margin:0 auto;
  padding:0 var(--fb-canvas-pad-x);
}

.fb-head-card{
  background:var(--card);
  border:1px solid var(--line-soft);
  border-top:4px solid var(--blue-mid);
  border-radius:var(--r);
  box-shadow:var(--shadow);
  padding:22px 24px;
  margin-bottom:14px;
}
.fb-head-t{
  font-family:var(--font-head);
  font-size:var(--t-title2);font-weight:600;color:var(--ink);line-height:1.3;
}
.fb-head-d{font-size:var(--t-footnote);color:var(--muted);margin-top:7px;line-height:1.55}

/* Field card. Selected is the editing surface. */
.fb-card{
  background:var(--card);
  border:1px solid var(--line-soft);
  border-radius:var(--fb-card-r);
  box-shadow:var(--shadow);
  padding:var(--fb-card-pad);
  margin-bottom:var(--fb-card-gap);
  cursor:pointer;
  transition:box-shadow var(--m-med) ease,border-color var(--m-med) ease;
}
.fb-card:hover{border-color:var(--line)}
.fb-card.sel{
  border-color:var(--fb-card-sel-line);
  box-shadow:var(--fb-card-sel-shadow);
}
.fb-card-grip{
  flex:0 0 16px;
  color:var(--fb-grip-ink);
  font-size:var(--t-footnote);line-height:1.5;
  cursor:grab;user-select:none;
  touch-action:none;   /* pointer drag (wireRowDrag) — a finger must not scroll */
}
.fb-card-grip:active{cursor:grabbing}
.fb-card.dragging{opacity:.85;box-shadow:0 4px 14px rgba(10,20,60,.22)}
.fb-card-q{font-size:var(--t-footnote);font-weight:700;color:var(--ink)}
.fb-card-req{color:var(--st-stuck);font-weight:800;font-size:var(--t-footnote)}
.fb-card-help{font-size:var(--t-caption);color:var(--muted);margin-top:3px}
.fb-card-cond{
  font-size:var(--t-caption2);font-weight:700;letter-spacing:.03em;
  padding:2.5px 7px;border-radius:5px;
  background:var(--due-bg);color:var(--due-ink);
}
.fb-card-acts{display:flex;gap:5px;flex:0 0 auto;opacity:.35;transition:opacity var(--m-med) ease}
.fb-card:hover .fb-card-acts,.fb-card.sel .fb-card-acts{opacity:1}
.fb-card-acts button{
  border:1px solid var(--line-soft);background:var(--card);
  color:var(--muted);border-radius:8px;
  width:28px;height:28px;font-size:var(--t-caption);cursor:pointer;
}
.fb-card-acts button:hover{border-color:var(--cyan);color:var(--blue-mid)}
.fb-card-acts button.danger:hover{border-color:var(--st-stuck);color:var(--st-stuck)}

/* Control previews. Shapes, not inputs — nothing here is focusable. */
.fb-prev{margin-top:9px}
.fb-prev-input,.fb-prev-select{
  height:var(--fb-ctl-h);
  border:1px solid var(--line);border-radius:var(--fb-ctl-r);
  background-color:var(--card);
  display:flex;align-items:center;padding:0 12px;
  font-size:var(--t-footnote);color:var(--fb-ph-ink);
}
.fb-prev-select{justify-content:space-between}
.fb-prev-area{
  height:var(--fb-ctl-area-h);
  border:1px solid var(--line);border-radius:var(--fb-ctl-r);
  background:var(--card);padding:10px 12px;
  font-size:var(--t-footnote);color:var(--fb-ph-ink);
}
.fb-prev-chips{display:flex;gap:7px;flex-wrap:wrap}
.fb-prev-chip{
  font-size:var(--t-caption);font-weight:600;color:var(--ink-soft);
  border:1px solid var(--line);background:var(--card);
  border-radius:99px;padding:6px 13px;
}
.fb-prev-drop{
  height:var(--fb-drop-h);
  border:1px dashed var(--line-dash);border-radius:var(--fb-ctl-r);
  background:#fbfcfe;
  display:flex;align-items:center;justify-content:center;gap:7px;
  font-size:var(--t-caption);color:var(--fb-ph-ink);
}

.fb-add{
  width:100%;margin-top:6px;
  border:1px dashed var(--line-dash);background:var(--card);
  border-radius:12px;padding:15px;
  font:inherit;font-size:var(--t-footnote);font-weight:600;color:var(--muted);
  cursor:pointer;
}
.fb-add:hover{border-color:var(--cyan);color:var(--blue-mid);background:rgba(55,189,245,.04)}

/* Inspector. */
.fb-ins{
  flex:0 0 var(--fb-inspector-w);
  border-left:1px solid var(--line-soft);
  background:var(--card);
  overflow:auto;
  display:flex;flex-direction:column;
}
.fb-ins-tabs{
  display:flex;gap:2px;
  padding:14px 16px 0;
  border-bottom:1px solid var(--line-soft);
}
.fb-ins-tabs button{
  border:none;background:none;font:inherit;
  font-size:var(--t-footnote);font-weight:700;color:var(--muted);
  padding:9px 12px 11px;cursor:pointer;
}
.fb-ins-tabs button[aria-selected="true"]{
  color:var(--blue-mid);
  box-shadow:inset 0 -2px 0 var(--blue-mid);
}
.fb-ins-pane{padding:18px 16px 26px;display:flex;flex-direction:column;gap:var(--fb-ins-gap)}
.fb-ins-l{
  font-size:var(--fb-ins-label);font-weight:700;letter-spacing:.06em;
  text-transform:uppercase;color:var(--muted);margin-bottom:6px;
}
.fb-ins-field{
  border:1px solid var(--line);border-radius:var(--fb-ctl-r);
  padding:9px 12px;font-size:var(--t-footnote);color:var(--ink);
  background:var(--card);min-height:var(--fb-ctl-h);
}
.fb-ins-field:focus-within{border-color:var(--cyan);box-shadow:var(--ring);outline:none}
.fb-ins-field.ph{color:var(--fb-ph-ink);font-size:var(--t-footnote)}
.fb-ins-opt{
  display:flex;align-items:center;gap:8px;
  border:1px solid var(--line-soft);border-radius:9px;
  padding:7px 10px;background:var(--card);
}
.fb-cond{
  border:1px solid var(--line-soft);border-radius:11px;
  background:var(--bg);padding:11px 12px;
  display:flex;flex-direction:column;gap:8px;
}

/* Toggle. */
.fb-sw{
  width:var(--fb-switch-w);height:var(--fb-switch-h);
  border-radius:99px;padding:2px;cursor:pointer;border:none;
  background:var(--line);
  transition:background var(--m-med) ease;
}
.fb-sw[aria-checked="true"]{background:var(--st-done)}
.fb-sw i{
  display:block;
  width:var(--fb-knob);height:var(--fb-knob);
  border-radius:99px;background:#fff;
  box-shadow:0 1px 3px rgba(0,0,0,.25);
  transition:transform var(--m-med) ease;
}
.fb-sw[aria-checked="true"] i{transform:translateX(calc(var(--fb-switch-w) - var(--fb-knob) - 4px))}
.fb-sw:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}


/* ── 3. RESPONSIVE ─────────────────────────────────────────────────────────
   The canvas keeps its width. Everything else gives way, in this order. */

/* Tablet — palette to an icon rail, inspector overlays the canvas. */
@media (max-width:1200px){
  .fb-palette{flex-basis:var(--fb-palette-mini-w)}
  .fb-palette-h{display:none}
  .fb-tiles{grid-template-columns:1fr;padding:12px 0;justify-items:center}
  .fb-tile{
    width:var(--fb-tile-mini);height:var(--fb-tile-mini);
    align-items:center;justify-content:center;padding:0;
    border-radius:var(--fb-ctl-r);
  }
  .fb-tile-l{
    position:absolute;width:1px;height:1px;
    overflow:hidden;clip-path:inset(50%);
  }
  .fb-ins{
    position:absolute;right:0;top:0;bottom:0;
    border-left:1px solid var(--line);
    box-shadow:var(--fb-ins-overlay-shadow);
    z-index:20;
  }
  .fb-ins[hidden]{display:none}
  .fb-body{position:relative}
}

/* Phone — one column. Field cards collapse to rows; palette and inspector
   are sheets; a fixed bottom bar carries Add field. */
@media (max-width:760px){
  .fb-palette{
    position:fixed;left:0;right:0;bottom:0;top:auto;
    flex-basis:auto;
    border-right:none;border-top:1px solid var(--line);
    border-radius:var(--r-lg) var(--r-lg) 0 0;
    box-shadow:0 -14px 34px rgba(9,18,60,.18);
    max-height:70vh;z-index:30;
  }
  .fb-palette[hidden]{display:none}
  .fb-tiles{
    grid-template-columns:repeat(auto-fill,minmax(96px,1fr));
    padding:12px;justify-items:stretch;
  }
  .fb-tile{
    width:auto;height:auto;min-height:var(--tap-min);
    flex-direction:row;gap:9px;align-items:center;
    padding:var(--fb-tile-pad);border-radius:var(--fb-tile-r);
  }
  .fb-tile-l{position:static;width:auto;height:auto;clip-path:none}

  .fb-ins{left:0;width:auto;border-radius:var(--r-lg) var(--r-lg) 0 0}
  .fb-sheet{padding:0 13px}
  .fb-canvas{padding:14px 0 88px}

  /* Collapsed field row: label, type, grip, chevron. The preview is dropped —
     on a phone the builder is a list of questions, and the preview lives
     behind the Preview switch. */
  .fb-card{display:flex;align-items:center;gap:10px;padding:13px 14px}
  .fb-card .fb-prev{display:none}
  .fb-card-acts{display:none}
  .fb-card::after{content:"›";color:var(--fb-ph-ink);font-size:var(--t-footnote);margin-left:auto}

  .fb-bottom{
    position:fixed;left:0;right:0;bottom:0;
    background:var(--card);
    border-top:1px solid var(--line-soft);
    padding:12px 13px calc(12px + env(safe-area-inset-bottom));
    display:flex;gap:9px;z-index:25;
  }
  .fb-bottom .btn{min-height:48px;flex:1}
}

@media (prefers-reduced-motion:reduce){
  .fb-tile:hover,.fb-tile:active{transform:none}
  .fb-sw i{transition:none}
}

/* ── 4. PORT DELTA ─────────────────────────────────────────────────────────
   Rules the handoff did not need and this port does. Two causes:

   (a) The handoff's renderers carry an inline copy of every value so a
       fragment renders standalone. The ported formsbuilder.js drops the inline
       copies — this sheet is always linked in the app — so the few colours and
       boxes that only existed inline are written out here.
   (b) Three controls are real form elements rather than drawn boxes: Type and
       the condition source are <select>, the condition value is an <input>.
       The handoff drew them as static divs with a ▾, which reads correctly in
       a prototype and cannot be operated in an app. */

/* Canvas header, editable in place. */
.fb-head-t:focus,.fb-head-d:focus{outline:none;box-shadow:var(--ring);border-radius:6px}
.fb-head-d.ph:not(:focus):empty::before,
.fb-head-t:not(:focus):empty::before,
.fb-card-q:not(:focus):empty::before,
.fb-card-help.ph:not(:focus):empty::before,
.fb-ins-field[contenteditable]:not(:focus):empty::before{
  content:attr(data-ph);color:var(--fb-ph-ink);
}

/* The question is typed HERE, on the card, not in the side panel (Chad,
   2026-08-24). It has to LOOK writable before it is clicked, or nobody
   discovers it: a hairline box on hover, a focus ring while typing. */
.fb-card-q[contenteditable="true"],
.fb-card-help[contenteditable="true"]{
  display:block;min-width:60px;max-width:100%;
  padding:2px 5px;margin:-2px -5px;border-radius:6px;
  border:1px solid transparent;
  cursor:text;white-space:pre-wrap;overflow-wrap:anywhere;
  transition:border-color var(--m-fast) ease,background var(--m-fast) ease;
}
.fb-card:hover .fb-card-q[contenteditable="true"],
.fb-card:hover .fb-card-help[contenteditable="true"]{border-color:var(--line-soft)}
.fb-card-q[contenteditable="true"]:focus,
.fb-card-help[contenteditable="true"]:focus{
  outline:none;background:var(--card);
  border-color:transparent;box-shadow:var(--ring);
}
.fb-chip-board{background:var(--tag-bg);color:var(--tag-ink)}

.fb-palette-note{
  margin:0 12px 14px;padding:11px 12px;
  border-radius:11px;background:var(--bg);
  border:1px solid var(--line-soft);
  font-size:var(--t-caption);line-height:1.5;color:var(--muted);
}

.fb-prev-select span{color:var(--muted)}

/* On submit card — the one thing the modal never showed. */
.fb-onsubmit{
  margin-top:22px;padding:16px 18px;
  border-radius:12px;background:var(--card);
  border:1px solid var(--line-soft);
  display:flex;align-items:center;gap:12px;
}
.fb-onsubmit-ic{
  width:34px;height:34px;border-radius:9px;
  background:rgba(0,200,117,.12);
  display:grid;place-items:center;font-size:var(--t-body);color:var(--st-done);
}
.fb-onsubmit-t{font-size:var(--t-footnote);font-weight:700;color:var(--ink)}
.fb-onsubmit-d{font-size:var(--t-caption);color:var(--muted);margin-top:2px}
.fb-onsubmit-d b{color:var(--ink-soft)}

/* Real controls in the inspector. select/input need the font and box reset
   that a div got for free. */
select.fb-ins-field,input.fb-ins-field{
  width:100%;font:inherit;font-size:var(--t-footnote);
  appearance:none;-webkit-appearance:none;
  background:var(--card) no-repeat right 11px center;
  background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),
                   linear-gradient(135deg,var(--muted) 50%,transparent 50%);
  background-size:5px 5px,5px 5px;
  background-position:right 14px center,right 9px center;
  padding-right:28px;cursor:pointer;
}
input.fb-ins-field{background-image:none;padding-right:12px;cursor:text}
select.fb-ins-field:focus,input.fb-ins-field:focus{
  outline:none;border-color:var(--cyan);box-shadow:var(--ring);
}
select.fb-ins-field:disabled,input.fb-ins-field:disabled{
  color:var(--muted);cursor:default;background-color:var(--bg);
}

/* Option editor rows. */
.fb-ins-opt-grip{color:var(--line);font-size:var(--t-caption2)}
.fb-ins-opt-v{flex:1;font-size:var(--t-footnote);color:var(--ink);outline:none}
.fb-ins-opt-v:focus{box-shadow:var(--ring);border-radius:4px}
.fb-ins-opt button{
  border:none;background:none;color:var(--fb-ph-ink);
  font-size:var(--t-caption2);cursor:pointer;
}
.fb-ins-opt button:hover{color:var(--st-stuck)}
.fb-opt-add{
  border:1px dashed var(--line-dash);background:var(--card);
  border-radius:9px;padding:8px;font:inherit;font-size:var(--t-caption);
  font-weight:600;color:var(--muted);cursor:pointer;
}
.fb-opt-add:hover{border-color:var(--cyan);color:var(--blue-mid)}

.fb-cond-op{font-size:var(--t-caption);color:var(--muted);flex:0 0 auto}
.fb-ins-hint{font-size:var(--t-caption);color:var(--muted);line-height:1.5}

/* Phone: the inspector's sheet needs to clear the bottom bar. */
@media (max-width:760px){
  .fb-ins{max-height:76vh}
  .fb-ins-pane{padding-bottom:calc(26px + var(--tap-min))}
}

/* Stacking. The handoff's .fb is position:fixed with no z-index, which is
   correct in a prototype that has no app shell around it. In the app, .rail is
   position:relative with z-index:1 children, so the nav bled through the
   palette. 50 sits above the rail and below the app bar (60), which .fb insets
   below anyway — so the bar stays visible and clickable. */
.fb{z-index:50}

/* Tablet fixes. Both are gaps in the handoff's section 3, found at 834px.

   1. .fb-ins keeps only `flex:0 0 var(--fb-inspector-w)` when it becomes
      position:absolute — and absolute positioning ignores flex-basis, so the
      panel shrink-to-fits and ate ~60% of an 834 viewport instead of 320px.
      An explicit width is what the flex-basis was standing in for.
   2. .fb-palette-h is hidden in the icon rail but the note at the foot is not,
      so it wrapped to one word per line down the 62px rail. */
@media (max-width:1200px){
  .fb-ins{width:var(--fb-inspector-w);max-width:88vw}
  .fb-palette-note{display:none}
}
/* On phone the palette is a full-width sheet again, so the note comes back. */
@media (max-width:760px){
  .fb-palette-note{display:block}
  .fb-ins{width:auto;max-width:none}
}

/* Phone: the builder owns the bottom edge. The app's own .tabbar is fixed at
   z-140 and would sit on top of .fb-bottom (z-25) at the same 49px of screen —
   two bottom bars stacked. While the builder is open the tab bar is hidden;
   the builder has its own back arrow and Cancel, so nothing is unreachable.
   .fb-bottom then clears the app bar's z-order for the same reason .fb does. */
body.fb-open .tabbar{display:none}
.fb-bottom{z-index:55}

/* The sheets sit above the bottom bar, and both must clear it when open. */
@media (max-width:760px){
  .fb-palette{z-index:56}
  .fb-ins{z-index:56}
  .fb-canvas{padding-bottom:calc(88px + env(safe-area-inset-bottom))}
}

/* Close control on the overlaid inspector. RESPONSIVE.md specifies a ✕ in the
   header plus Escape at tablet and phone; without it the rail sits over the
   canvas on an iPad with no way to uncover it. Never rendered when docked. */
.fb-ins-x{
  border:none;background:none;cursor:pointer;
  color:var(--muted);font-size:var(--t-footnote);
  width:32px;height:32px;border-radius:8px;margin:2px 0 6px;
}
.fb-ins-x:hover{background:var(--bg);color:var(--ink)}

/* ── 5. PREVIEW ────────────────────────────────────────────────────────────
   The Preview switch position. Canvas only — no palette, no inspector. The
   controls inside .fb-preview-body are formFill()'s own markup (.fld and
   friends), so this styles the frame around them and nothing else; .modal is
   borrowed for the field spacing rather than restated, which is also what
   keeps the two from drifting. */
.fb-prev-note{
  font-size:var(--t-caption);font-weight:600;color:var(--due-ink);
  background:var(--due-bg);border-radius:8px;
  padding:8px 11px;margin-bottom:12px;
}
.fb-preview-body{
  background:var(--card);
  border:1px solid var(--line-soft);
  border-radius:var(--r);
  box-shadow:var(--shadow);
  padding:22px 24px;
  /* .modal is a dialog box elsewhere; inside the canvas it is just the field
     stack, so the dialog's own sizing and centring are undone. */
  max-width:none;width:auto;margin:0;max-height:none;position:static;
}
.fb-preview-body .fld:last-of-type{margin-bottom:0}
.fb-preview-body .hidden{display:none}

/* Temperature: a plain number input with the unit pinned inside the box. The
   value stays unitless so readings sort and compare; only the display carries
   the °F, same reason the server stores the bare number. */
.ff-suffix{position:relative;display:block}
.ff-suffix input{width:100%;padding-right:34px}
.ff-suffix i{
  position:absolute;right:12px;top:50%;transform:translateY(-50%);
  font-style:normal;font-size:var(--t-caption);color:var(--muted);pointer-events:none;
}

/* Dollar amount: a typeable text box with the $ pinned inside on the left
   (ticket Repair cost, 8/19). Same idea as .ff-suffix, mirrored. The value
   is stored as a bare number; only the box shows the sign. */
.money-in{position:relative;display:block}
.money-in input{width:100%;padding-left:24px;font-variant-numeric:tabular-nums}
.money-in i{
  position:absolute;left:12px;top:50%;transform:translateY(-50%);
  font-style:normal;font-size:var(--t-footnote);font-weight:700;color:var(--muted);pointer-events:none;
}

/* ── 6. PHOTO & SIGNATURE ──────────────────────────────────────────────────
   Both are fill-out controls, so they live with .fld in the modal and in the
   builder's Preview alike. Sized for a thumb on a store iPad, not a mouse. */

.ff-drop{
  position:relative;
  min-height:var(--fb-drop-h);
  border:1px dashed var(--line-dash);border-radius:var(--fb-ctl-r);
  background:#fbfcfe;cursor:pointer;
  display:flex;align-items:center;justify-content:center;
  overflow:hidden;
}
.ff-drop:hover{border-color:var(--cyan);background:rgba(55,189,245,.04)}
.ff-drop-cta{font-size:var(--t-caption);color:var(--fb-ph-ink);padding:24px 12px}
.ff-drop-thumb{max-width:100%;max-height:220px;display:block;border-radius:var(--fb-ctl-r)}
.ff-drop-clear{
  position:absolute;top:6px;right:6px;
  width:28px;height:28px;border-radius:8px;
  border:none;background:rgba(9,18,60,.62);color:#fff;
  font-size:var(--t-caption);cursor:pointer;
}
.ff-drop-clear:hover{background:var(--st-stuck)}

.ff-sign{
  border:1px dashed var(--line-dash);border-radius:var(--fb-ctl-r);
  background:#fbfcfe;padding:8px;position:relative;
}
.ff-sign canvas{
  display:block;width:100%;height:150px;
  background:var(--card);border-radius:8px;
  /* The canvas IS the control on a touch device; without this the browser
     scrolls the modal instead of drawing the stroke. */
  touch-action:none;cursor:crosshair;
}
.ff-sign-hint{
  font-size:var(--t-caption2);color:var(--fb-ph-ink);
  text-align:center;margin-top:6px;
}
.ff-sign-clear{
  position:absolute;top:14px;right:14px;
  border:1px solid var(--line-soft);background:var(--card);
  border-radius:8px;padding:5px 10px;
  font:inherit;font-size:var(--t-caption);font-weight:600;color:var(--muted);cursor:pointer;
}
.ff-sign-clear:hover{border-color:var(--st-stuck);color:var(--st-stuck)}

/* Phone: both controls get real tap room. */
@media (max-width:760px){
  .ff-drop{min-height:96px}
  .ff-sign canvas{height:170px}
  .ff-drop-clear,.ff-sign-clear{min-height:36px}
}

/* styles.css:2774 sets `.fld input,.fld select,.fld textarea{display:block}`,
   which outranks the UA's [hidden] rule -- so the file input rendered as a
   "Choose File" button inside the drop zone, and the marker inputs were being
   given a padded bordered box they must never have. Both need a selector that
   beats 0,1,1. */
.fld .ff-drop input[type=file],
.fld input[type=hidden]{display:none !important}

/* ================= Equipment (#49) =================
   Cards are the default view because this module's first job is recognition:
   a shift leader matches the machine in front of them to a picture, not to a
   model number. Everything below serves that — a big thumbnail, the serial in
   monospace so digits off a nameplate are checkable character by character,
   and an explicit empty state instead of a blank cell. */
.eq-cards{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(240px,1fr))}
.eq-card{display:flex;gap:12px;align-items:flex-start;text-align:left;padding:12px;
  background:var(--card);border:1px solid var(--line);border-radius:12px;cursor:pointer}
.eq-card:hover{border-color:var(--accent)}
.eq-card-b{min-width:0;flex:1}
.eq-card-h{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
.eq-thumb{width:64px;height:64px;object-fit:cover;border-radius:8px;background:var(--bg2);flex:none}
/* The manufacturer's catalogue shot is a product render on white — contain,
   not cover, or it crops the machine. A store photo is a real photo: cover. */
.eq-thumb-cat{object-fit:contain;background:#fff;padding:3px}
.eq-thumb-none{display:flex;align-items:center;justify-content:center;font-size:var(--t-title1);opacity:.5}
.eq-ser{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:var(--t-caption)}
.eq-ser-none{font-family:inherit;color:var(--muted);font-style:italic}
.eq-photos{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0}
.eq-photo{width:120px;height:120px;object-fit:cover;border-radius:10px;border:1px solid var(--line)}
.eq-photo-cat{object-fit:contain;background:#fff;padding:4px}
.eq-catprop{display:flex;gap:12px;align-items:flex-start;padding:12px;margin:10px 0;
  background:var(--bg2);border:1px dashed var(--line);border-radius:10px}
/* The warranty banner. Three states, and the UNKNOWN one is deliberately
   coloured as a caution rather than a negative: "we don't know" must never
   read as "not covered", or it talks someone out of a free repair. */
.eq-warn{padding:10px 12px;border-radius:10px;margin:10px 0;font-size:var(--t-footnote);line-height:1.45}
.eq-warn-in{background:rgba(16,150,90,.10);border:1px solid rgba(16,150,90,.35)}
.eq-warn-out{background:var(--bg2);border:1px solid var(--line);color:var(--muted)}
.eq-warn-unk{background:rgba(214,158,46,.10);border:1px solid rgba(214,158,46,.38)}
.eq-warn-terms{margin-top:6px;font-size:var(--t-caption);color:var(--muted)}
.eq-replace{color:var(--danger,#c0392b)}

/* Card flags (Chad, 8/8). Two levels only. RED means a person has to decide
   something; AMBER means we are missing data. They are deliberately not the
   same weight — if everything shouts, the shouting stops working, and a store
   with 24 machines will have amber on several of them for a while.
   The red card also gets a left edge, so a wall of cards scans in one pass
   without reading a single chip. */
.eq-flags{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
.eq-flag{font-size:var(--t-caption);font-weight:650;line-height:1;padding:4px 8px;border-radius:999px;
  white-space:nowrap}
.eq-flag-red{background:rgba(192,57,43,.12);border:1px solid rgba(192,57,43,.42);
  color:var(--danger,#c0392b)}
.eq-flag-amber{background:rgba(214,158,46,.12);border:1px solid rgba(214,158,46,.40);
  color:var(--panel-warn-ink,#8a5a00)}
.eq-flag-info{background:var(--bg2);border:1px solid var(--line);color:var(--muted)}
.eq-card-red{border-left:3px solid var(--danger,#c0392b)}
.eq-ackbtn{margin-top:8px;padding:4px 10px;font-size:var(--t-caption)}
/* Fleet value. Manager+ only — the server simply does not send it otherwise. */
.eq-value{padding:14px 16px;margin-bottom:12px}
.eq-value-h{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.eq-value-tot{font-size:var(--t-title2);letter-spacing:-.4px}
.eq-value-rows{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));
  gap:10px;margin-top:10px}
.eq-value-row{display:flex;flex-direction:column;gap:2px;padding:9px 11px;
  background:var(--bg2);border:1px solid var(--line);border-radius:9px}
.eq-value-name{font-weight:650;font-size:var(--t-footnote)}
.eq-value-n{font-size:var(--t-headline);font-weight:700}
.eq-value-foot{margin-top:10px;line-height:1.45}
.eq-tfoot td{border-top:2px solid var(--line);padding-top:10px}
/* An estimate is never allowed to look like an invoice figure. */
.eq-est{font-size:var(--t-caption2);font-weight:700;padding:1px 5px;border-radius:999px;
  background:var(--bg2);border:1px solid var(--line);color:var(--muted);vertical-align:middle}
.eq-est-line{font-style:italic}
.eq-value-est{font-size:var(--t-footnote);font-weight:600;color:var(--muted);letter-spacing:0}

/* The "no serial on this unit" tick in the equipment editor. Laid out as a row
   so the explanatory line sits under the label rather than beside the box. */
.fld-check{display:grid;grid-template-columns:auto 1fr;gap:4px 8px;align-items:center;
  font-weight:600}
/* display:grid beats the `hidden` attribute, so say it explicitly — the tick
   box is hidden once a machine has barrels, and it stayed on screen without
   this. */
.fld-check[hidden]{display:none}
.fld-check input[type=checkbox]{width:16px;height:16px;margin:0}
.fld-check .ct-sub{grid-column:2;font-weight:400;line-height:1.4}


/* #49 — a machine that is several units (a fryer battery, a stacked pair).
   One record, one card, one thing to pick on a ticket; each barrel keeps its
   own plate. The detail lists them; the editor edits them as rows. */
.eq-units{display:grid;gap:4px}
.eq-unit{display:flex;gap:10px;align-items:baseline;padding:5px 0;
  border-bottom:1px dashed var(--line)}
.eq-unit:last-child{border-bottom:0}
.eq-unit-l{font-weight:600;min-width:96px;flex:none}
.eq-unit-s{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:var(--t-caption)}
.eq-unitrow{grid-template-columns:1fr 1fr auto auto;align-items:end;margin-bottom:8px}
.eq-unitrow .eq-u-nsw{font-weight:400;font-size:var(--t-caption);white-space:nowrap;padding-bottom:8px}
.eq-unitrow .eq-u-del{padding:6px 10px;align-self:end;margin-bottom:2px}
@media (max-width:620px){
  .eq-unitrow{grid-template-columns:1fr 1fr;row-gap:6px}
}

/* #49 — the ticket <-> equipment link, both directions */
.eq-tixnote{margin:6px 0 10px 92px;font-size:var(--t-caption);color:var(--ink-soft);line-height:1.5}
.eq-tixhist{margin-top:6px;font-weight:600;color:var(--ink)}
.eq-tixopen{margin-top:8px;padding:4px 10px;font-size:var(--t-caption)}
.eq-hist{margin-top:6px}
.eq-histrow{display:flex;gap:10px;align-items:baseline;width:100%;text-align:left;padding:9px 12px;
  background:var(--card);border:1px solid var(--line);border-radius:9px;margin-bottom:6px;cursor:pointer}
.eq-histrow:hover{border-color:var(--accent)}
.eq-histkey{font-weight:700;font-size:var(--t-caption);color:var(--muted);flex:none}
.eq-histsum{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.eq-histmeta{font-size:var(--t-caption);color:var(--muted);flex:none;text-transform:capitalize}

/* #49 Ramp link — the Spend rows on a machine. Same row chrome as the repair
   history; a kind pill (Card / Bill) and the amount, then the Ramp link. */
.eq-spendrow{text-decoration:none;color:inherit;font-size:var(--t-footnote)}
.eq-spendkind{flex:none;font-size:var(--t-caption2);font-weight:var(--w-semibold);line-height:1.35;
  padding:var(--sp-1) var(--sp-3);border-radius:var(--r-pill);background:var(--tag-bg);color:var(--tag-ink);
  text-transform:uppercase;letter-spacing:.02em}
.eq-spendkind.bill{background:rgba(16,150,90,.12);color:#0f7a4a}
.eq-spendamt{flex:none;font-weight:var(--w-semibold);font-variant-numeric:tabular-nums;font-size:var(--t-footnote)}
.eq-spendopen{flex:none;font-size:var(--t-caption);color:var(--tag-ink);white-space:nowrap}
.eq-spendrow:hover .eq-spendopen{text-decoration:underline}
.eq-spend-none{padding:var(--sp-4) var(--sp-5);border:1px dashed var(--line-dash);border-radius:9px;
  font-size:var(--t-footnote);color:var(--ink-soft);line-height:1.5}
.eq-spend-none .ct-sub{display:block;margin-top:var(--sp-1)}
@media (max-width:620px){
  .eq-spendrow{flex-wrap:wrap}
  .eq-spendrow .eq-histsum{flex-basis:100%;white-space:normal;order:-1}
}

/* #49 — manuals shelf */
.eq-manuals{margin-top:4px}
.eq-manual{display:flex;gap:10px;align-items:baseline;padding:9px 12px;margin-bottom:6px;
  background:var(--card);border:1px solid var(--line);border-radius:9px;text-decoration:none;color:inherit}
.eq-manual:hover{border-color:var(--accent)}
.eq-manual-i{flex:none}
.eq-manual-n{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}
.eq-manual-m{font-size:var(--t-caption);color:var(--muted);flex:none}
.eq-manual-add{display:inline-block;margin-top:4px;cursor:pointer}

/* #49 — the found picture is in use; this is its caption, not a gate */
.eq-catcap{margin:2px 0 12px}
.eq-catcap .btn{margin-top:6px;padding:3px 9px;font-size:var(--t-caption)}

/* ================= Manager Bonuses =================
   The bonus board, rendered identically in My Space -> Finances and in the
   Exec department (Chad, 2026-08-07). Prefixed .bon-* rather than the KB's
   .b* so nothing here can collide with an existing single-letter class. */
.bon-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px}
.bon-card{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);
  box-shadow:var(--k-shadow);padding:16px 18px 14px;overflow:hidden}
.bon-card-h{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;
  border-bottom:1px solid var(--line-soft);padding-bottom:12px;margin-bottom:12px}
.bon-card-h h2{font-size:var(--t-headline);margin:0;color:var(--navy);font-weight:700}
.bon-card-sub{display:block;font-size:var(--t-caption2);font-weight:500;color:var(--muted);margin-top:2px}
.bon-tot{text-align:right;white-space:nowrap}
.bon-tot-l{display:block;font-size:var(--t-caption2);color:var(--muted)}
.bon-tot-v{display:block;font-size:var(--t-title2);font-weight:800;color:var(--blue-royal);line-height:1.1;
  font-variant-numeric:tabular-nums}
.bon-empty{color:var(--muted);font-size:var(--t-body);padding:6px 0 4px}
.bon-flag{display:inline-block;background:#fdf3d7;color:#92600b;border:1px solid #f0dca6;
  border-radius:999px;font-size:var(--t-caption2);font-weight:600;padding:3px 10px;margin-bottom:12px}
.bon-pills{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:6px}
.bon-pill{background:var(--k-surface2);border-radius:var(--r-sm);padding:8px 6px;text-align:center}
.bon-pill-l{font-size:var(--t-caption2);text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}
.bon-pill-v{font-size:var(--t-body);font-weight:700;color:var(--ink);margin-top:2px;
  font-variant-numeric:tabular-nums}
.bon-hist{margin-top:12px}
.bon-hist summary{cursor:pointer;font-size:var(--t-caption);color:var(--blue-royal);font-weight:600;
  list-style:none;user-select:none}
.bon-hist summary::-webkit-details-marker{display:none}
.bon-hist summary::before{content:"▸ ";color:var(--muted)}
.bon-hist[open] summary::before{content:"▾ "}
.bon-table{width:100%;border-collapse:collapse;margin-top:8px;font-size:var(--t-footnote)}
.bon-table th{text-align:left;color:var(--muted);font-weight:600;font-size:var(--t-caption2);
  text-transform:uppercase;letter-spacing:.03em;padding:4px 6px;border-bottom:1px solid var(--line)}
.bon-table td{padding:5px 6px;border-bottom:1px solid var(--line-soft);color:var(--ink)}
.bon-table th.r,.bon-table td.r{text-align:right}
.bon-table td.r{font-variant-numeric:tabular-nums;font-weight:600}
.bon-note{color:var(--muted);font-size:var(--t-caption);margin-top:16px;max-width:640px}
/* The shared period-history table (2026-09-08): rows line up across every card. */
.bon-histwide{margin-top:16px}
.bon-scroll{overflow-x:auto}
.bon-table-wide th:first-child,.bon-table-wide td:first-child{white-space:nowrap}
.bon-table-wide td.r{min-width:110px}
/* The unfolded row: one column per card, side by side. min-width:0 on every
   grid item or the pillar grids and NOI notes overflow their tracks and pile
   onto the neighbour. Sticky-left with a viewport cap so on a phone, where
   the table itself scrolls sideways, the breakdown stays pinned in view
   instead of running off with the columns. */
.bon-hist-cols{display:grid;grid-template-columns:repeat(var(--n,4),minmax(0,1fr));gap:14px;
  position:sticky;left:0;max-width:calc(100vw - 60px)}
.bon-hist-col{min-width:0}
.bon-hist-col .bon-pills{grid-template-columns:repeat(2,minmax(0,1fr))}
.bon-hist-col .bon-pill{min-width:0;overflow-wrap:anywhere}
.bon-hist-col .bon-noi{min-width:0;display:block;white-space:normal}
.bon-hist-col .bon-noi-l,.bon-hist-col .bon-noi-say{display:block;white-space:normal;overflow-wrap:anywhere}
.bon-hist-col .bon-noi-l{margin-bottom:2px}
.bon-hist-col-h{font-size:var(--t-caption2);text-transform:uppercase;letter-spacing:.03em;color:var(--muted);font-weight:600;margin-bottom:6px}
.bon-hist-na{color:var(--muted);font-weight:400}
.bon-pills-2{grid-template-columns:repeat(2,minmax(0,1fr))}
@media (max-width:767px){.bon-hist-cols{grid-template-columns:1fr;max-width:calc(100vw - 48px)}}

/* The rollup strip — the one-line answer, above the per-store detail. */
.bon-roll{background:var(--grad);color:#fff;border-radius:var(--r-lg);padding:18px 20px;
  margin-bottom:18px;display:flex;flex-wrap:wrap;gap:20px;align-items:center;
  justify-content:space-between}
.bon-roll-kicker{font-size:var(--t-caption2);text-transform:uppercase;letter-spacing:.06em;opacity:.85}
.bon-roll-total{font-size:var(--t-display);font-weight:800;line-height:1.05;margin-top:2px;
  font-variant-numeric:tabular-nums}
.bon-roll .bon-roll-sub{font-size:var(--t-caption);opacity:.85;margin-top:2px}
.bon-roll-grid{display:flex;flex-wrap:wrap;gap:10px}
.bon-roll-cell{background:rgba(255,255,255,.14);border-radius:var(--r-sm);padding:10px 14px;
  min-width:132px}
.bon-roll-l{font-size:var(--t-caption2);opacity:.85}
.bon-roll-v{font-size:var(--t-title3);font-weight:700;margin-top:2px;font-variant-numeric:tabular-nums}

@media (max-width:640px){
  .bon-pills{grid-template-columns:repeat(2,1fr)}
  .bon-roll-total{font-size:var(--t-title1)}
}

/* Bonus board tabs + the "How it's calculated" explainer */
.bon-tabs{display:flex;gap:6px;margin:0 0 18px;border-bottom:1px solid var(--line)}
.bon-tab{background:none;border:0;border-bottom:2px solid transparent;padding:8px 14px;
  font-size:var(--t-body);font-weight:600;color:var(--muted);cursor:pointer;margin-bottom:-1px}
.bon-tab:hover{color:var(--ink)}
.bon-tab.on{color:var(--blue-royal);border-bottom-color:var(--blue-royal)}
.bon-card-wide{margin-bottom:16px}
.bon-card-muted{opacity:.55}
.bon-tier-note{font-size:var(--t-caption);color:var(--muted);margin:0 0 10px;line-height:1.5}
.bon-steps{margin:4px 0 0;padding-left:20px;font-size:var(--t-body);color:var(--ink);line-height:1.7}
.bon-steps li{margin-bottom:4px}
.bon-banner{background:var(--k-surface2);border:1px solid var(--line);
  border-left:3px solid var(--blue-royal);border-radius:var(--r-sm);
  padding:14px 16px;margin-bottom:18px}
.bon-banner-k{font-size:var(--t-caption2);text-transform:uppercase;letter-spacing:.05em;
  color:var(--blue-royal);font-weight:700;margin-bottom:6px}
.bon-banner p{margin:0;font-size:var(--t-body);color:var(--ink);line-height:1.6;max-width:78ch}
.bon-banner-now{margin-top:8px !important;color:var(--muted) !important;font-size:var(--t-footnote) !important}

/* The achieved figure sits above the dollars it earned — the number is the
   answer to "why $200?", so it reads first and the payout reads as its
   consequence. Absent on historical periods pushed before actuals existed. */
.bon-pill-a{font-size:var(--t-body);font-weight:700;color:var(--ink);margin-top:3px;
  font-variant-numeric:tabular-nums;line-height:1.2}
.bon-pill-v-sub{font-size:var(--t-caption) !important;font-weight:600 !important;
  color:var(--blue-royal) !important}

/* NOI — the gate, not a pillar. Its own row, stating what it did to the total. */
.bon-noi{display:grid;grid-template-columns:auto 1fr;align-items:baseline;gap:6px 10px;
  margin-top:10px;padding:9px 12px;border-radius:var(--r-sm);border:1px solid var(--line);
  background:var(--k-surface2)}
.bon-noi-l{font-size:var(--t-caption2);text-transform:uppercase;letter-spacing:.04em;color:var(--muted);
  font-weight:700}
.bon-noi-v{font-size:var(--t-headline);font-weight:800;font-variant-numeric:tabular-nums;color:var(--ink)}
.bon-noi-say{grid-column:1/-1;font-size:var(--t-caption);color:var(--muted);line-height:1.45}
.bon-noi-ok{border-left:3px solid #2f8f4e}
.bon-noi-ok .bon-noi-v{color:#2f8f4e}
.bon-noi-bad{border-left:3px solid #c0392b;background:#fdf1ef}
.bon-noi-bad .bon-noi-v{color:#c0392b}
.bon-noi-wait{border-left:3px solid #d0a13a}
.bon-noi-wait .bon-noi-v{font-size:var(--t-body);color:#92600b}

/* A long score ("102 hrs under") wraps and pushed that pill's dollar figure
   below the others, so the row of payouts no longer read as a row. Column
   layout + margin-top:auto bottom-aligns every payout whatever the score does. */
.bon-pill{display:flex;flex-direction:column}
.bon-pill .bon-pill-v{margin-top:auto;padding-top:2px}

/* ===== In the bank — balance strip on exec Cash & Deposits ===== */
.bank-strip{margin:0 0 18px}
.bank-strip-h{display:flex;align-items:baseline;gap:10px;margin-bottom:8px}
.bank-strip-as{font-size:var(--t-caption);color:var(--muted)}
.bank-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px}
.bank-acct{background:var(--card);border:1px solid var(--line);border-radius:var(--r-sm);
  padding:11px 13px;display:flex;flex-direction:column;min-width:0}
.bank-acct-store{border-left:3px solid var(--blue-royal)}
.bank-acct-n{font-size:var(--t-caption2);font-weight:700;color:var(--ink);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.bank-acct-o{font-size:var(--t-caption2);color:var(--muted);margin-bottom:6px;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.bank-acct-v{font-size:var(--t-title3);font-weight:800;font-variant-numeric:tabular-nums;
  color:var(--ink);margin-top:auto}
.bank-acct-v.bad{color:#c0392b}
.bank-acct-a{font-size:var(--t-caption2);color:var(--muted);margin-top:2px}
.bank-strip-t{font-size:var(--t-caption);color:var(--muted);margin-top:8px}
@media print{.bank-strip{break-inside:avoid}}
.bank-more{margin-top:10px}
.bank-more summary{cursor:pointer;font-size:var(--t-caption);color:var(--blue-royal);font-weight:600;
  list-style:none;user-select:none}
.bank-more summary::-webkit-details-marker{display:none}
.bank-more summary::before{content:"▸ ";color:var(--muted)}
.bank-more[open] summary::before{content:"▾ "}
.bank-tbl{width:100%;border-collapse:collapse;margin-top:8px;font-size:var(--t-footnote);max-width:620px}
.bank-tbl td{padding:4px 8px;border-bottom:1px solid var(--line-soft);color:var(--ink)}
.bank-tbl td.r{text-align:right;font-variant-numeric:tabular-nums;font-weight:600}
.bank-tbl td.r.bad{color:#c0392b}
.bank-tbl tr.bank-org td{background:var(--k-surface2);font-size:var(--t-caption2);font-weight:700;
  text-transform:uppercase;letter-spacing:.04em;color:var(--muted);padding-top:8px}
.bank-org-n{font-weight:600;opacity:.7;margin-left:4px}

/* One card per store / per company, each carrying its own accounts and total
   (Chad, 2026-08-08). The card leads with the number he actually wants — what
   that store or company HAS — and the account rows sit underneath as the
   breakdown, so the page reads at a glance and still answers "which account". */
.bank-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));
  gap:12px;align-items:start}
.bank-card{background:var(--card);border:1px solid var(--line);border-radius:var(--r-sm);
  border-left:3px solid var(--blue-royal);padding:13px 15px;min-width:0}
.bank-card-h{font-size:var(--t-caption);font-weight:800;color:var(--ink);letter-spacing:.01em}
.bank-card-v{font-size:var(--t-title2);font-weight:800;font-variant-numeric:tabular-nums;
  color:var(--ink);line-height:1.15;margin-top:3px}
.bank-card-v.bad{color:#c0392b}
.bank-card-s{font-size:var(--t-caption2);color:var(--muted);margin-top:2px}
.bank-card .bank-tbl{margin-top:9px;font-size:var(--t-caption);max-width:none}
.bank-card .bank-tbl td{padding:3px 0}
.bank-acct-sub{display:block;font-size:var(--t-caption2);color:var(--muted)}
/* Owed, not held — a card sits under a rule so the rows above it read as the
   bank accounts that make up the headline total. */
.bank-owed{border-top:2px solid var(--line)}
.bank-owed td{padding-top:7px!important;color:var(--ink-soft)}
/* Liabilities read as a positive amount OWED — the bank signs a LOC and a
   mortgage differently for the same kind of debt, so the sign is not a
   trustworthy cue. See the note on owedRow(). */
.bank-owed .owed-head td{font-size:var(--t-caption2);font-weight:800;text-transform:uppercase;
  letter-spacing:.06em;color:var(--muted);padding-bottom:2px!important}
.bank-tbl td.r.owed{color:#c0392b}
.bank-owed .owed-tot td{border-top:1px solid var(--line-soft);font-weight:700;
  color:var(--ink);padding-top:5px!important}
@media print{.bank-card{break-inside:avoid}}

/* % of net sales / % of section, to the RIGHT of each P&L figure
   (Chad, 2026-08-13: "put them to the right of each number"). The number and
   the percent are two fixed-width tracks inside the cell so both stay in their
   own straight column down the table — a plain inline span would let a wide
   figure shove the percentages out of alignment. */
.pnl-cell{display:inline-flex;align-items:baseline;gap:7px;justify-content:flex-end}
.pnl-cell .pnl-num{text-align:right}
.pnl-pct{font-size:var(--t-caption2);font-weight:600;color:var(--muted);line-height:1.25;
  min-width:42px;text-align:right;flex:0 0 auto;
  font-variant-numeric:tabular-nums}
.pnl-pct-sec{font-weight:400;color:var(--ink-soft);opacity:.75}
.pnl-legend{font-size:var(--t-caption);color:var(--ink-soft);margin-top:10px;line-height:1.6;
  background:var(--k-surface2);border-radius:var(--r-sm);padding:9px 12px;max-width:780px}

/* ===== Financial Access matrix ===== */
.fa-legend{display:flex;gap:18px;flex-wrap:wrap;align-items:center;font-size:var(--t-caption);
  color:var(--muted);margin-bottom:12px}
.fa-legend b{color:var(--blue-royal);font-size:var(--t-body)}
.fa-legend-mute{opacity:.8}
.fa-tbl{width:100%;border-collapse:collapse;font-size:var(--t-footnote)}
.fa-tbl th,.fa-tbl td{border-bottom:1px solid var(--line-soft)}
.fa-tbl th.fa-p{padding:8px 4px;text-align:center;vertical-align:bottom;min-width:64px}
.fa-p-n{font-size:var(--t-caption);font-weight:600;color:var(--ink);margin-bottom:3px}
/* Narrowed 280 -> 210 (Chad, 2026-08-08). The frozen column costs the same
   width on every horizontal scroll position, so every pixel it gives back is a
   person column you can see without scrolling — which is the whole reason to
   freeze it. 210 still fits the longest label ("Cash & Deposits
   reconciliation") on two lines. */
.fa-tbl th.fa-l,.fa-tbl td.fa-l{text-align:left;padding:8px 10px;min-width:210px;
  max-width:210px;border-right:1px solid var(--line)}
.fa-l-n{font-size:var(--t-caption);font-weight:600;color:var(--ink);line-height:1.3}
.fa-l-d{font-size:var(--t-caption2);color:var(--muted);margin-top:2px;line-height:1.35}
/* Cells are circles, not glyphs — a permissions grid has to read at a glance
   (Chad, 2026-08-08: "make the dots more substantive... round or coloured"). */
.fa-c{text-align:center;line-height:1;padding:9px 4px;vertical-align:middle}
.fa-dot{display:inline-block;width:13px;height:13px;border-radius:50%;vertical-align:middle}
.fa-dot-code{background:#94a3b8;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
.fa-dot-on{background:#2f8f4e;box-shadow:0 0 0 3px rgba(47,143,78,.22)}
.fa-dot-lock{background:#b6bfcc;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
.fa-dot-wait{background:var(--line);animation:faPulse .8s ease-in-out infinite}
@keyframes faPulse{0%,100%{opacity:.35}50%{opacity:1}}
/* The empty grantable cell: a real target, big enough to look clickable
   (Chad: "make the plus a little larger where it acts like [a button]"). */
.fa-plus{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;
  border-radius:50%;font-size:var(--t-headline);font-weight:600;line-height:1;color:transparent;
  border:1px dashed transparent;transition:all .12s ease;vertical-align:middle}
.fa-off,.fa-on{cursor:pointer}
.fa-off:hover .fa-plus{color:var(--blue-royal);border-color:var(--blue-royal);
  background:#eef3ff}
.fa-c:hover{background:var(--k-surface2)}
.fa-off .fa-plus{color:#cbd5e1;border-color:#e2e8f0}
.fa-on:hover .fa-dot-on{box-shadow:0 0 0 3px rgba(192,57,43,.25);background:#c0392b}
.fa-code{cursor:default}
.fa-lock{cursor:default}
.fa-lockrow{background:var(--k-surface2)}
.fa-lockrow .fa-l-n{color:var(--muted)}
.fa-tag{display:inline-block;background:var(--line);color:var(--muted);border-radius:999px;
  font-size:var(--t-caption2);font-weight:700;text-transform:uppercase;letter-spacing:.04em;
  padding:1px 7px;margin-left:6px;vertical-align:middle}
.fa-sec td{background:var(--k-surface2);font-size:var(--t-caption2);font-weight:700;
  text-transform:uppercase;letter-spacing:.05em;color:var(--muted);padding:7px 12px}
.fa-open{padding:4px 11px;font-size:var(--t-caption)}
.fa-focus-note{background:#eef3ff;border:1px solid #cfdcfb;border-left:3px solid var(--blue-royal);
  border-radius:var(--r-sm);padding:9px 12px;margin-bottom:12px;font-size:var(--t-caption);color:var(--ink)}
.fa-focus>td{background:#eef3ff !important}
.fa-focus .fa-l-n{color:var(--blue-royal)}
.adm-fin{display:flex;flex-wrap:wrap;gap:6px;align-items:center;padding:2px 0 6px}
.adm-fin-chip{display:inline-block;background:var(--k-surface2);border:1px solid var(--line);
  border-radius:999px;font-size:var(--t-caption);font-weight:600;color:var(--ink);padding:3px 10px}
.adm-fin-dlg{border-color:#9ccdae;background:#eef8f1;color:#2f8f4e}
.adm-fin-none{font-size:var(--t-caption);color:var(--muted)}

/* Freeze the Surface column, and keep the people header in view (Chad,
   2026-08-08: "freeze the surface column so that as I scroll over, I can see
   who I'm giving access to"). Scrolled right, the rows were reading "…tory",
   "…all stores" — dots with no idea which surface they belonged to, which is
   the one thing you must not be unsure of on a permissions screen.
   Both axes are pinned: the column so you know WHAT, the header so you know
   WHO. Sticky cells need an opaque background or the cells scroll underneath. */
.fa-tbl{position:relative}
.fa-tbl th.fa-l,.fa-tbl td.fa-l{position:sticky;left:0;z-index:2;background:var(--card)}
.fa-tbl thead th{position:sticky;top:0;z-index:3;background:var(--card)}
.fa-tbl thead th.fa-l{z-index:4}
.fa-lockrow td.fa-l{background:var(--k-surface2)}
.fa-focus>td.fa-l{background:#eef3ff !important}
/* the seam, so the frozen column reads as frozen rather than as overlap */
.fa-tbl th.fa-l::after,.fa-tbl td.fa-l::after{content:"";position:absolute;top:0;right:0;
  bottom:0;width:6px;transform:translateX(100%);pointer-events:none;
  background:linear-gradient(90deg,rgba(15,23,42,.10),transparent)}
/* the section bands span every column — pin their label, not the band */
.fa-sec td{position:relative}
.fa-sec td>span{position:sticky;left:12px}
.fa-perstore{padding:2px 8px;font-size:var(--t-caption2);margin-top:5px}

/* ── Meetings: Table + Calendar views (Chad, 2026-08-10) ───────────────────
   Two new ways to read the same filtered list, rendered into the .mtg-doc pane
   that previously held only the "Pick a meeting" placeholder. Both borrow the
   calendar's existing hairline-grid treatment so the module still looks like
   one module. */
.mtg-table-wrap{overflow:auto;padding:0 22px 28px}
.mtg-table{width:100%;border-collapse:separate;border-spacing:0;font-size:var(--t-footnote)}
.mtg-table thead th{z-index:2;background:var(--card);
  text-align:left;padding:0;border-bottom:1px solid var(--line)}
.mtg-table thead th.num{text-align:right}
.mt-sort{width:100%;border:none;background:none;font:inherit;font-weight:700;
  color:var(--muted);padding:10px 10px;cursor:pointer;text-align:inherit;
  letter-spacing:.02em;text-transform:uppercase;font-size:var(--t-caption2)}
.mt-sort:hover{color:var(--ink)}
.mtg-table tr.mt-filters th{padding:0 6px 6px}
.mt-cf{width:100%;font-size:var(--t-caption);padding:3px 6px;border:1px solid var(--line);
  border-radius:6px;background:#fff;color:var(--ink)}
.mtg-table tbody tr{cursor:pointer}
.mtg-table tbody td{padding:9px 10px;border-bottom:1px solid var(--line-soft);
  vertical-align:middle}
.mtg-table tbody td.num{text-align:right;white-space:nowrap;color:var(--muted)}
.mtg-table tbody tr:hover td{background:#f7f8fb}
.mt-title{font-weight:600;color:var(--ink)}
.mt-time{display:block;font-size:var(--t-caption);color:var(--muted)}
/* Status reads as a state, not a tag — muted fills, no shouting. */
.mt-st{display:inline-block;padding:2px 9px;border-radius:999px;font-size:var(--t-caption);
  font-weight:650;white-space:nowrap}
.mt-st-sched{background:#eef2ff;color:#3b4da8}
.mt-st-live {background:#e7f7ee;color:#1c7a49}
.mt-st-done {background:#eef0f4;color:#5b6473}
.mt-st-nodoc{background:#fdf0e6;color:#9a5b1c}

/* Month grid — deliberately the same hairline treatment as My Calendar. */
.mtg-month{padding:4px 22px 28px}
.mm-head{display:flex;align-items:center;gap:8px;margin:2px 0 12px}
.mm-head h3{font-size:var(--t-title3);font-weight:800;letter-spacing:-.3px;margin:0;min-width:190px}
.mm-note{margin-left:auto;font-size:var(--t-caption);color:var(--muted)}
.mm-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:1px;
  background:var(--line-soft);border:1px solid var(--line-soft);border-radius:12px;
  overflow:hidden}
.mm-dow{background:var(--card);text-align:center;font-size:var(--t-caption2);font-weight:700;
  color:var(--muted);text-transform:uppercase;letter-spacing:.04em;padding:7px 0}
.mm-cell{background:#fff;min-height:104px;padding:5px 6px;display:flex;
  flex-direction:column;gap:2px}
.mm-out{background:#fafafc}
.mm-out .mm-dnum{color:#c0c3cf}
.mm-dnum{font-size:var(--t-caption);font-weight:650;color:var(--ink);padding:1px 2px}
.mm-today .mm-dnum{background:var(--accent,#2f6df6);color:#fff;border-radius:999px;
  width:21px;height:21px;display:grid;place-items:center}
.mm-ev{display:flex;align-items:center;gap:4px;width:100%;border:none;
  background:none;font:inherit;font-size:var(--t-caption);color:var(--ink);text-align:left;
  padding:2px 3px;border-radius:5px;cursor:pointer;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.mm-ev:hover{background:#f0f2f7}
.mm-ev .mm-t{color:var(--muted);font-variant-numeric:tabular-nums}
.mm-ev .fdot{flex:0 0 auto}

/* Scope + when segmented controls sit in the filter row together. */
.mtg-scope .pn-count{margin-left:5px;opacity:.65;font-weight:600}
.mt-none{color:var(--muted);opacity:.55}
/* A bare text node in a flex row won't shrink, so the meeting title bled past
   the day cell into the next column. Give it a span that can. */
.mm-ev{gap:5px}
.mm-lbl{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mm-ev .mm-t{flex:0 0 auto}
/* Curated views: the first thing in the rail, and each one a shareable link. */
.mtg-views{display:flex;flex-direction:column;gap:1px}
.mtg-vw{display:flex;align-items:center;gap:7px}
.mtg-vw .vw-ic{flex:0 0 auto;width:16px;text-align:center;font-size:var(--t-caption)}
.mtg-vw-copy{margin-top:5px;font-size:var(--t-caption);color:var(--muted)}
.mtg-vw-copy:hover{color:var(--ink)}
/* Equipment's cost total, kept when its table moved onto the shared package. */
.eq-total{padding:10px 14px;border-top:1px solid var(--line);font-size:var(--t-footnote);text-align:right}
/* The Related panel (relations.js). */
.rel-panel{border:1px solid var(--line);border-radius:12px;padding:12px 14px;margin-top:14px;background:var(--card)}
.rel-head{font-size:var(--t-caption2);font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--muted);margin-bottom:8px}
.rel-empty{color:var(--muted);font-size:var(--t-footnote)}
.rel-group{margin-bottom:10px}
.rel-gh{font-size:var(--t-caption);font-weight:650;color:var(--muted);display:flex;align-items:center;gap:6px;margin:6px 0 3px}
.rel-n{background:var(--line-soft);border-radius:999px;padding:0 7px;font-size:var(--t-caption2)}
.rel-row{display:flex;flex-direction:column;align-items:flex-start;width:100%;border:none;background:none;
  font:inherit;text-align:left;padding:5px 7px;border-radius:7px;cursor:pointer}
.rel-row:hover{background:#f4f6fa}
.rel-t{font-size:var(--t-footnote);color:var(--ink)}
.rel-s{font-size:var(--t-caption);color:var(--muted)}
/* Stated, never hidden — a count you can't open is information, not an error. */
.rel-blocked{font-size:var(--t-caption);color:var(--muted);font-style:italic;padding:4px 7px}
/* A related row with nowhere to go: still information, but not a control. */
.rel-row.rel-flat{cursor:default}
.rel-row.rel-flat:hover{background:none}
/* Column resizing in the shared view package. */
.mtg-table.vw-fixed{table-layout:fixed}
.mtg-table.vw-fixed td{overflow:hidden;text-overflow:ellipsis}
.mtg-table thead th{position:relative}
.vw-grip{position:absolute;top:0;right:-3px;width:7px;height:100%;cursor:col-resize;
  z-index:3;display:block}
.vw-grip:hover::after,body.vw-resizing .vw-grip::after{content:"";position:absolute;
  top:6px;bottom:6px;left:3px;width:1.5px;background:var(--accent,#2f6df6);border-radius:1px}
body.vw-resizing{cursor:col-resize;user-select:none}
.vw-wreset{text-align:right;padding:4px 2px 0}
.vw-wreset button{border:none;background:none;font:inherit;font-size:var(--t-caption);
  color:var(--muted);cursor:pointer;text-decoration:underline}
.vw-wreset button:hover{color:var(--ink)}
/* Tasks table on the shared package: keep the cell treatments the grid layout
   used to provide, now that the container is a real <table>. */
.mtg-table .tt-task{display:flex;align-items:center;gap:7px;min-width:0}
.mtg-table .tt-task .l-title{border:none;background:none;font:inherit;text-align:left;
  cursor:pointer;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mtg-table .tt-store-btn{border:none;background:none;font:inherit;font-size:var(--t-caption);
  cursor:pointer;display:inline-flex;align-items:center;gap:5px;color:var(--ink);padding:0}
.mtg-table .tt-due.od{color:#c0392b;font-weight:650}
.mtg-table .tt-owner{cursor:pointer;display:inline-flex}
.mtg-table tbody tr.done .l-title{text-decoration:line-through;color:var(--muted)}

/* ── Rails scroll INSIDE themselves, never the page (Chad, 2026-08-10) ──────
   "It would be nice to be able to scroll the meetings up and down without
    scrolling the whole screen... anytime there's a lot of things, scroll just
    that area."

   Two layers, deliberately:

   1. BOARD-WIDE BASELINE. Every module rail is already pinned to the viewport
      height; it just had no way to scroll what overflowed, so a long list
      pushed the PAGE instead and the filters above scrolled out of reach.
      Giving the rail its own overflow fixes that everywhere at once — Meetings,
      Equipment, Tasks, Knowledge, the calendar source list.

   2. PINNED HEADERS where a rail names its long section with `grow`. There the
      section absorbs the leftover height and only IT scrolls, so Views,
      Folders and Stores stay put while the list moves under them. This is the
      nicer behaviour, which is why it is opt-in per rail rather than guessed. */
.page-flush .mtg-rail,
.page-flush .kb-tree,
.page-flush .cal-side-col,
.mod-shell > .mod-rail{overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain}

/* CORRECTED after seeing it on Chad's real rail: the first version made the
   `grow` section absorb the leftover height and scroll on its own, with the
   headers pinned. That collapses to ZERO height the moment the sections above
   it are tall — and Chad's rail has 6 views, 11 folders and 7 stores, so the
   list got squeezed out of existence entirely.

   So the rail scrolls as one area instead. The headers scroll with the list,
   which is slightly less elegant, but it cannot collapse and it still does the
   thing that was asked for: the rail scrolls, the page does not. The list keeps
   a floor so it stays usable even in a cramped window. */
.msec-grow{display:flex;flex-direction:column;min-height:0}
/* The list must NOT keep its own scroller inside a scrolling rail — that gave
   two nested scrollbars and showed meetings through a 220px slot, which is
   worse than either choice on its own. The list runs full height; the rail is
   the one thing that scrolls. */
.msec-grow > .mtg-rail-list{flex:0 0 auto;min-height:0;max-height:none;overflow:visible}

/* Scrollbars stay slim so a scrolling rail doesn't read as a second panel. */
.page-flush .mtg-rail,
.mtg-rail-list,
.mod-shell > .mod-rail{scrollbar-width:thin}

/* List view's agenda in the main pane. Grouped by day; today and tomorrow are
   named so nobody has to do date arithmetic to find "what's next". */
.mtg-agenda{padding:2px 22px 30px;display:flex;flex-direction:column;gap:18px}
.ag-day{display:flex;flex-direction:column;gap:2px}
.ag-dhead{display:flex;align-items:baseline;gap:9px;padding:0 4px 6px;
  border-bottom:1px solid var(--line-soft);margin-bottom:4px}
.ag-dlabel{font-size:var(--t-body);font-weight:800;letter-spacing:-.2px;color:var(--ink)}
.ag-dsub{font-size:var(--t-caption);color:var(--muted)}
.ag-dn{margin-left:auto;font-size:var(--t-caption2);font-weight:700;color:var(--muted);
  background:var(--line-soft);border-radius:999px;padding:1px 8px}
.ag-row{display:flex;align-items:center;gap:14px;width:100%;border:none;background:none;
  font:inherit;text-align:left;padding:9px 8px;border-radius:10px;cursor:pointer}
.ag-row:hover{background:#f6f8fc}
.ag-time{flex:0 0 78px;font-size:var(--t-caption);font-weight:650;color:var(--muted);
  font-variant-numeric:tabular-nums}
.ag-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:3px}
.ag-title{font-size:var(--t-footnote);font-weight:600;color:var(--ink);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.ag-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.ag-who{font-size:var(--t-caption);color:var(--muted)}

/* ── The table scrolls in its own area, header pinned (Chad, 2026-08-10) ────
   "There's a meeting cut off in that screenshot that I can't see, but it needs
    to be there."

   The sticky <thead> was pinning to the PAGE's scroll container, so it slid up
   under the top bar and parked on top of the first data row — the row was
   rendered, just covered. Giving the table wrapper its own bounded height and
   overflow makes the header stick INSIDE the table instead, where it belongs,
   and it never overlaps a row again.

   Same principle Chad asked for on the rail: scroll the area, not the screen. */
.page-flush .mtg-doc{display:flex;flex-direction:column;min-height:0}
.page-flush .mtg-doc > .mtg-table-wrap,
.page-flush .mtg-doc > .mtg-agenda{flex:1 1 auto;min-height:0;overflow-y:auto;
  overscroll-behavior:contain}
/* The two sticky rows need an opaque backdrop or rows show through them as
   they pass underneath. */
.mtg-table thead th{background:var(--card)}
.mtg-table tr.mt-filters th{background:var(--card)}

/* ── Continuous calendars (Chad, 2026-08-10) ───────────────────────────────
   "If we could scroll and keep the calendars rolling up between the months,
    and even making years scrollable when we're on the year view."

   Month was one fixed 6-week page you stepped through, so the last week of a
   month and the first week of the next were never on screen together — which
   is exactly where "what's coming" lives. Year drew twelve months and made you
   page to cross December into January. Both now run continuously, with the
   month/year heading pinned so you always know what you're looking at while it
   rolls past. */
/* A TRUE GRID (Chad, 2026-08-14: "look at the spaces between the dates on the
   monthly calendar — I think that is wasted space, can we just make it a true
   grid, compared to the Apple calendar").

   Both axes carried a 6px gap, so every day floated as its own little card and
   six columns' worth of gutter ate space that could hold another line of
   events. The gap is now 1px and the container behind it is the LINE colour,
   so the gaps themselves become the hairlines — one grid, macOS-style, with no
   borders on the cells to double up or misalign at the edges. */
.cal-run{display:flex;flex-direction:column;gap:1px;background:var(--line-soft)}
/* One block per month. It is what scopes the sticky label: the name holds
   while you're inside the month and is pushed out by the next block arriving
   beneath it, instead of every label sticking at the top forever and stacking.
   The 1px gap between blocks keeps the hairline rhythm unbroken. */
.cal-month{display:flex;flex-direction:column;gap:1px;background:var(--line-soft)}
.cal-week{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:1px;background:var(--line-soft)}
/* The boundary label. Sticky under the weekday header so it names the month
   you're currently scrolled into rather than scrolling away with it.
   SOLID background, not the old fade: it is a child of the run, and the run's
   background is now the hairline colour, so a gradient to transparent would
   let that line colour show through behind the month name.

   Chad, 2026-08-14, second pass with the Mac side by side: the month name is
   LARGE and LEFT, and it rides up as you scroll until the next month takes
   over. My first pass shrank it to a quiet 12.5px marker — wrong read of
   "where the months are": the problem was never the size, it was that it sat
   in a full-width slab that cost a row of height and cut the grid in two.

   So it goes back to display size, left-aligned, sitting IN the run as a label
   rather than as a divider — sticky, so the month you are inside stays named at
   the top of the viewport and is pushed out by the next one arriving beneath
   it, which is the hand-off in Chad's screenshots. No band, no full-width rule,
   just the name over the grid. */
/* top = the pinned weekday header's measured height (wireCalendar publishes
   --cal-head-h on the scroller). At top:0 the label and the header both pinned
   to the same edge and the label painted OVER the header — Sun…Sat vanished
   the moment you scrolled, and the scroll-to-today maths, which only allowed
   for the header, left the label sitting on today's row with the dates half
   hidden (Chad, 2026-08-19). Header above, label under it, rows below both. */
.cal-msep{position:sticky;top:var(--cal-head-h,0px);z-index:4;background:var(--bg);
  padding:9px 2px 5px;margin-top:0;font-size:var(--t-title1);font-weight:800;letter-spacing:-.6px;
  color:var(--ink);text-transform:none;line-height:1.15;
  font-family:Poppins,Figtree,sans-serif}
/* The year sits back so the MONTH is what you read while scrolling, the way
   "October 2026" reads on the Mac. */
.cal-msep span{font-weight:800}
.cal-msep:first-child{margin-top:0}
/* The month you're actually in gets the accent, so it reads at a glance
   without needing size to do the work. */
.cal-msep.now{color:var(--accent,#2f6df6);font-weight:800}
.cal-run > .cal-week:first-of-type{margin-top:0}

/* Year: the same idea one level up. */
.yr-run{display:flex;flex-direction:column;gap:22px}
.yr-head{position:sticky;top:0;z-index:4;background:linear-gradient(var(--bg) 70%,transparent);
  padding:8px 2px 8px;font-size:var(--t-headline);font-weight:800;letter-spacing:-.3px;color:var(--ink)}
.yr-head.now{color:var(--accent,#2f6df6)}

/* RSVP note + resend (Chad, 2026-08-10). */
.gv-note{width:100%;margin-top:8px;padding:7px 10px;border:1px solid var(--line);
  border-radius:9px;font:inherit;font-size:var(--t-caption);color:var(--ink);background:#fff}
.gv-note::placeholder{color:var(--muted)}
.gv-rsvp-foot{display:flex;align-items:center;gap:10px;margin-top:6px}
.gv-rsvp-foot .field-hint{flex:1 1 auto;margin:0}
.gv-resend{flex:0 0 auto;font-size:var(--t-caption);padding:4px 10px}

/* ── The header pins as ONE unit (Chad, 2026-08-10) ────────────────────────
   "There still looks like there's a meeting that the drop down is covering."

   The two header rows were pinned SEPARATELY, with the filter row offset by a
   hardcoded 33px. The real header row is taller than that, so the filter row
   parked on top of the first meeting — the row was there, just underneath.
   Sticking the whole <thead> instead removes the offset arithmetic: both rows
   travel together and the body always begins below them, whatever the header
   ends up measuring. */
.mtg-table thead{position:sticky;top:0;z-index:3;background:var(--card)}
.mtg-table thead tr{background:var(--card)}
/* An opaque backdrop on the whole head, or rows show through it as they pass. */
.mtg-table thead::after{content:"";position:absolute;left:0;right:0;bottom:0;
  height:1px;background:var(--line)}

/* ═══ PHONE: collapse the two-column shell (Chad, 2026-08-10) ═══════════════
   "The view on the phone is very hard to read for meetings. It's very cut off."

   Root cause, measured at 375px: `.page-flush` is a GRID —
   `grid-template-columns: var(--rail-w) minmax(0,1fr)` — so the rail column
   claimed a hard 240px and the content pane was left with **107px**. The
   existing phone block did neutralise the rail's own width, but that never
   mattered: the width came from the grid TRACK, not from the rail.

   `.mod-shell` modules (Tasks, Equipment) were already handled — they collapse
   to display:block and the rail becomes a short scrolling strip. This does the
   same for the `.page-flush` family (Meetings, Knowledge, Calendar), so the
   whole app now behaves one way on a phone instead of two. */
@media (max-width: 767px){
  .page.page-flush:has(> .mtg-shell),
  .page.page-flush:has(> .kb-shell),
  .page.page-flush:has(> .cal-flex){display:block !important}
  /* These are display:contents on desktop so the grid can see through them.
     With the grid gone they have to lay their own children out again. */
  .page-flush > .mtg-shell,
  .page-flush > .kb-shell,
  .page-flush > .cal-flex{display:block !important}
  /* The rail stops being a full-height column and becomes the same capped,
     scrollable strip .mod-rail already uses on a phone. */
  .page-flush .mtg-rail,
  .page-flush .kb-tree,
  .page-flush .cal-side-col{
    grid-column:auto !important;grid-row:auto !important;
    position:static !important;width:auto !important;max-width:none !important;
    height:auto !important;max-height:150px !important;overflow:auto;
    margin:0 0 10px !important;border-radius:0;border-right:none;
    border-bottom:1px solid var(--line-soft)}
  /* …and the content finally gets the whole screen. */
  .page-flush .mtg-doc,
  .page-flush .kb-doc{grid-column:auto !important;grid-row:auto !important;min-width:0}
  .page-flush > .topbar{grid-column:auto !important;padding-left:0 !important}

  /* A 7-column table cannot be read on a 375px screen. The wrap already
     scrolls, but scrolling to find the date of every row is not reading — so
     drop the columns that are either repeated elsewhere on the row or rarely
     the reason you opened the table, and keep Meeting / Date / Status. */
  .mtg-table th:nth-child(3), .mtg-table td:nth-child(3),   /* Type   */
  .mtg-table th:nth-child(5), .mtg-table td:nth-child(5),   /* Store  */
  .mtg-table th:nth-child(6), .mtg-table td:nth-child(6),   /* Attendees */
  .mtg-table th:nth-child(7), .mtg-table td:nth-child(7){display:none}
  .mtg-table.vw-fixed{table-layout:auto}
  .mtg-table.vw-fixed colgroup{display:none}
  .vw-grip{display:none}          /* no column dragging on a touch screen */

  /* The agenda is the view that actually works on a phone — give it room. */
  .mtg-agenda{padding:2px 0 24px}
  .ag-time{flex:0 0 62px;font-size:var(--t-caption)}
  .ag-row{gap:10px;padding:10px 6px}

  /* Month cells at 375px: 7 tracks means ~50px each, so the pills need to be
     small and the cell short or a month is ten screens tall. */
  .cal-cell,.mm-cell{min-height:58px;padding:3px 4px}
  .cal-msep{font-size:var(--t-footnote);padding:8px 2px 4px}
}

/* ── PHONE: the top bar has to wrap ────────────────────────────────────────
   Measured at 375px: .topbar-actions ran 123px past the right edge and
   .topbar-views collapsed to ZERO width, which is why the view tabs rendered
   as a clipped "Lis". Desktop deliberately pins .topbar-main to one nowrap row
   at a fixed height — that is the rule that stops the bar changing shape
   between modules — but on a phone it just pushes the controls off-screen.
   Here it wraps, and each zone gets its own line. */
@media (max-width: 767px){
  /* #179 promoted the calendar's 1fr/auto/1fr grid to every desktop topbar —
     on a phone that grid would pin three columns nobody has room for, so
     flex comes back first (the wrap rules below assume it). */
  .topbar-main{display:flex !important}
  .topbar-main{flex-wrap:wrap !important;height:auto !important;
    align-items:center;row-gap:8px;column-gap:10px}
  .topbar-title{flex:1 1 auto;min-width:0}
  .topbar-title h1{font-size:var(--t-title3);white-space:normal}
  /* Tabs and actions each take a full row rather than fighting for the
     remainder of one. */
  .topbar-views{flex:1 0 100%;margin-left:0;order:3}
  /* SCOPED to the shared view package's own switcher (.vw-switch). The first
     version said `.topbar-views .view-tabs`, which also matched the MODULE
     switcher inside .view-wrap — deliberately collapsed to a dropdown below
     1379px — and because components.css loads after styles.css at equal
     specificity, it resurrected the tab strip so BOTH it and its replacement
     dropdown rendered, pushing tabs off the edge on Tasks and Calendar. */
  .topbar-views .vw-switch{width:100%;display:flex}
  .topbar-views .vw-switch .view-tab{flex:1 1 0;text-align:center;white-space:nowrap}
  .topbar-actions{flex:0 0 auto;justify-content:flex-end;flex-wrap:wrap;
    max-width:100%;order:2}

  /* Nothing in the filter row may exceed the screen. */
  .topbar-filters{max-width:100%}
  .topbar-filters .filter-row{width:100%}
  .hub-fsel,.topbar-filters select,.topbar-filters input[type=date]{
    max-width:100%;min-width:0;flex:1 1 auto}
  .topbar-filters .pn-listtop{flex:1 1 100%;max-width:none}
  .topbar-filters .seg{flex-wrap:wrap}

  /* The rail strip scrolls vertically; it must never scroll the page sideways. */
  .page-flush .mtg-rail,.mod-shell > .mod-rail{overflow-x:hidden}
  .mtg-row,.pn-fol,.mtg-vw{max-width:100%}
  .mtg-row-t{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
}

/* ── PHONE: get a meeting on screen without scrolling half a page ──────────
   After the wrap fix the bar was correct but expensive: title, actions, tabs,
   scope, when, search, kind, folder and TWO date inputs stacked to ~700px
   before a single meeting appeared. Correct is not the same as usable.

   So the secondary filters (kind / folder / dates) become ONE horizontally
   scrolling line instead of three stacked rows — the same treatment the
   desktop bar already gives an overflowing filter row — and the rail strip
   gets shorter. Scope, when and search stay full-size because those are the
   three people actually touch. */
@media (max-width: 767px){
  .topbar-filters .filter-row:nth-child(2){
    flex-wrap:nowrap !important;overflow-x:auto;padding-bottom:2px;
    scrollbar-width:none}
  .topbar-filters .filter-row:nth-child(2)::-webkit-scrollbar{display:none}
  .topbar-filters .filter-row:nth-child(2) > *{flex:0 0 auto !important;
    min-width:0;width:auto !important;max-width:60vw}
  .topbar-filters input[type=date]{max-width:9.5rem}

  /* The rail is a jump-list here, not a workspace. */
  .page-flush .mtg-rail,.mod-shell > .mod-rail{max-height:118px !important}

  /* The primary action was clipping past the right edge — let the bar give it
     room instead of letting it run off. */
  .topbar-actions{gap:6px}
  .topbar-actions .btn{white-space:nowrap}
  .topbar-actions .btn-primary{padding-left:12px;padding-right:12px}
  .page,.page-flush{padding-right:14px !important}
}

/* The top bar was escaping the page's right padding, so the primary action sat
   flush against the screen edge — it measured exactly 375 of 375, which reads
   as "clipped" even though nothing overflowed. */
@media (max-width: 767px){
  .page-flush > .topbar,.page > .topbar{box-sizing:border-box;max-width:100%;
    padding-right:14px !important}
}

/* The module view switcher (.view-wrap) is flex:none, so on a phone it refused
   to shrink and pushed itself past the right edge on Tasks and Calendar — the
   same failure the Meetings tabs had, in a different control. It may shrink
   here, and its dropdown menu is pinned inside the screen. */
@media (max-width: 767px){
  .view-wrap{flex:0 1 auto !important;min-width:0;max-width:100%}
  .view-wrap .view-btn{max-width:100%;overflow:hidden;text-overflow:ellipsis}
  .view-wrap .view-menu{right:0;left:auto;max-width:calc(100vw - 28px)}
}

/* The month header (‹ › Today + the count) ran past the edge on a phone. */
@media (max-width: 767px){
  .mm-head{flex-wrap:wrap;row-gap:6px}
  .mm-head h3{font-size:var(--t-headline);min-width:0;flex:1 1 auto}
  .mm-note{flex:1 1 100%;margin-left:0;text-align:left}
  .mtg-month{padding:2px 0 24px}
}

/* ── TABLET: the top bar must wrap here too (audit, 2026-08-10) ────────────
   Full-OS audit at 768px: Equipment's action cluster ran to 901px and
   Contacts' to 1000px inside a 768px screen, because .topbar-main is
   flex-wrap:nowrap at a fixed height. That rule is right on a wide desktop —
   it is what stops the bar changing shape between modules — but any module
   carrying a search box AND a primary button overflows it below ~1200px.

   Same treatment as the phone, one breakpoint up: the bar may grow a second
   line, and the search input may shrink. Desktop (>=1200px) is untouched and
   still measures 28/28 clean. */
@media (min-width: 768px) and (max-width: 1199px){
  .topbar-main{flex-wrap:wrap !important;height:auto !important;row-gap:8px}
  .topbar-actions{flex:1 1 auto;flex-wrap:wrap;justify-content:flex-end;
    max-width:100%;min-width:0}
  .topbar-actions .doc-search,
  .topbar-actions input[type=search],
  .topbar-actions input[type=text]{flex:1 1 180px;min-width:0;max-width:100%}
  .topbar-actions .btn{white-space:nowrap;flex:0 0 auto}
  .topbar-title{min-width:0}
  .topbar-title h1{white-space:normal}
}

/* ── PHONE: task rows get a readable title (audit, 2026-08-10) ─────────────
   Measured: the title had 85px of a 316px row, so every task read
   "Bank checkli…" and you could not tell one from another. The single-line
   grid gave 84px to the status pill, 56 to the due chip and 26 to avatars
   before the title got anything.

   The row becomes two lines by explicit grid placement — no markup change:
     line 1  check | TITLE (all remaining width) | who
     line 2        | status                      | due
   Which is the order you actually read it in. */
@media (max-width: 767px){
  .lrow,.lrow.no-store{
    grid-template-columns:19px minmax(0,1fr) auto !important;
    grid-auto-rows:auto;row-gap:5px;column-gap:8px;align-items:center;
    padding:10px 8px !important}
  .lrow .l-check{grid-column:1;grid-row:1}
  .lrow .l-main {grid-column:2;grid-row:1;min-width:0}
  .lrow .l-right{grid-column:3;grid-row:1;justify-self:end}
  .lrow .status-pill{grid-column:2;grid-row:2;justify-self:start;width:auto !important;
    padding:4px 12px !important;font-size:var(--t-caption2) !important}
  .lrow .l-due{grid-column:3;grid-row:2;justify-self:end;white-space:nowrap}
  .lrow .l-pri,.lrow .l-store,.lrow>.l-cell-empty{display:none}
  /* The title may now use the space it was given. */
  .lrow .l-title{font-size:var(--t-body);white-space:normal;overflow:hidden;
    display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
    line-height:1.3;text-align:left}

  /* The primary action was still clipping on modules that use .page (not
     .page-flush) — cover both shells. */
  .page > .topbar,.page-flush > .topbar,.mod-shell ~ .topbar{
    box-sizing:border-box;max-width:100%;padding-right:14px !important}
  .topbar-actions{max-width:100%}
}

/* ── PHONE: two structural fixes the audit surfaced ────────────────────────
   1. The primary action still clipped because the top bar is nested
      (topbar > mod-doc > mod-shell > page) — my earlier `.page > .topbar`
      selector never matched it. Target the bar wherever it lives and let the
      action cluster shrink instead of overflowing its own bar.
   2. The rail is the FIRST child of the shell, so "FILTER" rendered ABOVE the
      module's own title — you met the filters before you knew what screen you
      were on. On a phone the title leads and the rail follows. */
@media (max-width: 767px){
  /* 14px so the primary action has the same breathing room as the page,
     instead of sitting flush against the glass. */
  .topbar{box-sizing:border-box;max-width:100%;padding-right:14px !important}
  .topbar-main{max-width:100%;min-width:0}
  .topbar-actions{min-width:0;overflow:visible}
  .topbar-actions > *{max-width:100%}
  .topbar-actions .btn{flex-shrink:1;min-width:0;overflow:hidden;
    text-overflow:ellipsis}

  .mod-shell,.page-flush > .mtg-shell{display:flex !important;flex-direction:column}
  .mod-shell > .mod-doc,.page-flush > .mtg-shell > .mtg-doc{order:1}
  .mod-shell > .mod-rail,.mod-shell > .mtg-rail,
  .page-flush > .mtg-shell > .mtg-rail{order:2;margin:10px 0 0 !important;
    border-bottom:none;border-top:1px solid var(--line-soft)}
}

/* Flex children default to min-width:auto and refuse to shrink below their
   content — which is how making .mod-shell a flex column (to reorder the rail
   below the content) left .mod-doc 519px wide inside a 347px shell on the
   calendar screens. Same trap as the rail scroller earlier today; it needs
   stating once per flex container, not once per project. */
@media (max-width: 767px){
  .mod-shell > *,.page-flush > .mtg-shell > *{min-width:0}
}

/* min-width:0 was not enough on the calendar screens — .mod-doc still measured
   519px inside a 347px shell because its own contents (the calendar's wide
   title/nav cluster) set its min-content width. Cap it outright: on a phone the
   content pane is the screen, never wider. */
@media (max-width: 767px){
  .mod-shell > .mod-doc,.page-flush > .mtg-shell > .mtg-doc{
    width:100% !important;max-width:100% !important;box-sizing:border-box}
  .topbar,.topbar-main,.topbar-title,.topbar-views,.topbar-actions,.topbar-filters{
    max-width:100%;min-width:0}
  .topbar-title h1{overflow-wrap:anywhere}
}

/* ── PHONE: the rail was a narrow white stub ───────────────────────────────
   Chad's real-device screenshots showed a tall, mostly-empty white box beside
   the content. Cause: .page-flush sets align-self:start on the rail so that
   `position:sticky` works in the DESKTOP GRID. When I made the shell a flex
   COLUMN to reorder the rail below the content, that same align-self became a
   CROSS-AXIS instruction and collapsed the rail to its content width — 179px
   of a 393px screen, which reads as a broken empty panel.

   On a phone it stretches, and if it has nothing in it, it isn't drawn at all. */
@media (max-width: 767px){
  .mod-shell > .mod-rail,.mod-shell > .mtg-rail,
  .page-flush .mtg-rail,.page-flush .cal-side-col,.page-flush .kb-tree{
    align-self:stretch !important;width:100% !important;max-width:100% !important;
    box-sizing:border-box}
  /* An empty rail is worse than no rail — it reads as a rendering fault. */
  .mod-rail:empty,.mtg-rail:empty,.cal-side-col:empty{display:none !important}
}

/* ── PHONE: content tables scroll instead of crushing ──────────────────────
   Chad's screenshot of a KB credentials page showed a 3-column table wrapping
   one word per line — "ffc-0002_houma@k / aleohospitality.c / om" — which is
   text on screen but not readable text.

   A table with real columns cannot fit 393px, and squeezing it is the worst of
   the options. It scrolls sideways instead, with each column kept wide enough
   to read. This is scoped to CONTENT tables (KB articles, documents) and
   deliberately excludes .mtg-table, which already has its own phone treatment
   of dropping columns. */
@media (max-width: 767px){
  .kb-doc,.mod-doc .doc-body,.mod-doc .kb-body{overflow-x:auto;
    -webkit-overflow-scrolling:touch}
  .kb-doc table:not(.mtg-table),
  .mod-doc .doc-body table:not(.mtg-table),
  .mod-doc .kb-body table:not(.mtg-table){min-width:520px;table-layout:auto}
  .kb-doc table:not(.mtg-table) td,.kb-doc table:not(.mtg-table) th,
  .mod-doc .doc-body table:not(.mtg-table) td,
  .mod-doc .doc-body table:not(.mtg-table) th{min-width:140px;
    overflow-wrap:normal;word-break:normal;padding:8px 10px}
  /* Long unbroken strings (emails, URLs) may break, but only where they must. */
  .kb-doc td,.mod-doc .doc-body td{overflow-wrap:anywhere}
}

/* ── PHONE: two corrections to my own earlier phone rules ──────────────────
   1. I gave .topbar-actions .btn flex-shrink:1 + overflow:hidden while chasing
      an overflow that turned out not to exist. The button then squeezed to
      108px and CLIPPED ITS OWN LABEL — "+ New event" lost its last letters.
      The bar already wraps, so the button never needed to shrink: it keeps its
      natural width and the row wraps instead.
   2. The module view switcher was given its own full-width row, where a 50px
      dropdown trigger sits alone looking like a stray control. It belongs
      beside the title, which is where it is on every other size. */
@media (max-width: 767px){
  .topbar-actions .btn{flex:0 0 auto !important;overflow:visible !important;
    text-overflow:clip !important;min-width:max-content}
  .topbar-actions{flex-wrap:wrap;row-gap:8px}

  .topbar-views{flex:0 0 auto !important;order:1 !important;margin-left:8px}
  .topbar-views .view-wrap{flex:0 0 auto !important}
  /* The package's own List/Table/Calendar strip still wants the full row —
     it is a segmented control, not a dropdown. */
  .topbar-views:has(.vw-switch){flex:1 0 100% !important;order:3 !important;margin-left:0}
}

/* The desktop bar scrolls horizontally when it overflows; on a phone it WRAPS
   instead, so that scroll track is a grey bar under the buttons signalling an
   overflow that no longer exists. And the primary button had exactly zero
   slack (content width == box width), so its label sat flush against its own
   edge. Both are leftovers from the desktop rules. */
@media (max-width: 767px){
  .topbar-main,.topbar-filters{overflow-x:visible !important}
  .topbar-filters .filter-row:nth-child(2){overflow-x:auto}   /* this one still scrolls on purpose */
  .topbar-actions .btn,.topbar-actions .btn-primary{
    padding-left:14px !important;padding-right:14px !important}
}

/* ── TABLET: task rows too ─────────────────────────────────────────────────
   The upgraded audit probe (which now catches a label clipped INSIDE its own
   control, not just elements past the screen edge) found the same disease one
   breakpoint up: on a store Tasks list at 768px the title got 52px while
   needing 200-345. The seven fixed tracks of the desktop row simply do not fit
   beside a rail at tablet width.

   Same two-line treatment as the phone: title on its own line, status and due
   beneath. */
@media (min-width: 768px) and (max-width: 1199px){
  .lrow,.lrow.no-store{
    grid-template-columns:19px minmax(0,1fr) auto !important;
    grid-auto-rows:auto;row-gap:5px;column-gap:9px;align-items:center}
  .lrow .l-check{grid-column:1;grid-row:1}
  .lrow .l-main {grid-column:2;grid-row:1;min-width:0}
  .lrow .l-right{grid-column:3;grid-row:1;justify-self:end}
  .lrow .status-pill{grid-column:2;grid-row:2;justify-self:start;width:auto !important}
  .lrow .l-due{grid-column:3;grid-row:2;justify-self:end;white-space:nowrap}
  .lrow .l-pri,.lrow .l-store,.lrow>.l-cell-empty{display:none}
  .lrow .l-title{white-space:normal;overflow:hidden;display:-webkit-box;
    -webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.3;text-align:left}
}

/* ── A meeting row must answer the mouse ──────────────────────────────────
   Chad, 2026-08-11: "there's no indication of like you're actually clicking
   the meeting... it should highlight or underline when you put your mouse
   over it and it should show some indication that you've actually clicked
   it." Three states, not one: hover (you could click this), press (you did),
   and opening (we heard you and we're fetching it). The opening state is the
   important one — the fetch is no longer 16 s, but it is never free, and a
   row that looks inert gets clicked again. */
.mtg-row, .ag-row, [data-vwrow], [data-mtg]{
  cursor:pointer;
  transition:background .12s ease, transform .06s ease;
}
.mtg-row:hover .mtg-row-t,
.ag-row:hover .ag-title,
[data-vwrow]:hover .vw-title{ text-decoration:underline; text-underline-offset:2px; }
.mtg-row:active, .ag-row:active, [data-vwrow]:active{ transform:translateY(1px); }
.mtg-row:focus-visible, .ag-row:focus-visible, [data-vwrow]:focus-visible{
  outline:2px solid var(--blue-deep, #1733a5); outline-offset:-2px; border-radius:10px;
}
/* clicked — held until the meeting is on screen */
.row-opening{ background:rgba(23,51,165,.12) !important; position:relative; }
.row-opening .mtg-row-t, .row-opening .ag-title{ color:var(--blue-deep,#1733a5); }
.row-opening::after{
  content:""; position:absolute; left:0; right:0; bottom:0; height:2px;
  background:var(--blue-deep,#1733a5); animation:rowOpening 1s ease-in-out infinite;
}
@keyframes rowOpening{
  0%{ transform:scaleX(0); transform-origin:left; }
  50%{ transform:scaleX(1); transform-origin:left; }
  100%{ transform:scaleX(0); transform-origin:right; }
}
@media (prefers-reduced-motion:reduce){
  .row-opening::after{ animation:none; transform:scaleX(1); }
  .mtg-row:active,.ag-row:active,[data-vwrow]:active{ transform:none; }
}

/* Sort control above the meeting list */
.mtg-sortbar{display:flex;gap:4px;padding:4px 8px 6px}
.mtg-sort{flex:1;border:1px solid var(--line,#dfe3ee);background:#fff;border-radius:8px;
  padding:3px 6px;font-size:var(--t-caption2);font-weight:600;color:var(--muted,#6b7280);cursor:pointer}
.mtg-sort:hover{background:#f6f8fc}
.mtg-sort.on{background:rgba(23,51,165,.10);border-color:rgba(23,51,165,.35);color:var(--blue-deep,#1733a5)}
/* A topic only you can see */
.topic-private{display:inline-block;margin-left:6px;padding:1px 6px;border-radius:999px;
  background:rgba(23,51,165,.10);color:var(--blue-deep,#1733a5);font-size:var(--t-caption2);font-weight:700}

/* Meeting rail: collapsible time buckets */
.mtg-bucket{border-bottom:1px solid var(--line,#eef1f7)}
.mtg-bhead{display:flex;align-items:center;gap:7px;width:100%;border:0;background:none;
  padding:7px 10px;cursor:pointer;font:inherit;font-size:var(--t-caption);font-weight:700;
  letter-spacing:.03em;text-transform:uppercase;color:var(--muted,#6b7280)}
.mtg-bhead:hover{background:#f6f8fc}
.mtg-bhead.open{color:var(--blue-deep,#1733a5)}
.mtg-bcaret{width:10px;flex:none;font-size:var(--t-caption2)}
.mtg-blabel{flex:1;text-align:left}
.mtg-bhead .pn-count{flex:none}

/* ── Meetings sidebar, Notion-style ──────────────────────────────────────── */
.mtg-side-sec{padding:8px 6px;border-bottom:1px solid var(--line,#eef1f7)}
.mtg-side-h{display:flex;align-items:center;justify-content:space-between;
  padding:2px 8px 6px;font-size:var(--t-caption2);font-weight:700;letter-spacing:.05em;
  text-transform:uppercase;color:var(--muted,#8a93a6)}
.mtg-side-clear{border:0;background:none;cursor:pointer;font:inherit;font-size:var(--t-caption2);
  font-weight:600;text-transform:none;letter-spacing:0;color:var(--blue-deep,#1733a5)}
.mtg-side-clear:hover{text-decoration:underline}
.mtg-side-row{display:flex;align-items:center;gap:8px;width:100%;border:0;background:none;
  padding:6px 8px;border-radius:8px;cursor:pointer;font:inherit;font-size:var(--t-footnote);
  color:var(--ink,#1b2130);text-align:left}
.mtg-side-row:hover{background:#f2f5fb}
.mtg-side-row.on{background:rgba(23,51,165,.10);color:var(--blue-deep,#1733a5);font-weight:600}
.mtg-side-l{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mtg-side-row .pn-count{flex:none;font-size:var(--t-caption2);color:var(--muted,#8a93a6)}
.mtg-side-row.cal input{margin:0;flex:none}
.mtg-side-row .bdot{width:9px;height:9px;border-radius:50%;flex:none}
.mtg-side-row .fchip-logo{width:15px;height:15px;border-radius:3px;flex:none}

/* ── The day list needs lines you can read across ───────────────────────── */
.ag-day{margin-bottom:18px}
.ag-day .ag-row{border-bottom:1px solid var(--line,#eef1f7)}
.ag-day .ag-row:last-child{border-bottom:0}
.ag-dhead{border-bottom:2px solid var(--line,#e3e8f2);margin-bottom:0;padding-bottom:7px}
.ag-row{padding:11px 6px}
.ag-row:hover{background:#f6f8fc}

/* Accept / Maybe / Decline on an invitation row */
.ag-rsvp{display:flex;gap:6px;flex:none}
.ag-rsvp button{border:1px solid var(--line,#dfe3ee);background:#fff;border-radius:8px;
  padding:4px 11px;font:inherit;font-size:var(--t-caption);font-weight:600;cursor:pointer;color:var(--ink,#1b2130)}
.ag-rsvp button:hover{background:#f2f5fb}
.ag-rsvp button:disabled{opacity:.5;cursor:default}
.ag-rsvp .ag-yes{background:#0f9d58;border-color:#0f9d58;color:#fff}
.ag-rsvp .ag-yes:hover{background:#0c8047}
.ag-rsvp .ag-no:hover{background:#fdecea;border-color:#e0473a;color:#c5372c}

/* The way back out of an open meeting */
.mm-back{display:inline-flex;align-items:center;gap:6px;margin:0 0 12px;
  border:1px solid var(--line,#dfe3ee);background:#fff;border-radius:9px;
  padding:5px 12px;font:inherit;font-size:var(--t-caption);font-weight:600;cursor:pointer;
  color:var(--blue-deep,#1733a5)}
.mm-back:hover{background:#f2f5fb;border-color:rgba(23,51,165,.35)}

/* Fluid week: columns keep a real width so the extra days overflow and can be
   scrolled to, instead of 7 columns stretching to fit and leaving nothing to
   scroll. --tg-colw is a seventh of the grid, so a screenful is still a week. */
.tg-days-wide{--tg-colw:calc((100vw - 620px) / 7);}
@media (max-width:1199px){ .tg-days-wide{--tg-colw:calc((100vw - 360px) / 7)} }
@media (max-width:767px){ .tg-days-wide{--tg-colw:46vw} }
.tg-scroll{overflow-x:auto;scroll-behavior:auto;overscroll-behavior-x:contain}
.tg-scroll::-webkit-scrollbar{height:9px}
.tg-scroll::-webkit-scrollbar-thumb{background:rgba(23,51,165,.22);border-radius:99px}
.tg-scroll::-webkit-scrollbar-thumb:hover{background:rgba(23,51,165,.38)}

/* ══════════════════════════════════════════════════════════════════════════
   5. MOTION — one protocol, every module
   Chad, 2026-08-13: "make the entire website grow fluently, make everything
   very smooth and the way it moves in transitions ... make every module follow
   the same protocol as far as scrolling — week arrows go a week at a time,
   days go a day at a time, we can go left and right; the only thing that goes
   up and down [is] lists."

   Two halves. 5a is the FEEL: one easing and duration scale applied to the
   controls the whole app is built from, so a chip in Meetings reacts at
   exactly the speed of a chip in Cash. 5b is the SCROLL PROTOCOL: time moves
   sideways, lists move down, in every module that has both.

   Deliberately NOT `*{transition:all}`. That animates layout properties
   (width, height, top) on every element in a 24k-line app, so the grid
   visibly re-flows on each render — and this app re-renders on nearly every
   click. Only compositor-cheap properties are animated: colour, opacity,
   transform, shadow, border.

   Equally deliberately, there is NO page-level fade. render() rebuilds #main
   for filters, chip clicks and the calendar's own scroll hand-off, so a
   .page animation would flicker on all of them — the opposite of what was
   asked for. Arrival animation is reserved for things that genuinely appear.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 5a. FEEL ─────────────────────────────────────────────────────────────
   Every clickable surface reacts on the same curve. transform is in the list
   so the press states below have something to animate against. */
.btn, .fchip, .view-tab, .view-item, .nav-item, .side-sec-h, .pn-fol-head,
.tg-pill, .cal-pill, .tg-ev, .cal-cell, .hub-tile, .card, .list-card,
.mtg-row, .mtg-side-row, .side-row, .ct-row, .cs-row, .fin-row, .hr-row,
.lrow, .ag-row, .field-row, .lz, .x-close, .cal-arrow, .mm-back, .gt-zoom-b,
.mtg-bhead, .mtg-sort, .att-chip, .tg-dhead, input, select, textarea {
  transition:
    background-color var(--dur-1) var(--ease),
    border-color     var(--dur-1) var(--ease),
    color            var(--dur-1) var(--ease),
    box-shadow       var(--dur-2) var(--ease),
    opacity          var(--dur-2) var(--ease),
    transform        var(--dur-2) var(--ease);
}

/* A press should be felt, not just seen. 1px is enough — more than that and
   rows appear to bounce as you click down a long list. */
.btn:active, .fchip:active, .tg-pill:active, .cal-pill:active, .view-tab:active,
.cal-arrow:active, .mm-back:active, .x-close:active, .gt-zoom-b:active,
.hub-tile:active { transform:translateY(1px) }

/* Clickable ROWS lift slightly under the cursor, so the target is obvious
   before you commit — the affordance Chad asked for on meetings, applied
   everywhere a row is clickable rather than only there. Scoped to row
   classes; .card is not included because most cards are not click targets. */
.mtg-row:hover, .mtg-side-row:hover, .ct-row:hover, .cs-row:hover,
.fin-row:hover, .hr-row:hover, .lrow:hover, .ag-row:hover, .list-card:hover {
  box-shadow:0 1px 3px rgba(20,26,60,.10);
}

/* Focus rings arrive rather than snap. */
:focus-visible { transition:box-shadow var(--dur-1) var(--ease) }

/* Things the app draws ON TOP get a short rise as they appear. Restricted to
   overlays, which are created on demand — never to anything that lives inside
   #main and is therefore rebuilt on every render. */
.modal, .sheet, .toast, .ai-panel {
  animation:khRise var(--dur-3) var(--ease) both;
}
@keyframes khRise { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:none} }

/* ── 5b. SCROLL PROTOCOL ──────────────────────────────────────────────────
   Sideways = time. Down = list. Both get momentum, and neither leaks its
   gesture to the page behind it (overscroll-behavior) — that leak is what
   makes a trackpad swipe across a week feel like two scrollers fighting
   instead of one surface. */
.tg-scroll, .tg-strip, .gt-scroll, .gb-kan, .kanban, .topbar-views,
.topbar-filters, .ct-table-wrap, .fb-table-wrap, .vault-card, .kb-body {
  -webkit-overflow-scrolling:touch;
  overscroll-behavior-x:contain;
  scrollbar-width:thin;
}
.nav, .mtg-rail-list, .ai-log {
  overscroll-behavior-y:contain;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:thin;
}

/* Day view renders the SAME 28-column window as week (app.js) — the only
   difference is that one column is the whole pane, so one screenful is one
   day. These mirror .tg-days-wide's breakpoints exactly; they have to, or day
   and week would park on different pixels at the same window size. */
.tg-days-oneday{ --tg-colw:calc(100vw - 620px) }
@media (max-width:1199px){ .tg-days-oneday{ --tg-colw:calc(100vw - 360px) } }
@media (max-width:767px){  .tg-days-oneday{ --tg-colw:100vw } }

/* ── 5c. REDUCED MOTION ───────────────────────────────────────────────────
   Everything in 5a is decoration: if the OS says no animation, there is none.
   5b stays — containment and momentum are behaviour, not decoration. */
@media (prefers-reduced-motion:reduce){
  .modal, .sheet, .toast, .ai-panel { animation:none !important }
  .btn:active, .fchip:active, .tg-pill:active, .cal-pill:active, .view-tab:active,
  .cal-arrow:active, .mm-back:active, .x-close:active, .gt-zoom-b:active,
  .hub-tile:active { transform:none }
}

/* ══════════════════════════════════════════════════════════════════════════
   6. AGENDA LIST — columns, not two stacked lines
   Chad, 2026-08-14: "Instead of having two lines with some of the information
   below it, can you move some of the information out ... maybe column style of
   where the information lands would be great, there's a lot of real estate in
   the open area between scheduled and the meeting invite. You could also maybe
   have different shadings of the meeting ... so they don't get lost."

   Two problems in one screenshot: every row was TWO lines tall because the
   store chip, the headcount and the flags wrapped under the title — and the
   whole middle of the row was empty while they did it. And with 5 identical
   white rows stacked up, nothing separated one meeting from the next.

   The fix is a grid. Each fact gets a column at a fixed x-position, so the eye
   reads DOWN a column instead of re-parsing every row. Rows are one line tall,
   which fits ~40% more meetings on screen.
   ══════════════════════════════════════════════════════════════════════════ */

.ag-row{
  display:grid;
  /* time | title | store | headcount | flags | status */
  grid-template-columns:74px minmax(0,1fr) 132px 74px 150px 116px;
  align-items:center;
  gap:12px;
  padding:9px 10px 9px 0;
  text-align:left;
  border-left:3px solid transparent;   /* store tint lives here */
  border-radius:0 8px 8px 0;
}
.ag-title{
  /* No longer inside .ag-main — it is its own grid cell, so it must clamp on
     its own or a long title pushes every column out of alignment. */
  font-size:var(--t-footnote);font-weight:600;color:var(--ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;
}
.ag-c{display:flex;align-items:center;gap:6px;min-width:0}
.ag-c-who{justify-content:flex-start}
.ag-c-flags{flex-wrap:nowrap;overflow:hidden}
.ag-c-st{justify-content:flex-end}
.ag-time{flex:none;text-align:right}

/* ── Striping. Chad: "one would be that light blue, one would be white."
   Zebra rather than colour-by-store, because the store is ALREADY named in its
   own column — tinting the whole row by brand as well would say the same thing
   twice and leave no neutral ground for hover to work against. */
/* nth-of-TYPE, not nth-child. .ag-dhead is the day's first child, so
   nth-child(even) made the FIRST meeting of every day the tinted one — and on
   the many days that hold a single meeting, that read as "every row is blue".
   .ag-row is a <button> and the header is a <div>, so counting by type counts
   meetings only, and each day restarts cleanly on white. */
.ag-day .ag-row:nth-of-type(even){background:#f5f8fd}
.ag-day .ag-row:hover{background:#e8f0fc}
.ag-day .ag-row{border-bottom:none}       /* the stripe IS the separator now */

/* ── Store tint on the left edge only. A 3px bar groups a run of meetings for
   the same store at a glance without colouring the text or fighting the
   stripe. Chad's palette: blues/teals/greens, never orange. */
.ag-row.ag-b-csc{border-left-color:var(--csc,#69b048)}
.ag-row.ag-b-ffc{border-left-color:var(--ffc,#e31937)}
.ag-row.ag-b-kh{border-left-color:var(--kh,#1733a5)}
.ag-row.ag-b-me{border-left-color:#c9d2e6}

/* Keep the headcount quiet — it is a supporting number, not a heading. */
.ag-who{font-size:var(--t-caption);color:var(--muted);white-space:nowrap}

/* ── Phones: no room for six columns, so fall back to the stacked shape. The
   stripe and the store bar survive, which is what carries legibility here. */
@media (max-width:900px){
  .ag-row{
    grid-template-columns:64px minmax(0,1fr) auto;
    grid-template-areas:"time title status" "time meta status";
    row-gap:3px;
  }
  .ag-time  {grid-area:time}
  .ag-title {grid-area:title}
  .ag-c-st  {grid-area:status}
  .ag-c-store,.ag-c-who,.ag-c-flags{grid-area:meta;justify-content:flex-start}
  .ag-c-who  {padding-left:96px}
  .ag-c-flags{padding-left:170px}
}
@media (max-width:600px){
  .ag-c-flags{display:none}          /* flags are the first thing to go */
  .ag-c-who{padding-left:96px}
}

/* ── Quick-add a topic to the OPEN meeting ────────────────────────────────
   Chad, 2026-08-14: "there ought to be a topic button that we could just
   simply add to that meeting without having to search all the meetings."
   One box, Enter, done. It sits at the TOP of the agenda block, which now
   renders directly under the meeting header rather than below the Google
   invite blob — where it was landing off-screen on any meeting with a long
   Teams or Zoom invite pasted into the description. */
.mm-agenda{margin-top:2px}
.mm-topicadd{display:flex;gap:7px;align-items:center;margin:2px 0 9px}
.mm-topicadd input{
  flex:1 1 auto;min-width:0;
  border:1px solid var(--line-soft,#e3e8f2);border-radius:9px;
  padding:8px 11px;font-family:inherit;font-size:var(--t-footnote);background:#fff;
}
.mm-topicadd input:focus{
  outline:none;border-color:var(--cyan,#37BDF5);box-shadow:var(--ring,0 0 0 3px rgba(55,189,245,.18));
}
.mm-topicadd input:disabled{opacity:.6}
.mm-topicadd .btn{flex:none}
@media (max-width:600px){
  .mm-topicadd{flex-wrap:wrap}
  .mm-topicadd input{flex:1 0 100%}
}

/* ══════════════════════════════════════════════════════════════════════════
   7. ONE HEADER GEOMETRY, AND A MEETING THAT FITS ON THE SCREEN
   Chad, 2026-08-14: "there needs to be consistency across the entire OS on the
   size of the buttons at the top and the menu and the placement of it, those
   should all be locked in place ... that gives it a really weird clunky vibe
   that menus are always moving." And: "there is a lot of wasted space around
   the edge and at the top of the meeting ... all of the stuff at the bottom of
   the AI notetaker and all that you have to scroll up to get to. If the
   meeting doesn't have topics there's no reason for it to open that Big."

   MEASURED before touching anything (1600x950, local preview):

     module     bar top   main row   view switcher left
     Tasks         88        64px          594
     Meetings      88        64px          638
     Calendar      88        64px          639
     Contacts      88        64px          637
     Notes         88        64px          713

   The bar's top and its main-row height were already locked — that part of the
   LAYOUT LAW was holding. What moves is the VIEW SWITCHER: it sits immediately
   after the title, so its x-position is a function of how long the module's
   name happens to be. "Notes" pushes it 119px right of where "Tasks" puts it.
   Every module switch slides those tabs sideways under the cursor. That is the
   jumping.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 7a. The switcher is CENTRED, so it lands on the same pixel in every
   module regardless of the title beside it. Title stays left, actions stay
   right; only the middle zone is pinned. Below 1200px the three zones can no
   longer coexist on one line, so it reverts to the old flow rather than
   overlapping. */
/* #179 (2026-08-20): RETIRED — this block used to centre the switcher on
   non-calendar modules by absolute-positioning it at ≥1200px, because the
   calendar's grid was still scoped to .page-cal. That grid is now promoted to
   EVERY .topbar (styles.css, "#179" block: 1fr/auto/1fr with the views in the
   centre track), so the absolute copy became the second implementation of the
   same idea — and once the views carried grid-column:2, `left:50%` resolved
   against a zero-width grid area and threw the tabs ~100px right of centre
   with the actions underneath. One mechanism now, at every desktop width, in
   every module — the calendar's. */
.topbar .topbar-title,.topbar .topbar-views,.topbar .topbar-actions{min-width:0}
.topbar .topbar-title h1{overflow:hidden;text-overflow:ellipsis}

/* ── 7b. The meeting page. Measured at 1600x950 with 6 topics on the meeting:
   content ran 1543px — 1.6 screens — and the AI Note Taker sat at y=1681,
   731px BELOW the fold. The agenda alone was 526px of that.

   The agenda list now scrolls inside its own box. It keeps its place at the
   top (that is where Chad wants the topic control), but it can no longer push
   the notes and the recorder off the bottom of the screen no matter how many
   topics accumulate. */
.mm-agenda #mmTopics{
  max-height:214px;
  overflow-y:auto;
  overscroll-behavior-y:contain;
  padding-right:2px;
}
/* An empty agenda should cost almost nothing. The quick-add box already says
   what this section is for, so the "No open topics for this meeting yet."
   line is redundant — and it was reserving a block on every meeting that has
   no topics, which is exactly the "no reason for it to open that Big". */
.mm-agenda #mmTopics .empty-note{display:none}
.mm-agenda #mmTopics:empty{display:none}

/* ── 7c. Tighten the meeting header. The title, the date row and the attendee
   chips were each carrying their own generous margin, which is the "wasted
   space at the top" — it added up to ~294px before the agenda even started. */
.meet-page-wrap .mm-head{margin-bottom:8px}
.meet-page-wrap h1{margin-bottom:2px}
.meet-page-wrap .field{margin-top:12px}
.meet-page-wrap .field > label{margin-bottom:4px}
.mm-attendees{margin-top:6px}

/* ── 7d. The notes box was reserving 46vh (437px on a 950px screen) before you
   had typed a character, and the Save-notes / AI Note Taker row sits UNDER it.
   That is why Chad had to scroll to reach the recorder on every single
   meeting: "all of the stuff at the bottom of the AI notetaker and all that
   you have to scroll up to get to."

   Two changes, because either alone leaves a case broken:
   (1) a smaller starting height — it still drag-resizes and still grows with
       content, so nothing is lost on a meeting with long notes;
   (2) the action row STICKS to the bottom of the pane, so the recorder is
       reachable no matter how much has been typed or how many topics are on
       the agenda. */
.meet-page .notes-area{min-height:24vh}
@media (max-height:800px){ .meet-page .notes-area{min-height:20vh} }

/* (The sticky variant that used to live here is gone: the action row now
   renders at the TOP of the meeting, so there is nothing to pin.) */

/* ══════════════════════════════════════════════════════════════════════════
   8. ONE HEADER, BOTH FAMILIES
   Chad, 2026-08-14: "they need to have a consistency at the top of size color
   font and location ... that needs to be for task, calendars, notes, etc."

   KH OS grew two header implementations. 10 surfaces call the shared topBar()
   (Meetings, Calendar, Tasks, Contacts, Notes, Knowledge, Documents,
   Equipment, My Files, Topics). The other 33 hand-roll `<div class="page-head">
   <h1>…</h1><div class="head-actions">…</div></div>` — Cash, Financials, HR,
   Support, Tickets, Audits, Checkoffs, Forms, Build Tracker, the dashboards.

   They had drifted on exactly the three axes Chad named:

     axis        topBar()                  .page-head
     size        26px                      25px
     colour      var(--blue-deep)          inherited ink (#323338)
     height      64px, locked              none — grew and WRAPPED with content
     align       centre                    flex-end
     actions     right, centred            right, bottom-aligned

   So the title changed size and colour, and the bar changed height, depending
   on which module you were in. This makes .page-head adopt the topBar
   geometry rather than rewriting 33 render functions — same result on screen,
   none of the risk of touching Financial/HR/Cash markup, which is permission-
   sensitive and has no business being edited for a cosmetic fix.
   ══════════════════════════════════════════════════════════════════════════ */

.page-head{
  min-height:var(--topbar-h);   /* the LAYOUT LAW invariant — 64px everywhere */
  align-items:center;           /* was flex-end */
  margin-bottom:14px;           /* was 20px; matches .topbar */
  gap:16px;
}
.page-head h1{
  font-size:var(--t-title1);               /* was 25px */
  letter-spacing:-.5px;
  color:var(--blue-deep);       /* was whatever it inherited */
  line-height:1.2;
}
.page-head .head-actions{margin-left:auto;align-items:center}

/* Wrapping is what let the bar grow a second row and shove the content down.
   Hold it to one line on desktop; phones keep the old wrap, where stacking is
   the correct behaviour. */
@media (min-width:900px){
  .page-head{flex-wrap:nowrap}
  .page-head > :first-child{min-width:0}
  .page-head h1{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
}
@media (max-width:899px){
  .page-head{min-height:0}
}

/* ── PHONE: Store P&L (Chad, 2026-08-21: "almost unreadable on the iPhone app…
   the line item is too wide… the menu buttons at the top are all too large…
   All reports could just simply be a back arrow in a circle… more Apple
   centric"). Measured at 375px before this: a 211px Line column (56% of the
   screen), every control 44px tall wrapping to three rows, a 112px back
   button. Apple-centric here means the iOS vocabulary — a circular back
   chevron, a segmented control for the view, compact scrolling chip rows, and
   one value column at a time (the period chips, in app.js). Scoped to
   .page-reports; !important where the phone tap-target pass fights back. */
@media (max-width: 767px){
  .page-reports .rep-dhead{gap:10px;align-items:center}
  .page-reports #repBack{font-size:0 !important;width:36px !important;height:36px !important;min-height:0 !important;padding:0 !important;border-radius:50% !important;display:inline-flex;align-items:center;justify-content:center}
  .page-reports #repBack::before{content:"\2039";font-size:var(--t-title2);line-height:1;font-weight:600;margin-top:-3px}
  .page-reports .rep-dtitle h2{font-size:var(--t-headline)}
  .page-reports .rep-dsub{font-size:var(--t-caption) !important;line-height:1.35}
  /* The store chips take their own scrolling line; the segmented control
     and the two L0 icons share the first line. */
  .page-reports .sp-ctl .sp-stores{order:9;flex:0 0 100% !important;margin-top:2px}
  .page-reports .sp-ctl{gap:6px !important;margin-bottom:8px !important}
  .page-reports .sp-ctl .btn,.page-reports .sp-pchips .btn{font-size:var(--t-caption) !important;padding:0 10px !important;height:30px !important;min-height:0 !important;line-height:30px !important;border-radius:15px !important;white-space:nowrap}
  .page-reports .sp-seg{background:#e8ebf2;border-radius:10px;padding:2px;gap:0 !important}
  .page-reports .sp-seg .btn{border:0 !important;background:transparent !important;color:var(--ink) !important;box-shadow:none !important;border-radius:8px !important;height:28px !important;line-height:28px !important}
  .page-reports .sp-seg .btn.btn-primary{background:#fff !important;box-shadow:0 1px 3px rgba(0,0,0,.18) !important;font-weight:700}
  .page-reports .sp-stores,.page-reports .sp-pchips{display:flex;gap:6px;overflow-x:auto;white-space:nowrap;flex-wrap:nowrap !important;max-width:100%;scrollbar-width:none;-webkit-overflow-scrolling:touch;padding-bottom:2px}
  .page-reports .sp-stores::-webkit-scrollbar,.page-reports .sp-pchips::-webkit-scrollbar{display:none}
  .page-reports .sp-pchips{margin:0 0 8px}
  .page-reports .sp-sheet{max-height:calc(100vh - 250px)}
  .page-reports .sp-sheet table{font-size:var(--t-caption) !important}
  .page-reports .sp-sheet td:first-child,.page-reports .sp-sheet th:first-child{max-width:46vw;min-width:112px;white-space:normal !important;line-height:1.2;padding-right:8px !important}
  .page-reports .sp-sheet td{padding-top:5px !important;padding-bottom:5px !important;padding-right:8px !important}
  .page-reports .sp-sheet thead th{padding:6px 46px 6px 8px !important}
  .page-reports .sp-sheet thead th:first-child{padding-right:8px !important}
  .page-reports .sp-sheet .pnl-pct{width:38px;font-size:var(--t-caption2)}
  .page-reports .sp-sheet thead .btn{font-size:var(--t-caption2) !important;padding:0 6px !important;height:20px !important;min-height:0 !important;line-height:20px !important}
}

/* ===== #223 — the repair-and-maintenance loop ==============================
   Down is the one state in this module that is louder than a flag: a machine
   that is not working is a store problem happening right now, not a hole in
   the data. It gets the danger colour and a solid edge; everything else in
   Equipment stays in the amber/red chip language it already speaks. */
.eq-down{padding:11px 13px;border-radius:10px;margin:10px 0;line-height:1.5;
  background:rgba(192,57,43,.10);border:1px solid rgba(192,57,43,.42);
  border-left:4px solid var(--danger,#c0392b)}
.eq-down b{color:var(--danger,#c0392b)}
.eq-down-q{margin-top:6px;font-weight:650}
/* A count that is not zero should catch the eye in a rail full of grey chips. */
.fchip-alarm{border-color:rgba(192,57,43,.45)}
.fchip-alarm .fchip-n{background:rgba(192,57,43,.16);color:var(--danger,#c0392b)}

/* Preventive maintenance rows — the same furniture as the repair history
   directly above them, because they are the same story about one machine. */
.eq-pmrow{display:flex;gap:10px;align-items:baseline;padding:9px 12px;margin-bottom:6px;
  background:var(--card);border:1px solid var(--line);border-radius:9px}
.eq-pmdel{padding:2px 8px;font-size:var(--t-caption);flex:none}

/* The Lemon List. A table of arithmetic, so it is laid out as columns rather
   than cards: this is the one Equipment screen nobody browses — they read it
   top to bottom and stop when the numbers stop being alarming. */
.eq-lemons{padding:12px 0 6px}
.eq-lemhead,.eq-lemrow{display:grid;gap:10px;align-items:baseline;padding:9px 14px;
  grid-template-columns:minmax(190px,2.2fr) 1fr .8fr .9fr .9fr .7fr .8fr}
.eq-lemhead{font-size:var(--t-caption);font-weight:700;text-transform:uppercase;letter-spacing:.4px;
  color:var(--muted);border-bottom:1px solid var(--line)}
.eq-lemrow{border-bottom:1px solid var(--line);cursor:pointer}
.eq-lemrow:hover{background:var(--bg2)}
.eq-lemrow .num,.eq-lemhead .num{text-align:right;font-variant-numeric:tabular-nums}
.eq-lemrow .ct-sub{display:block}
.eq-lem-over{border-left:3px solid var(--danger,#c0392b)}
.eq-vendrow{grid-template-columns:minmax(190px,2.2fr) 1fr 1fr 1fr;cursor:default}
.eq-vendrow:hover{background:transparent}
@media (max-width:820px){
  /* On a phone the arithmetic still has to be readable, so the columns that
     only support the argument (store, days down) fold away rather than
     squeezing the two that make it. */
  .eq-lemhead{display:none}
  .eq-lemrow{grid-template-columns:1fr auto auto;row-gap:2px}
  .eq-lemrow>:nth-child(2),.eq-lemrow>:nth-child(5),.eq-lemrow>:nth-child(7){display:none}
}

/* #223 — the label sheet printed from INSIDE the app (Mac/iPhone shells have
   no second window). White always, whatever the theme: a dark surface prints
   as a black page and empties a toner cartridge to make a code that will not
   scan. */
.eq-lblov{align-items:flex-start}
.eq-lblmodal{max-width:min(980px,96vw);background:#fff;color:#111;padding:14px}
.eq-lblbar{display:flex;gap:10px;align-items:baseline;margin-bottom:10px}
.eq-lblbar .ct-sub{color:#555}
.eq-lblsheet{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;
  max-height:70vh;overflow:auto}
.eq-lblsheet .lbl{display:flex;gap:8px;align-items:center;border:1px solid #bbb;
  border-radius:6px;padding:8px;break-inside:avoid}
.eq-lblsheet .lbl img{width:104px;height:104px}
.eq-lblsheet .lbl-t{display:flex;flex-direction:column;gap:1px;overflow:hidden;min-width:0}
.eq-lblsheet .lbl-t b{font-size:var(--t-footnote)}
.eq-lblsheet .lbl-t span{font-size:var(--t-caption2);color:#444;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.eq-lblsheet .lbl-cta{margin-top:3px;font-weight:600;color:#111}
@media print{
  body.eq-printing>*:not(.eq-lblov){display:none !important}
  .eq-lblov{position:static;background:#fff;display:block}
  .eq-lblmodal{max-width:none;box-shadow:none;border:0}
  .eq-lblbar{display:none}
  .eq-lblsheet{max-height:none;overflow:visible;grid-template-columns:repeat(3,1fr)}
}
/* ⚠️ THE BRACE ABOVE WAS MISSING (found 2026-09-04). This @media print block ran
   to the END OF THE FILE, so every rule written after it has been print-only —
   dead on screen — since it landed. Two shipped features were silently unstyled
   in the browser the whole time:
     · .fld .fld-help  — the form builder's help-text control (2026-08-24)
     · .ff-drop-file / .ff-drop-name — the file-upload answer (2026-08-24)
   Both are PUBLIC form surfaces, which is why nobody hit them in the app.
   Found because the collapsible-sidebar CSS appended below it did nothing and
   the CSSOM reported it nested under @media print. Anything appended to this
   file from here on lands on screen, as intended. */

/* Sub-label under a question on a form being filled out. Added with the
   builder's help-text control (2026-08-24) -- the record had nowhere to put
   this before, which is why the control was pulled from the design kit. */
.fld .fld-help{
  display:block;font-weight:400;font-size:var(--t-caption);line-height:1.45;
  color:var(--muted);margin:3px 0 5px;
}
.fld a{color:var(--accent,#315DC8)}

/* File upload answer (2026-08-24). Same drop-zone shape as the photo control,
   but it names the file instead of showing a thumbnail — there is nothing to
   preview about a PDF, and the name is what the submitter wants to see
   confirmed back to them. */
.ff-drop-file{cursor:pointer}
.ff-drop-name{
  font-size:var(--t-footnote);font-weight:600;color:var(--ink);
  overflow-wrap:anywhere;padding:4px 2px;
}

/* ══════════════════════════════════════════════════════════════════════════
   COLLAPSIBLE SIDEBARS (Chad, 2026-09-04)

   "Can you also make it to where we can collapse the sidebar ... into the main
    channel or as we make the window smaller and there's only enough room for
    the frame for the content that we need to see that the sidebar and the main
    work tree gets collapsed into that ... similar to what Apple does."

   The reason: he runs KH OS in a half-screen window beside a video call while
   interviewing, and the two sidebars left ~590px for the notes.

   THE MODEL IS macOS's, and the important half is which state is remembered.
   A stored "I want the sidebar" must not fight the window: below --sb-auto
   (1100px) BOTH rails always collapse, and the toggle opens them as an OVERLAY
   over the content instead of a column beside it, so opening one never costs
   width again. The stored preference applies to WIDE windows only — shrink the
   window and it collapses, widen it and your choice comes back. That is why
   sbApply() reads the width first and the preference second.

   Almost nothing here is new mechanism. The rail already had a slide-over
   (.rail.open + .rail-backdrop.show) and a button (.ab-burger); both were
   gated to ≤767px. This promotes them and gives the module rails the same. */
:root{ --sb-anim:.2s cubic-bezier(.2,.8,.25,1); }

/* ── 1. The two toggle buttons ──────────────────────────────────────────── */
/* The app-bar button is .ab-burger, which exists and is display:none above
   767px. Turn it on everywhere and make it read as a sidebar control rather
   than a phone hamburger. Its ≤767 styling is left exactly as it was. */
@media (min-width:768px){
  .ab-burger{display:inline-flex !important;align-items:center;justify-content:center;
    width:34px;height:34px;margin-right:2px;border:none;border-radius:9px;
    background:rgba(255,255,255,.10);color:#fff;cursor:pointer;
    font-size:0;line-height:0;transition:background var(--m-fast) ease}
  .ab-burger:hover{background:rgba(255,255,255,.20)}
  .ab-burger:active{transform:var(--press)}
  /* Apple's sidebar glyph, drawn from borders so there is no glyph to go
     missing and no icon font to load. Outer rounded box, one divider. */
  .ab-burger::before{content:"";display:block;width:17px;height:14px;
    border:1.6px solid currentColor;border-radius:3px;position:relative}
  .ab-burger::after{content:"";position:absolute;width:1.6px;height:14px;
    background:currentColor;transform:translateX(-3.2px)}
}

/* The module-rail toggle rides in the module header, left of the title, so it
   sits directly above the rail it controls. topBar() is the ONE builder for
   every module's header, so this button exists in every module and CSS decides
   where it is meaningful: a page with no rail to toggle does not draw it. */
.rail-toggle{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
  width:30px;height:30px;margin-right:2px;border:1px solid var(--line);border-radius:8px;
  background:#fff;color:var(--muted);cursor:pointer;font-size:0;line-height:0;
  transition:border-color var(--m-fast) ease,color var(--m-fast) ease}
.rail-toggle:hover{border-color:var(--cyan);color:var(--blue-mid)}
.rail-toggle:active{transform:var(--press)}
.rail-toggle::before{content:"";display:block;width:15px;height:12px;
  border:1.5px solid currentColor;border-radius:3px;position:relative}
.rail-toggle::after{content:"";position:absolute;width:1.5px;height:12px;
  background:currentColor;transform:translateX(-2.7px)}
/* Draw it only where there is actually a rail underneath. :has() keeps this a
   CSS decision, so a new module gets the button by having a rail and nothing
   else has to be registered anywhere. */
.page:not(:has(.mtg-rail,.cal-side-col,.kb-tree,.mod-rail)) .rail-toggle{display:none}
@media (max-width:767px){ .rail-toggle{display:none} }

/* ⚠️ .topbar-main IS A GRID, not a flex row — #179 promoted the calendar's
   1fr/auto/1fr template to every desktop topbar. A fourth child therefore
   claims its own CELL and drops the title onto a second row, which is exactly
   what it did on first run. So the button comes OUT of the grid flow and is
   pinned to the left edge, with the title indented to meet it: the grid keeps
   its three tracks and the header keeps its one-line height in every module.
   The indent is gated on the same :has() as the button, so a module with no
   rail gets neither the button nor a 38px hole where it would have been. */
.topbar-main{position:relative}
.rail-toggle{position:absolute;left:0;top:50%;transform:translateY(-50%);margin:0}
.rail-toggle:active{transform:translateY(-50%) var(--press)}
.page:has(.mtg-rail,.cal-side-col,.kb-tree,.mod-rail) .topbar-main > .topbar-title{padding-left:38px}
@media (max-width:767px){
  .page:has(.mtg-rail,.cal-side-col,.kb-tree,.mod-rail) .topbar-main > .topbar-title{padding-left:0}
}

/* ⚠️ RESPONSIVE.md: "never key layout off window.innerWidth in JS at render
   time. One render at zero width permanently hid the calendar rail." The first
   cut of this feature broke that rule — JS read innerWidth and wrote .nav-off /
   .rail-off — and the Calendar caught it immediately: arriving at 880px it kept
   a 240px account list because the classes were computed on a stale width and
   nothing re-ran. So the split is now strict:

     · CSS media queries own the AUTOMATIC collapse. No class required.
     · .nav-off / .rail-off carry only the user's PREFERENCE, and are honoured
       at >=1100 only — which is what makes "the window decides first" true by
       construction rather than by JS remembering to re-check.
     · .open / .rail-peek are transient peek state, set by a click.

   JS reads the width only inside click handlers, which is not render time. */

/* ── 2a. AUTOMATIC — 768-1099: both rails step aside, no JS involved ─────── */
@media (min-width:768px) and (max-width:1099px){
  .shell > .rail:not(.open){width:0 !important;flex-basis:0 !important;
    padding-left:0 !important;padding-right:0 !important;opacity:0;pointer-events:none}
  .page:not(.rail-peek) .mtg-rail,
  .page:not(.rail-peek) .cal-side-col,
  .page:not(.rail-peek) .kb-tree,
  .page:not(.rail-peek) .mod-rail{display:none}
  .page:not(.rail-peek).page-flush:has(> .mtg-shell),
  .page:not(.rail-peek).page-flush:has(> .kb-shell),
  .page:not(.rail-peek).page-flush:has(> .cal-flex){grid-template-columns:minmax(0,1fr)}
  .page:not(.rail-peek).page-flush > .topbar,
  .page:not(.rail-peek).page-flush .mtg-doc,
  .page:not(.rail-peek).page-flush .kb-doc,
  .page:not(.rail-peek).page-flush .cal-main{grid-column:1}
  .page:not(.rail-peek) .mod-shell{grid-template-columns:minmax(0,1fr)}
  /* Opening one must NOT take width back, or we are straight back to the
     problem this was written for. It floats, with the phone drawer's backdrop. */
  .shell > .rail.open{width:270px !important;flex-basis:0 !important;opacity:1 !important;
    pointer-events:auto;position:fixed;left:0;top:var(--appbar-h);bottom:0;z-index:200;
    padding-left:14px !important;padding-right:14px !important;
    overflow:auto;box-shadow:0 0 40px rgba(0,0,0,.35)}
  .rail-backdrop.show{display:block;position:fixed;inset:0;
    background:rgba(10,15,40,.45);z-index:150}
}

/* ── 2b. PREFERENCE — >=1100 only, so a stored "off" can never fight 2a ──── */
@media (min-width:1100px){
  .shell > .rail{transition:width var(--sb-anim),flex-basis var(--sb-anim),
    padding var(--sb-anim),opacity var(--sb-anim)}
  .shell.nav-off > .rail{width:0 !important;flex-basis:0 !important;
    padding-left:0 !important;padding-right:0 !important;opacity:0;pointer-events:none}
  .page.rail-off .mtg-rail,
  .page.rail-off .cal-side-col,
  .page.rail-off .kb-tree,
  .page.rail-off .mod-rail{display:none}
  .page.rail-off.page-flush:has(> .mtg-shell),
  .page.rail-off.page-flush:has(> .kb-shell),
  .page.rail-off.page-flush:has(> .cal-flex){grid-template-columns:minmax(0,1fr)}
  .page.rail-off.page-flush > .topbar,
  .page.rail-off.page-flush .mtg-doc,
  .page.rail-off.page-flush .kb-doc,
  .page.rail-off.page-flush .cal-main{grid-column:1}
  .page.rail-off .mod-shell{grid-template-columns:minmax(0,1fr)}
}

/* ── 3. Peeking a collapsed module rail: it overlays, never re-takes the column */
.page.rail-peek .mtg-rail,
.page.rail-peek .cal-side-col,
.page.rail-peek .kb-tree,
.page.rail-peek .mod-shell > .mod-rail{
  display:block !important;position:fixed !important;
  left:0;top:var(--appbar-h);bottom:0;width:min(300px,86vw) !important;
  max-width:none !important;max-height:none !important;height:auto !important;
  z-index:260;overflow:auto;margin:0 !important;border-radius:0 14px 14px 0;
  box-shadow:0 0 40px rgba(8,26,102,.28)}
.page.rail-peek .rail-sheetback{display:block;position:fixed;inset:0;
  background:rgba(10,15,40,.45);z-index:250}
.rail-sheetback{display:none}
/* On a phone the existing bottom-sheet pass owns this; do not double up. */
@media (max-width:767px){
  .page.rail-peek .mtg-rail,
  .page.rail-peek .cal-side-col,
  .page.rail-peek .kb-tree,
  .page.rail-peek .mod-shell > .mod-rail{position:static !important;width:auto !important;
    box-shadow:none;border-radius:0;z-index:auto}
  .page.rail-peek .rail-sheetback{display:none}
}

/* ══════════════════════════════════════════════════════════════════════════
   PHONE PASS (Chad, 2026-09-05) — finishing the type/sidebar work of 9/4.

   The premise I started with was WRONG and the measurement said so: the phone
   was NOT still running the old 11px sizes. roles.css is not inside a media
   query, so the 9/4 role bump (269771f) already reached every width — measured
   across Tasks, Contacts, Meetings, Notes and HR at 375px, nothing rendered
   below 13px except three pill styles. So this is a scalpel, not a sweep. */

/* ── 1. THE HIRE TAB WAS UNREACHABLE ON THE PHONE ───────────────────────────
   "How this works" is deliberately an absolutely-positioned pill on the title
   line, top-right, on every module (Chad, 8/20). HR is the one module whose
   view tabs are wide enough to run under it: the strip reaches x=291 while the
   pill starts at x=214.

   That is not cosmetic. document.elementFromPoint at the centre of each tab at
   375px:
       Team        -> the tab
       Attendance  -> the tab
       Hire        -> THE "HOW THIS WORKS" BUTTON
   Tapping Hire opened the help pill instead. The tab could not be reached on
   an iPhone at all.

   Fixed for the CLASS, not for HR: any module carrying the pill reserves its
   footprint, and the tab strip wraps rather than sliding underneath. Contacts
   (tabs end at x=173) is already inside the cap and is unchanged; a future
   module with a fourth tab is covered without anyone remembering this. Wrap
   rather than overflow-x:auto on purpose — a horizontally scrolled tab strip
   hides the last tab, which is the bug we are fixing, not a fix for it. */
/* THE FIX: stop the pill being an absolute overlay on the ONE line it can
   collide with. It is positioned `absolute; top:6px; right:14px` so it rides
   the title line (Chad, 8/20) — which is fine until a module puts view tabs on
   that same line. Capping widths to dodge it was arithmetic against a moving
   target: reserving from .topbar-views measured from the wrong origin (the box
   starts after the title, so it still ran to x=255 against a button at 214, and
   only wrapped by luck), and reserving from .topbar-main squeezed the row into
   MORE wrapping — HR's header went 204 -> 300px and Contacts' tabs picked up a
   different overlap.

   So: when the title line carries tabs, the pill stops overlaying and joins the
   actions row underneath, which is already a wrapping flex row. Nothing is
   layered over anything, so there is no geometry to get wrong and no module
   left to check. Modules with no tabs (Reports) keep the top-right pill exactly
   as specified. */
@media (max-width:767px){
  .topbar:has(.topbar-views) .topbar-actions .btn.btn-howto{
    position:static;top:auto;right:auto;z-index:auto;
    align-self:center;margin-left:auto}
}

/* ── 2. The last three sub-13px pills ───────────────────────────────────────
   9/4 moved --role-chip-size 11 -> 13 for every chip in the app. These three
   never saw it because the phone pass writes --t-caption2 directly, at higher
   specificity than roles.css. They are the literal "the pills are really
   small" complaint, on the device Chad reads them on. 12 -> 13 only: they are
   status pills in tight rows, and 15 would reflow the rows they sit in. */
@media (max-width:767px){
  .lrow .status-pill{font-size:var(--role-chip-size)}
  .ct-pill,.ct-pill-co{font-size:var(--role-chip-size)}
  .mt-sort{font-size:var(--role-chip-size)}
}

/* ══════════════════════════════════════════════════════════════════════════
   ATTENDANCE POINTS — an aligned table, not a list (Chad, 2026-09-06)

   "The points list looks elementary at best ... we should have a better
   presentation." Four things were wrong with the old one-line-per-person list,
   and they are worth naming because they are the same four every such list
   gets wrong:

     1. The POINTS value — the only reason the list exists — was the least
        prominent thing on the row, a pale 13px chip trailing the name.
     2. 2.5 and 1.25 rendered identically, so nothing showed WHO IS CLOSE.
     3. "OK" appeared on all 24 rows. A word on every row distinguishes nobody.
     4. One narrow column on a 2000px screen; the right two-thirds was empty.

   So: a real table. The number is right-aligned and heavy so the column can be
   scanned as a column; a meter shows each person's distance to the FIRST
   write-up threshold, which is the decision this screen supports; and the date
   says both when and how long ago.

   Scoped to .att-recent .att-table on purpose — the 300px Team-members
   slide-over keeps the stacked .hr-so-row two-liner, because a table in 300px
   is worse than a list. One row FUNCTION, two presentations (app.js). */
.att-table{--att-cols:minmax(150px,1.5fr) minmax(110px,1fr) 62px minmax(80px,150px) minmax(130px,1.1fr) minmax(120px,1fr)}
.att-thead,.att-trow{display:grid;grid-template-columns:var(--att-cols);
  align-items:center;gap:var(--sp-4,12px)}
.att-thead{padding:0 10px 6px;font-size:var(--role-cap-size);font-weight:var(--role-cap-weight);
  letter-spacing:.06em;text-transform:uppercase;color:var(--muted);
  border-bottom:1px solid var(--line-soft)}
.att-trow{width:100%;text-align:left;border:0;background:none;font:inherit;cursor:pointer;
  padding:9px 10px;border-radius:var(--r-sm);border-bottom:1px solid var(--line-soft)}
.att-trow:last-child{border-bottom:0}
.att-trow:hover{background:var(--bg)}
.att-trow.active{background:var(--scope-on-bg,#eef2ff)}
.att-trow:active{transform:var(--press)}

.att-t-name{font-size:var(--role-rowtitle-size);font-weight:var(--w-semibold,600);color:var(--ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.att-t-store,.att-t-last{font-size:var(--role-rowmeta-size);color:var(--muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The number leads. Tabular figures so 1.5 and 12.5 line up on the decimal
   down a 24-row column instead of drifting with glyph width. */
/* The row reuses the att-ok/warm/hot severity classes so there is ONE severity
   vocabulary — but those classes are the old PILL styling in styles.css and
   bring a tinted background with them. A tinted chip behind every number is
   exactly the look this change replaced, and it fights the alignment of a
   numeric column, so the background is dropped here and only the COLOUR is
   kept. Padding and min-width go too: they were pill geometry. */
.att-t-pts{justify-self:end;font-size:var(--t-title3);font-weight:800;line-height:1;
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1;
  background:none !important;padding:0;margin:0;min-width:0;border-radius:0}
.att-t-pts.att-ok{color:var(--muted)}
.att-t-pts.att-pos{color:var(--st-done)}
.att-t-pts.att-warm{color:color-mix(in srgb,var(--st-doing) 62%,#000)}
.att-t-pts.att-hot,.att-t-pts.att-final{color:var(--st-stuck)}

/* Distance to the first write-up, as a bar. Empty track is drawn, not blank —
   a person at 0 shows an empty meter rather than nothing, so the column reads
   as a scale in every row (cells state both answers). */
.att-t-gauge{position:relative;height:6px;border-radius:999px;
  background:var(--line-soft);overflow:hidden}
.att-t-fill{position:absolute;left:0;top:0;bottom:0;border-radius:999px;
  background:var(--st-todo);transition:width var(--m-med) ease}
.att-t-fill.att-pos,.att-t-fill.att-ok{background:var(--st-done)}
.att-t-fill.att-warm{background:var(--st-doing)}
.att-t-fill.att-hot,.att-t-fill.att-final{background:var(--st-stuck)}
.att-t-away{font-size:var(--role-rowmeta-size);color:var(--muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.att-t-away b{color:var(--ink);font-weight:700}
/* Anyone who has crossed a threshold gets a left edge, so the danger zone is
   visible before a single number is read. */
.att-trow-hot{box-shadow:inset 3px 0 0 var(--st-stuck)}

/* Tablet: drop the two softest columns rather than crush all six. */
@media (max-width:1100px){
  .att-table{--att-cols:minmax(140px,1.6fr) 58px minmax(70px,120px) minmax(110px,1fr)}
  .att-thead>:nth-child(2),.att-trow>.att-t-store,
  .att-thead>:nth-child(6),.att-trow>.att-t-last{display:none}
}
/* Phone: a table in 375px is the thing this replaced. Back to two lines, with
   the number kept large and the meter kept — they are the point. */
@media (max-width:767px){
  .att-table{--att-cols:1fr 52px}
  .att-thead{display:none}
  .att-trow{grid-template-columns:1fr 52px;grid-template-areas:"name pts" "gauge pts" "away away";
    row-gap:4px;padding:10px 8px}
  .att-trow>.att-t-name{grid-area:name}
  .att-trow>.att-t-pts{grid-area:pts;align-self:center}
  .att-trow>.att-t-gauge{grid-area:gauge}
  .att-trow>.att-t-away{grid-area:away}
  .att-trow>.att-t-store,.att-trow>.att-t-last{display:none}
}
