/* A stable scrollbar gutter, so the canvas does not shift sideways.
   Measured: the page header was 993px on Today (long page, scrollbar present)
   and 1008px on Clients (short page, none), so every navigation between a long
   and a short screen moved the whole layout 15px. */
html { scrollbar-gutter: stable; }
/* A full-screen phone editor must cover the full visual viewport. Keeping the
   desktop's reserved scrollbar strip while the page itself is locked left a
   bare 15px seam down the right edge in Chromium-sized phone windows. */
html.is-modal-open { scrollbar-gutter: auto; }

/* Border-box everywhere. Padding is drawn inside the declared width, so a
   `width: 100%` element with padding still fits its container. Without this
   the phone list screens scrolled 11px sideways and the sideways scrollbar
   lifted the fixed nav bar off the bottom of the viewport. */
*, *::before, *::after { box-sizing: border-box; }

/* App shell. Tokens only, per SPEC 4.1. */

/* line-height on the body, not left to "normal": unspecified leading comes
   from whichever font actually loaded, the same defect the heading rule
   below already closed (F78), still open for every paragraph and cell. */
body { margin: 0; background: var(--bg); color: var(--ink); font-family: var(--font-sans); font-size: var(--t-md); line-height: var(--lh-normal); }

/* Every heading in the app computed to line-height: normal, which means the
   leading comes from whatever font actually loaded rather than from the type
   scale. --lh-tight exists for exactly this and was being used on one element.
   Unspecified leading is a vertical rhythm that changes when a webfont fails
   to load or falls back, which is the one moment you least want the layout
   moving. */
h1, h2, h3, h4 { line-height: var(--lh-tight); }

.app { display: grid; grid-template-columns: var(--sidebar-w) minmax(0, 1fr); min-height: 100vh; }

/* ------------------------------------------------------------- sidebar */
.sidebar {
  border-right: var(--border); background: var(--panel-sunken);
  padding: var(--s5) var(--s3); display: flex; flex-direction: column; gap: var(--s5);
  position: sticky; top: 0; height: 100vh; box-sizing: border-box;
}
.sidebar .brand {
  display: flex; align-items: center;
  padding: 0 var(--s2) var(--s2); color: var(--ink); text-decoration: none;
}
.brandwordmark { display: block; width: auto; max-width: 100%; height: var(--control-h-sm); object-fit: contain; }
@media (prefers-color-scheme: dark) { .brandwordmark { filter: invert(1); } }
[data-theme="dark"] .brandwordmark { filter: invert(1); }
[data-theme="light"] .brandwordmark { filter: none; }
.loginbrand { display: block; width: auto; max-width: 100%; height: var(--control-h); margin: 0 auto var(--s3); object-fit: contain; }
@media (prefers-color-scheme: dark) { .loginbrand { filter: invert(1); } }
[data-theme="dark"] .loginbrand { filter: invert(1); }
[data-theme="light"] .loginbrand { filter: none; }
.mobilebar { display: none; }
.sidebar ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; flex: 1; }
.navlink {
  display: flex; align-items: center; gap: var(--s2); width: 100%;
  padding: var(--s2) var(--s3); border-radius: var(--r-md); border: none;
  background: none; color: var(--ink-soft); font-family: var(--font-sans);
  font-size: var(--t-md); cursor: pointer; text-align: left; text-decoration: none;
}
.navlink:hover { background: var(--panel-hover); color: var(--ink); }
/* You are here, at 3:1 rather than 1.09:1.
   The current page was marked by swapping --panel-sunken for --panel, which
   measures 1.10:1 light and 1.09:1 dark against the rail: invisible to low
   vision, and in dark mode the hover fill was BRIGHTER than it, so hovering
   anything read as more selected than the page you were on. A hover fill can
   be outranked; a solid rule cannot. The narrow layout already does this with
   an inset shadow, so this is the desktop catching up. */
.navlink[aria-current="page"] {
  background: var(--info-bg); color: var(--info); font-weight: 600;
  box-shadow: inset 2px 0 0 0 var(--info);
}
.navlink:focus-visible { outline: 2px solid var(--line-focus); outline-offset: -2px; }
/* The icons are decorative glyphs of differing widths. A fixed column keeps
   the labels on one optical line instead of stepping in and out. */
.navicon { width: 16px; height: 16px; flex: none; display: inline-flex; align-items: center; justify-content: center; opacity: .75; }
.navlink[aria-current="page"] .navicon { opacity: 1; }
.sidefoot { display: flex; flex-direction: column; gap: var(--s1); border-top: var(--border); padding-top: var(--s3); }
.sidefoot .ui-btn { justify-content: flex-start; }

/* -------------------------------------------------------------- content */
main {
  padding: var(--s6) var(--s7) var(--s8);
  /* One left edge for every screen (DECISIONS Q14). This was margin-inline:
     auto, centring the capped column, which closed a finding about dead
     background on the right at 1920 and opened a worse one: Today, the board
     and the calendar are uncapped and start at the rail, so navigating
     between a wide screen and a capped one threw the content 258px sideways
     and back. The rail is the anchor; dead space on the right is the cost,
     accepted and recorded. */
  max-width: var(--content-max); width: 100%; box-sizing: border-box;
}
/* A canvas, not a document. --content-max is a reading width: right for a list
   or a form, wrong for six board columns you compare side by side. Capped, a
   2349px screen showed four of six columns with 961px of it empty. */
main.is-wide { max-width: none; }
main:focus { outline: none; }
.pagehead {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s4); margin-bottom: var(--s5); flex-wrap: wrap;
}
.pagehead h1 { font-family: var(--font-head); font-size: var(--t-2xl); margin: 0 0 var(--s1); letter-spacing: -0.02em; }
.pagehead p { margin: 0; color: var(--ink-faint); font-size: var(--t-sm); }
.pagehead .actions { display: flex; gap: var(--s2); flex-wrap: wrap; }


/* Today.
   The old version was five equal count tiles above eight equally weighted
   lists: every element the same size, so the eye had nowhere to land. This
   spends the whole type scale instead. One lead sentence carries the largest
   type on the page; group headings are quiet; rows are dense and uniform so
   scanning them is fast. Nothing here is decoration. */
.lead {
  border-left: 2px solid var(--line-strong);
  padding: var(--s1) 0 var(--s1) var(--s4);
  margin-bottom: var(--s5);
}
.lead.is-urgent { border-left-color: var(--bad); }
.lead-line {
  font-family: var(--font-head); font-size: var(--t-xl); font-weight: 600;
  line-height: var(--lh-tight); margin: 0; letter-spacing: -0.01em; color: var(--ink);
}
.lead-line b { font-weight: 800; }
.lead.is-urgent .lead-line b { color: var(--bad); }

.group { margin-bottom: 0; min-width: 0; }
/* Layout only. The heading VOICE (type, case, colour) is the shared
   section-heading rule in ui.css, one rule for this and .ui-panel-head h2,
   because the two are the same level and were drawn as opposites. */
.group-title {
  display: flex; align-items: baseline; gap: var(--s2);
  margin: 0 0 var(--s3);
}
.group-count { color: var(--ink-faint); font-weight: 600; letter-spacing: 0; }
.group.is-bad .group-title { color: var(--bad); }
.group.is-warn .group-title { color: var(--warn); }
/* .group-more and its anchor retired with the category groups: the triage
   queue never truncates, so there is no "and 3 more" to style. */

/* The .row2 family styled ListRow, which was deleted with the category
   groups the triage queue replaced. A dead rule absorbs fixes: a change
   made to it looks applied and does nothing. */

.stack { display: flex; flex-direction: column; gap: var(--s1); }
.line {
  display: flex; align-items: center; gap: var(--s3); padding: var(--s3);
  border: var(--border); border-radius: var(--r-md); background: var(--panel);
}
.line .main { flex: 1; min-width: 0; }
.line .main b { display: block; font-weight: 600; }
.line .main span { color: var(--ink-faint); font-size: var(--t-sm); }
.line .when { color: var(--ink-faint); font-size: var(--t-sm); white-space: nowrap; }

/* Long-form text a human wrote: briefs and scripts. Keep the line breaks. */
.prose { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; line-height: var(--lh-normal); }

/* The footnote under a flush table: where the raw lives, footage notes. */
.rawnote { padding: var(--s4); border-top: var(--border); }

/* Calendar */
.calgrid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 1px; background: var(--line); border: var(--border); border-radius: var(--r-lg); overflow: hidden; }
.calhead { background: var(--panel-sunken); padding: var(--s2); text-align: center; font-size: var(--t-xs); text-transform: uppercase; letter-spacing: .06em; color: var(--ink-faint); font-weight: 600; }
.calday { background: var(--panel); min-width: 0; min-height: 96px; padding: var(--s2); display: flex; flex-direction: column; gap: 2px; }
.calday.is-other { background: var(--panel-sunken); }
.calday.is-today { background: var(--info-bg); }
.calday .dnum { font-size: var(--t-xs); color: var(--ink-faint); font-weight: 600; margin-bottom: 2px; }
.calday.is-today .dnum { color: var(--info); }
/* The calendar entry itself is `.ui-calitem`, styled in ui.css beside the
   primitive that renders it. A near-identical `.calitem` block lived here
   until iteration 12 and matched nothing: the iteration 11 tap-target fix was
   applied to it and silently did nothing, and the matrix passed anyway
   because that run had no entries in view. Dead CSS does not just waste
   bytes, it absorbs fixes. */

/* Detail */
.kv2 { display: grid; grid-template-columns: 180px 1fr; gap: var(--s2) var(--s4); font-size: var(--t-md); }
.kv2 dt { color: var(--ink-faint); }
.kv2 dd { margin: 0; overflow-wrap: anywhere; }
.rate { font-family: var(--font-head); font-size: var(--t-xl); font-weight: 700; }

/* Login: the card is declared once, in ui.css, because the app shell and
   the legacy admin both render it and each stylesheet carried its own
   geometry (round-4). */

/* ---------------------------------------------------------------- phone */
@media (max-width: 800px) {
  .commit { min-height: var(--tap); }
  /* minmax(0, 1fr), never bare 1fr: a grid track will not shrink below its
     content's min width, so one wide table pushed every phone screen 214px
     sideways. Found at iteration 10, the first run in a real browser. */
  /* The bottom-bar block below declares this too; one is enough. */
  /* The phone sidebar used to be declared here as a horizontal scrolling
     strip, and again further down as the fixed bottom bar that replaced it.
     Same breakpoint, same specificity, later rule wins, so every declaration
     here was dead except the two the bottom bar happened not to re-declare:
     a border-bottom, which drew a stray hairline along the bottom edge of a
     bar that is already at the bottom of the screen, and align-items on
     .navlink, which the column layout down there needs and now states itself.
     A dead rule is not tidy-up: it absorbs fixes, and a change made to it
     looks applied and does nothing. */
  /* Touch floors by width, not by pointer type, so they hold in any narrow
     window and a harness can measure them. pointer: coarse alone is invisible
     to a resized desktop window. */
  /* .navlink had a touch floor here too. The bottom-bar block below gives it
     --navbar-h, which is taller, so this one only ever lost. */
  /* Every button, not only the small variant: the default .ui-btn measures
     36px, which the is-sm-only floor left short. */
  .ui-btn { min-height: var(--tap); }
  /* The row's real control (a text-height inline on desktop, where the
     whole 49px row is the mouse target). On a phone the card is still the
     tap surface via delegation, but the CONTROL is what a thumb or a switch
     user actually acquires, and it measured 21px in the mobile sweep. */
  .ui-rowa { display: inline-flex; align-items: center; min-height: var(--tap-min); }
  /* Both phone concerns for an input in one rule. The font size was declared
     170 lines away in a second block of this same media query, which is how
     .sidebar ended up described twice in two directions. */
  .ui-input { min-height: var(--tap); font-size: var(--t-lg); }
  .ui-chip { min-height: var(--tap); }
  /* A calendar entry is a real tap target, so it gets the floor too. The
     month grid grows taller on a phone, which is the correct trade: a 20px
     target is not hittable with a thumb. */
  /* This block used to squeeze the month grid into 46px cells and admit, in a
     comment, that 42px of truncated title was compliant and nearly unreadable.
     The agenda list further down is the answer it was waiting for, so the
     squeezing rules are gone; the tap target below still applies. */
  .ui-calitem {
    min-height: var(--tap); display: flex; align-items: center;
    white-space: normal; padding: var(--s1) 2px;
  }
  /* Standalone links (back links) get a full-height target. Inline links in
     running text are exempt per WCAG 2.5.8 and stay as they are. The
     "and 3 more" half of this rule retired with Today's category groups. */
  p > a.ui-sub { display: inline-flex; align-items: center; min-height: var(--tap); }
  /* .sidefoot and main were declared here as well. The bottom-bar block hides
     the first outright and gives main a bottom padding that clears the bar, so
     both of these were overridden in full. */
  /* Label beside value, not above it. Stacked, four facts reading "Not set"
     spent 225px of an 812px phone screen, and the label column does not need
     the 180px it gets on a desktop. It still wraps when a value is long,
     because the value track can shrink to nothing before it overflows. */
  .kv2 { grid-template-columns: minmax(0, auto) minmax(0, 1fr); gap: var(--s2) var(--s4); }
  .kv2 dt { margin-top: 0; }
}
.cellrow { display: inline-flex; align-items: center; gap: var(--s2); }
/* Why a client is in the list it is in. It replaces the contact name under
   the company, which was never the reason anyone opened the row. */
.why { font-size: var(--t-sm); color: var(--ink-faint); }
.why.is-bad { color: var(--bad); }
.why.is-warn { color: var(--warn); }
/* The day number carries a quiet "plan here" button that only shows on
   hover or keyboard focus, so the grid stays calm at rest. */
.calday .dnum { display: flex; align-items: center; justify-content: space-between; }
/* min-height: 0 made this an 18px target. It only appears on hover or focus,
   which does not exempt it: once it is there it has to be hittable. */
.calday .dnum .ui-btn { opacity: 0; padding: 0 var(--s1); min-height: var(--tap-min); min-width: var(--tap-min); }
.calday:hover .dnum .ui-btn, .calday .dnum .ui-btn:focus-visible { opacity: 1; }

/* The modal overlay lives on <body>, above everything, and the page behind it
   stops scrolling so the dialog is the only thing that moves. */
.modal-root { position: relative; z-index: 100; }
body.is-modal-open { overflow: hidden; }

/* Today, iteration 14: main column plus a rail, as DESIGN_REFERENCE.md
   describes. A dashboard is a layout problem before it is a component
   problem, and one full-width column was most of what made this bland. */
/* Three columns, each answering a different question: what is stuck (left),
   what lands and when (centre), the month and the money (right). Two columns
   was the old shape and it made "stuck" and "coming" share one scroll, which
   is why the seven lists all looked the same weight. */
.dash {
  display: grid; grid-template-columns: 300px minmax(0, 1fr) 300px;
  gap: var(--s6); align-items: start;
  /* Two columns since the triage queue took the centre: the queue, and the
     month and money beside it. The old left rail listed the same work the
     queue now explains, so it was duplication with a second set of
     controls, not a third question. */
  /* Uncapped `main` lets the centre column take the width on a large monitor,
     which is the point. A ceiling stops it sprawling on an ultrawide, where a
     single row of text a metre long is worse than the wasted space it fixed. */
  max-width: 1760px;
}
.dash.is-queue {
  grid-template-columns: minmax(0, 1fr) 300px;
  grid-template-areas: "summary rail" "queue rail";
}
.dash-main { min-width: 0; }
.today-summary { grid-area: summary; min-width: 0; }
.today-queue { grid-area: queue; min-width: 0; }
.dash-rail { grid-area: rail; min-width: 0; }
/* Under 1000 the rail stacks under the queue: the queue is the screen and
   the month, the money and the week are its footnotes, so that is the order
   they should read in when there is only one column. */
@media (max-width: 999px) {
  .dash, .dash.is-queue {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "summary" "queue" "rail";
  }
}

/* ---- the week ahead -------------------------------------------------
   Seven rows, one per day. The bar is content scheduled that day against the
   busiest day of the week, so the shape is relative to your own week rather
   than an absolute scale nobody has a feel for. */
.wkgrid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
.wk {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: var(--s2);
  padding: var(--s3); min-width: 0; min-height: 76px;
  text-decoration: none; color: inherit; border-right: var(--border);
}
.wk:last-child { border-right: none; }
.wk:hover { background: var(--panel-hover); }
.wk:focus-visible { outline: 2px solid var(--line-focus); outline-offset: -2px; }
.wk-day { font-size: var(--t-xs); color: var(--ink-faint); text-transform: uppercase; letter-spacing: .06em; }
.wk-day b { display: block; color: var(--ink); font-size: var(--t-md); font-weight: 700; margin: 2px 0 0; letter-spacing: 0; }
.wk.is-today .wk-day, .wk.is-today .wk-day b { color: var(--info); }
/* The week strip renders ProgressBar now, so this styles the primitive in
   context rather than defining a second bar that drifts from it. Height and
   track colour differ from the default deliberately: this one sits inside a
   day cell. */
.wk .ui-progress { display: block; height: 6px; grid-column: 1 / -1; }
.wk.is-today .ui-progress > i { background: var(--info); }
.wk-n { grid-column: 2; grid-row: 1; align-self: start; font-size: var(--t-xs); color: var(--ink-faint); font-weight: 600; }
.wk-shoots { grid-column: 2; grid-row: 2; display: inline-flex; gap: 3px; align-items: center; min-height: var(--s2); }
/* A day with nothing on it should look like nothing, not like a row that
   failed to load. The bar track carries that on its own. */
.wk.is-clear .ui-progress { opacity: .4; }

/* ---- billing, as a fortnight ---------------------------------------
   Reference only. SPEC 6 keeps invoicing and amounts out of this product;
   this says "a bill goes out on the 20th", nothing more. */
.bills { display: flex; flex-direction: column; }
.bill {
  display: grid; grid-template-columns: 2.5rem minmax(0, 1fr); gap: var(--s2) var(--s3);
  align-items: center; padding: var(--s2) var(--s4); min-height: 32px;
  text-decoration: none; color: inherit; border-bottom: var(--border);
}
.bill:last-child { border-bottom: none; }
.bill:hover { background: var(--panel-hover); }
.bill:focus-visible { outline: 2px solid var(--line-focus); outline-offset: -2px; }
.bill-when { font-size: var(--t-xs); font-weight: 700; color: var(--ink); }
/* Warn is earned, not worn: a billing day two weeks out drawn in the same hue
   as one tomorrow cried wolf six times per glance. It marks only the imminent
   ones, and it marks the number itself now that the rail under it is gone. */
.bill-when.is-near { color: var(--warn); }
.bill-name {
  display: flex; align-items: center; gap: var(--s2); min-width: 0;
  font-size: var(--t-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ----------------------------------------------------------- Billing guide */
/* Copy-it-exactly strings (QuickBooks records, email domains) read in mono so
   a near-identical record cannot masquerade as the right one. */
.mono { font-family: var(--font-mono); font-size: var(--t-sm); }
/* A hand-authored bullet in an editable rules body. A real list marker, not
   a glyph pasted into a paragraph. */
.billing-point { padding-left: var(--s4); position: relative; }
.billing-point::before { content: '•'; position: absolute; left: var(--s1); color: var(--ink-faint); }
/* Rules-page paragraph rhythm from the tokens, not the UA's 1em: bare <p>
   children of .prose had browser-default margins, the only place in the
   app where the UA set vertical rhythm (round-8). */
.prose p { margin: 0 0 var(--s3); }
.prose p:last-child { margin-bottom: 0; }
/* Presentation-only lowercase for the imported tagline. In CSS, so the
   stored data keeps its casing and a proper noun can be rescued by
   editing; .toLowerCase() in the renderer could not be undone. */
.btag { text-transform: lowercase; }

/* ------------------------------------------------------- the triage queue */
/* Today's spine, from the design tournament: three named bands, and inside
   them rows that are sentences. The grid is what makes a list of sentences
   scannable: the clock column and the action column sit at a fixed x down
   the whole screen, so the eye reads one ragged column of prose and two
   straight ones of state. */
/* The handover buttons are concatenated with no whitespace between them, so
   as a block they touched: two borders at the same x reading as one two-part
   control. Same shape as .ui-state-actions, which owns its own gap. */
.triage-more {
  color: var(--ink-faint); font-size: var(--t-sm); margin: var(--s2) 0 0;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s2);
}
.triage-band > [data-bandtoggle] { display: none; }
.grouphead {
  display: flex; align-items: baseline; gap: var(--s2);
  margin: var(--s6) 0 var(--s3);
  font-family: var(--font-head); font-size: var(--t-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: .1em; color: var(--ink-faint);
}
.grouphead:first-of-type { margin-top: 0; }
.groupn { color: var(--ink); font-weight: 700; letter-spacing: 0; }
.grouphead .railnote {
  margin-left: auto; text-transform: none; letter-spacing: 0;
  font-weight: 400; font-family: var(--font-sans); color: var(--ink-faint);
}
.triage { list-style: none; margin: 0; padding: 0; border-top: var(--border); }
.t {
  /* The third track was `auto`, so it sized to each row's OWN button label
     and shifted the whole grid per row: "Write it" against "Move it to
     Posted" measured a 60px spread across twelve rows, which is the exact
     opposite of the fixed x this comment promises. A fixed track holds the
     line; the button fills it via .t > .ui-btn below. */
  display: grid; grid-template-columns: minmax(0, 1fr) 132px 148px;
  align-items: start; gap: var(--s4);
  padding: var(--s3) 0 var(--s3) var(--s3);
  border-bottom: var(--border);
  border-left: 2px solid transparent;
}
/* The action fills its fixed track rather than sizing to its own words, so
   every row's button starts and ends at the same x. */
.t > .ui-btn {
  width: 100%; color: var(--info); border-color: transparent; background: transparent;
}
.t > .ui-btn .ui-btn-icon { order: 2; }
/* The band is already named in the heading, so the row's colour is a quiet
   second signal, never the only one. */
.t.is-bad { border-left-color: var(--bad); }
.t.is-warn { border-left-color: var(--warn); }
.t-body { min-width: 0; }
.t-line { margin: 0; display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap; }
/* The title is the control: a real button that dresses as the sentence it
   is, the same contract as a table row's .ui-rowa. */
.t-what {
  font: inherit; font-weight: 600; color: var(--ink);
  background: none; border: 0; padding: 0; text-align: left; cursor: pointer;
  /* A 200-character title ran to four lines and pushed the client name off
     the end of the row. Two lines, then an ellipsis, with the whole string
     on the button's `title` so the tail the ellipsis ate is still reachable
     by hover as well as by the aria-label. */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden; max-width: 100%;
}
.t-what:hover { text-decoration: underline; }
.t-what:focus-visible { outline: 2px solid var(--line-focus); outline-offset: 2px; border-radius: var(--r-sm); }
.t-who { display: inline-flex; align-items: center; gap: var(--s1); color: var(--ink-faint); font-size: var(--t-sm); }
.t-why { margin: var(--s1) 0 0; color: var(--ink-soft); font-size: var(--t-sm); line-height: var(--lh-normal); }
.t-clock { margin: 0; display: flex; flex-direction: column; }
.t-clock b { font-size: var(--t-md); font-weight: 700; }
.t-clock span { color: var(--ink-faint); font-size: var(--t-xs); }
@media (max-width: 800px) {
  /* On a phone each band becomes one quiet working surface. The title and
     reason read first; time and action form a compact footer. Nothing is a
     floating outlined rectangle competing with the work itself. */
  .triage-band {
    overflow: hidden; margin: 0 0 var(--s4); border: var(--border);
    border-radius: var(--r-lg); background: var(--panel);
  }
  .triage-band .grouphead {
    margin: 0; padding: var(--s3) var(--s4); border-bottom: var(--border);
    background: var(--panel-sunken);
  }
  .triage { border-top: 0; }
  .t {
    grid-template-columns: minmax(0, 1fr) auto; gap: var(--s3);
    padding: var(--s4); border-left-width: 3px;
  }
  .t-body { grid-column: 1 / -1; }
  .t-line { flex-direction: column; align-items: flex-start; gap: 2px; }
  .t-who { font-size: var(--t-xs); }
  .t-why { margin-top: var(--s2); }
  .t-clock {
    flex-direction: row; align-items: baseline; gap: var(--s2); align-self: center;
    min-width: 0; padding: var(--s1) var(--s2); border-radius: var(--r-full);
    background: var(--panel-sunken);
  }
  .t-clock b { font-size: var(--t-sm); }
  .t > .ui-btn {
    width: auto; min-width: 0; padding-inline: var(--s2); justify-self: end;
  }
  .grouphead .railnote { display: none; }
  /* The row title is a real control and measured 21px, under the 24px
     WCAG 2.5.8 floor: the same fix .ui-rowa already carries, because it is
     the same shape of control (a button dressed as the text it wraps).
     min-height alone, because `display: inline-flex` here overrode the
     -webkit-box above and switched the two-line clamp OFF on the narrowest
     viewport, which is the one where a long title does the most damage. */
  .t-what { min-height: var(--tap-min); }
  .triage-band:not(.is-expanded) .triage > .t:nth-child(n + 3) { display: none; }
  .triage-band > [data-bandtoggle] {
    display: flex; width: 100%; margin: 0; padding: var(--s3) var(--s4);
    justify-content: flex-start; color: var(--info); border-top: var(--border);
    border-radius: 0;
  }
  .triage-more {
    margin: 0; padding: var(--s3) var(--s4); border-top: var(--border);
    background: var(--panel-sunken);
  }
  .triage-more .ui-btn { min-height: var(--tap-min); padding-inline: var(--s2); color: var(--info); }
}

/* ------------------------------------------------ content editor + preview */
/* The asset is the subject of this editor, so it leads. Only native creative
   is presented here. A missing asset gets a compact, honest state. */
.ui-modal.is-content-editor, .ui-modal.is-record-editor { max-width: var(--modal-reel-max); }
.editor-context {
  display: flex; align-items: baseline; gap: var(--s2); margin: 0 0 var(--s5);
  color: var(--ink-soft); font-size: var(--t-sm); font-weight: 600;
}
.editor-context span { color: var(--ink-faint); font-size: var(--t-xs); font-weight: 400; }
.editor-context span::before { content: '·'; margin-right: var(--s2); }
.editor-intro { margin: 0 0 var(--s5); color: var(--ink-soft); font-size: var(--t-sm); }
.content-share-manage {
  display: flex; justify-content: flex-end; gap: var(--s2); flex-wrap: wrap;
  padding-top: var(--s3); border-top: var(--border);
}
.editor-role-note { margin: calc(-1 * var(--s2)) 0 var(--s4); color: var(--ink-faint); font-size: var(--t-sm); }
.editor-check-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s2) var(--s4); }
.editor-section { min-width: 0; padding-top: var(--s4); margin-top: var(--s4); border-top: var(--border); }
.editor-section:first-child { padding-top: 0; margin-top: 0; border-top: 0; }
.editor-section > h3 {
  margin: 0 0 var(--s3); color: var(--ink-faint); font-family: var(--font-head);
  font-size: var(--t-xs); font-weight: 700; text-transform: uppercase; letter-spacing: .09em;
}
.editor-section.is-key > h3 { color: var(--ink-soft); }
.editor-section-body { min-width: 0; }
.editor-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 var(--s4); min-width: 0;
}
.editor-grid > .ui-field { margin-bottom: var(--s4); }
.editor-grid > fieldset, .editor-grid > .ui-check { grid-column: 1 / -1; }
.editor-section-body > .ui-field:last-child { margin-bottom: 0; }
.source-shoot-head {
  display: flex; align-items: center; gap: var(--s3); min-width: 0;
  padding-bottom: var(--s3); border-bottom: var(--border);
}
.source-shoot-head > div:first-child { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.source-shoot-head > div:first-child span { color: var(--ink-faint); font-size: var(--t-sm); }
.source-part { padding-top: var(--s3); }
.source-label {
  display: block; margin-bottom: var(--s1); color: var(--ink-faint);
  font-size: var(--t-xs); font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
}
.source-brief, .source-script-body, .source-path { margin: 0; color: var(--ink-soft); white-space: pre-wrap; overflow-wrap: anywhere; }
.source-script-body { font-size: var(--t-sm); line-height: 1.55; }
.source-footage { list-style: none; margin: 0; padding: 0; }
.source-footage li {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3);
  padding: var(--s2) 0; border-top: var(--border); font-size: var(--t-sm);
}
.source-footage li:first-child { border-top: 0; }
.source-footage li span { color: var(--ink-faint); text-align: right; }
.content-preview-hero {
  display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(var(--content-preview-meta-min), .8fr);
  grid-column: 1 / -1; align-self: start; width: 100%; height: max-content;
  margin-bottom: var(--s6); overflow: hidden;
  border: var(--border); border-radius: var(--r-md); background: var(--panel);
}
.content-preview-hero:not(.has-summary) { grid-template-columns: minmax(0, 1fr); }
.content-preview-media {
  display: flex; align-items: stretch; min-width: 0;
  background: var(--panel-sunken); border-right: var(--border);
}
.content-asset-state {
  display: grid; grid-template-columns: var(--tap) minmax(0, 1fr) auto;
  align-items: center; gap: var(--s3); width: 100%; min-height: calc(2 * var(--control-h));
  padding: var(--s4);
}
.content-asset-icon {
  display: grid; place-items: center; width: var(--tap); height: var(--tap);
  border: var(--border); border-radius: var(--r-full); color: var(--ink-soft);
  background: var(--panel);
}
.content-asset-state.is-linked .content-asset-icon { color: var(--info); background: var(--info-bg); }
.content-asset-copy { min-width: 0; }
.content-asset-copy strong { display: block; font-family: var(--font-head); font-size: var(--t-md); }
.content-asset-copy p { margin: var(--s1) 0 0; color: var(--ink-faint); font-size: var(--t-sm); }
.content-preview-info {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--s3);
  min-width: 0; padding: var(--s5);
}
.content-preview-topline {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  width: 100%; min-width: 0;
}
.content-preview-client { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
.content-preview-client b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.content-preview-meta { display: flex; flex-wrap: wrap; gap: var(--s1) var(--s2); color: var(--ink-faint); font-size: var(--t-sm); }
.content-preview-meta span + span::before { content: '·'; margin-right: var(--s2); }
.content-native-stage { width: 100%; min-width: 0; background: var(--panel-sunken); }
.content-native-main {
  display: grid; place-items: center; width: 100%; min-width: 0;
  max-height: calc(8 * var(--navbar-h)); overflow: hidden; border-bottom: var(--border);
}
.content-native-main img, .content-native-main video {
  display: block; width: 100%; max-width: 100%; max-height: calc(8 * var(--navbar-h)); object-fit: contain;
}
.content-native-main iframe { display: block; width: 100%; aspect-ratio: 4 / 5; border: 0; }
.content-native-name {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  min-width: 0; padding: var(--s2) var(--s3); color: var(--ink-faint); font-size: var(--t-xs);
}
.content-native-name b { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-soft); }
.content-native-name span { flex: none; }
.content-native-name .ui-btn { flex: none; padding-inline: var(--s2); }
.content-asset-rail {
  display: flex; gap: var(--s2); max-width: 100%; padding: 0 var(--s3) var(--s3);
  overflow-x: auto; overscroll-behavior-inline: contain; scrollbar-width: none;
}
.content-asset-rail::-webkit-scrollbar { display: none; }
.content-asset-thumb {
  display: grid; place-items: center; flex: 0 0 calc(2 * var(--tap));
  width: calc(2 * var(--tap)); aspect-ratio: 1; overflow: hidden; padding: 0;
  border: var(--border-strong); border-radius: var(--r-md); background: var(--panel); color: var(--ink-soft); cursor: pointer;
}
.content-asset-thumb.is-active { border-color: var(--info); outline: 2px solid var(--info); outline-offset: 1px; }
.content-asset-thumb:focus-visible { outline: 2px solid var(--line-focus); outline-offset: 1px; }
.content-asset-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.content-asset-thumb span { display: grid; place-items: center; }
.content-asset-thumb small { color: var(--ink-faint); font-size: var(--t-xs); }
.content-drop .ui-filelist:empty { display: none; }
.content-upload-note { margin: var(--s2) 0 0; color: var(--ink-faint); font-size: var(--t-xs); }
.content-file-actions { display: flex; align-items: center; gap: var(--s1); flex: none; }
.content-file-actions .ui-btn { padding-inline: var(--s2); }
.content-editor-danger { margin-top: var(--s8); padding-top: var(--s4); border-top: var(--border); }
.content-feedback-admin { display: grid; gap: var(--s3); }
.content-feedback-admin article { padding: var(--s3); border-left: var(--border-strong); background: var(--warn-bg); }
.content-feedback-admin header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s3); }
.content-feedback-admin header > div { display: grid; gap: var(--s1); min-width: 0; }
.content-feedback-admin time { color: var(--ink-faint); font-size: var(--t-xs); }
.content-feedback-admin p { margin: var(--s2) 0 0; white-space: pre-wrap; overflow-wrap: anywhere; line-height: var(--lh-loose); }
.ced { width: 100%; max-width: var(--content-editor-measure); margin: 0 auto; }
.ced-form { min-width: 0; }
.ced-form > [role="alert"] { margin-top: var(--s4); }
@media (max-width: 680px) {
  .content-preview-hero {
    grid-template-columns: minmax(0, 1fr);
    width: calc(100% + (2 * var(--s4)));
    margin: calc(-1 * var(--s5)) calc(-1 * var(--s4)) var(--s4);
    border-inline: 0; border-top: 0; border-radius: 0;
  }
  .content-preview-media { border-right: 0; border-bottom: var(--border); }
  .content-asset-state {
    grid-template-columns: var(--tap) minmax(0, 1fr) auto; min-height: 0; padding: var(--s3) var(--s4);
  }
  .content-asset-state > .ui-btn { justify-self: end; }
  .content-preview-info { gap: var(--s2); padding: var(--s4); }
  .content-native-main, .content-native-main img, .content-native-main video { max-height: calc(7 * var(--navbar-h)); }
  .content-drop .ui-drop { min-height: calc(2 * var(--tap)); padding: var(--s4); }
  .content-drop .ui-filerow {
    display: grid; grid-template-columns: var(--tap) minmax(0, 1fr) auto;
    gap: var(--s2) var(--s3); padding: var(--s3);
  }
  .content-drop .ui-fsize { text-align: right; }
  .content-file-actions { grid-column: 2 / -1; justify-content: flex-end; }
  .editor-grid { grid-template-columns: minmax(0, 1fr); }
  .editor-grid > fieldset, .editor-grid > .ui-check { grid-column: auto; }
  .editor-check-list { grid-template-columns: minmax(0, 1fr); }
  .source-shoot-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; }
  .source-shoot-head > .ui-btn { grid-column: 1 / -1; justify-self: start; }
}

/* The caption's own actions sit under it, full row: copy-to-clipboard and
   the review link the feedback goes to. */
.ci-capacts { display: flex; gap: var(--s2); flex-wrap: wrap; margin-bottom: 0; }
.ci-capacts.is-empty { display: none; }

/* The caption's live relationship with its platform: a counter always, the
   platform's own objections only when earned. Sits directly under the caption
   box, full row, quiet until something is wrong. */
.cap-limits {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--s1) var(--s3); margin: calc(-1 * var(--s2)) 0 var(--s3);
  font-size: var(--t-xs); min-height: 1em;
}
.cap-meter { color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.cap-meter.is-bad { color: var(--bad); font-weight: 700; }
.cap-note { color: var(--ink-soft); }
.cap-note.is-bad { color: var(--bad); }
.cap-note.is-warn { color: var(--warn); }

/* ------------------------------------------------------------ feed preview */
/* One client's profile, from the plan. Instagram and TikTok are a 3-up grid
   because a profile IS one; the feed platforms are a single scroll. Tiles are
   text-on-panel, honestly: the app stores no imagery, and the thing being
   judged here is cadence and mix, which text tiles show perfectly well. */
.feedgrid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 3px; max-width: 620px; }
.feedroll { display: grid; gap: var(--s3); max-width: 620px; }
.ftile {
  display: flex; flex-direction: column; min-width: 0;
  font: inherit; color: inherit; text-align: left; cursor: pointer; padding: 0;
  background: var(--panel); border: var(--border); border-radius: var(--r-sm);
  overflow: hidden;
}
.ftile:hover .ftile-media { background: var(--panel-hover); }
.ftile:focus-visible { outline: 2px solid var(--line-focus); outline-offset: 1px; }
.ftile-media {
  display: flex; flex-direction: column; gap: var(--s2); justify-content: space-between;
  aspect-ratio: 1 / 1; padding: var(--s2); background: var(--panel-sunken); min-width: 0;
}
.ftile.is-vert .ftile-media { aspect-ratio: 3 / 4; }
.feedroll .ftile-media { aspect-ratio: 16 / 9; }
.ftile-fmt {
  font-size: var(--t-xs); font-weight: 700; text-transform: uppercase;
  letter-spacing: .08em; color: var(--ink-faint);
}
.ftile-cap {
  font-size: var(--t-xs); color: var(--ink-soft); overflow: hidden;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4;
}
.ftile-foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s1);
  padding: var(--s1) var(--s2); font-size: var(--t-xs); color: var(--ink-faint);
  border-top: var(--border); min-width: 0;
}
/* History reads quieter than plan: the grid's job is the future. */
.ftile:not(.is-planned) { opacity: .55; }
@media (max-width: 800px) { .feedgrid { gap: 2px; } }

/* Named classes for compile-time-constant layout. These were written
   through data-css, the channel reserved for values only runtime knows
   (a colour token, a percentage), which made static layout invisible to
   the stylesheet and dressed inline styles in the primitive's clothes
   (round-8). */
.ui-state-actions.is-start { justify-content: flex-start; }
.ui-chips.is-gap-below { margin-bottom: var(--s5); }
.ui-sub.is-flush { margin: 0; }

/* The strip: several figures where a single tile row used to be. The delta
   is doing as much work as the value, which is the point. */
.strip {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(130px, 100%), 1fr));
  gap: var(--s4) var(--s5); padding: var(--s4) 0 var(--s5);
  border-top: var(--border); border-bottom: var(--border); margin-bottom: var(--s6);
}

/* Commitment shape: every client's month, readable in one pass. */
.commits { display: flex; flex-direction: column; gap: var(--s2); }
/* Two lines, not one. On a 300px rail the name, the bar and the count
   cannot share a row: every client name was being truncated, and a client
   you cannot read is not a client you can act on. Name and count on top,
   the bar full width underneath, which also makes the bars comparable
   because they all start at the same x. */
.commit {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px var(--s2);
  text-decoration: none; color: var(--ink); font-size: var(--t-sm); padding: var(--s1) 0;
}
.commit:hover .commit-name { text-decoration: underline; }
.commit-name {
  min-width: 0; display: flex; align-items: center; gap: var(--s2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.commit-n { color: var(--ink-faint); font-size: var(--t-xs); white-space: nowrap; text-align: right; }
.commit .ui-segs { grid-column: 1 / -1; }

.rail-note { margin: var(--s3) 0 0; font-size: var(--t-xs); color: var(--ink-faint); }

@media (max-width: 1100px) {
  /* The rail becomes a band under the main column rather than a squeezed
     sidebar: 300px of month grid at 900px wide is unreadable. */
  .dash { grid-template-columns: minmax(0, 1fr); }
  .dash-rail { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--s4); }
}

/* The reel board (wave R). Six columns that scroll sideways as a unit, with
   the column heads staying put. Flat and hairline like everything else. */
.rfilters {
  display: flex; align-items: center; gap: var(--s3);
  flex-wrap: wrap; margin-bottom: var(--s5);
}

/* Columns are equal height and the drop zone fills whatever is left, so a
   card can be dropped ANYWHERE in a column rather than only on the few
   hundred pixels the existing cards happen to occupy. Harrison, 2026-08-07:
   "dragging things around should be easier and I can drag anywhere in the
   column". align-items: stretch is what does it; align-items: start was
   sizing each column to its contents. */
.rboard {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(230px, 1fr);
  gap: var(--s3); overflow-x: auto; padding-bottom: var(--s3);
  align-items: stretch;
  /* The board owns a viewport-sized box and each column scrolls inside it.
     Equal-height columns alone were right at 12 cards and wrong at 62: one
     tall column stretched all six to 5285px, so five empty columns became
     five screens of dead space and the page scrolled 5610px. Every real
     kanban scrolls its columns, not its page. */
  height: calc(100vh - 260px); min-height: 320px;
  /* dvh where it exists: on a phone, 100vh is the LARGEST viewport, the one
     that pretends the browser chrome is already hidden, so the board's bottom
     edge sat under the URL bar until the user scrolled it away. dvh tracks
     the viewport that is actually there. The vh line above is the fallback. */
  height: calc(100dvh - 260px);
}
.rcol {
  display: flex; flex-direction: column; min-width: 0;
  background: var(--panel-sunken); border: var(--border);
  border-radius: var(--r-lg); overflow: hidden; max-height: 100%;
}
.rcol-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s2);
  padding: var(--s3); border-bottom: var(--border); background: var(--panel-sunken);
  flex: none;
}
.rcol-head h2 {
  margin: 0; font-family: var(--font-head); font-size: var(--t-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em; color: var(--ink-faint);
}
.rcol-count { font-size: var(--t-xs); font-weight: 600; color: var(--ink-faint); }
.rcol-drop {
  display: flex; flex-direction: column; gap: var(--s2);
  padding: var(--s2); flex: 1; min-height: 88px;
  /* Scrolls itself, so the column head stays put and the whole column
     remains a drop target however many cards are in it. */
  overflow-y: auto;
}
.rcol-drop.is-over { background: var(--panel-hover); }
.rcol-empty {
  margin: 0; padding: var(--s4) var(--s2); text-align: center;
  color: var(--ink-faint); font-size: var(--t-sm);
}

/* A card answers "can this move?" without being opened, which is the whole
   point of a board. Everything on it is a badge for that reason. */
.rcard {
  /* flex: none, or the cards SHRINK. The drop zone is a flex column with a
     fixed height, so 62 cards each squashed to 18px with zero-height titles:
     the text was in the DOM and invisible. Found by looking at the board at
     density, one iteration after the scrolling column was added. */
  flex: none; position: relative;
  display: flex; gap: var(--s2); align-items: stretch; text-align: left;
  background: var(--panel); border: var(--border); border-radius: var(--r-md);
  padding: var(--s2); cursor: pointer; overflow: hidden;
  /* The card is a real <button> now (the SPEC 4.2 guard refused a faked
     role), so the UA's button font and width behaviour are reset. */
  font: inherit; color: inherit; width: 100%; appearance: none;
}
.rcard:hover { background: var(--panel-hover); }
.rcard:focus-visible { outline: 2px solid var(--line-focus); outline-offset: 1px; }
/* Rush and overdue are different facts and were sharing one signal: the
   card rendered is-rush with no rule anywhere (rendered-but-unstyled, the
   direction F80 warned about) and its pill wore the same bad tone as
   overdue. Rush is urgency (warn); overdue is failure (bad), and when a
   card is both, overdue wins, so it is declared second. */
.rcard.is-rush { border-color: var(--warn); }
.rcard.is-overdue { border-color: var(--bad); }
.rcard-stripe { flex: none; width: 3px; border-radius: var(--r-full); }
.rcard-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--s1); }
.rcard-title {
  font-weight: 600; font-size: var(--t-sm); line-height: var(--lh-tight);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rcard-client {
  font-size: var(--t-xs); color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The activity trail inside the card editor. */
.rtrail { margin-top: var(--s5); border-top: var(--border); padding-top: var(--s4); }
.rtrail h3 {
  margin: 0 0 var(--s2); font-family: var(--font-head); font-size: var(--t-xs);
  text-transform: uppercase; letter-spacing: .08em; color: var(--ink-faint);
}
.rtrail ul { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--s2); }
.rtrail li { font-size: var(--t-sm); color: var(--ink-soft); display: flex; justify-content: space-between; gap: var(--s3); }
.rtrail li span { color: var(--ink-faint); font-size: var(--t-xs); white-space: nowrap; }

/* The reel opens as a working record, not a stack of bare inputs. Working
   links stay explicit actions rather than loading third-party previews into
   the editor. */
.ui-modal.is-reel-editor { max-width: var(--modal-reel-max); }
.reel-editor-form { min-width: 0; }
.reel-stage .ui-chips { margin-top: var(--s2); }
.reel-link-block {
  min-width: 0;
}
.reel-link-block + .reel-link-block { margin-top: var(--s4); }
.reel-link-block .ui-field { margin-bottom: var(--s2); }
.reel-link-actions { display: flex; gap: var(--s2); flex-wrap: wrap; }
.reel-editor-form > .ui-field { margin: var(--s4) 0 0; }

@media (max-width: 800px) {
  .reel-stage .ui-chips {
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    width: calc(100% + var(--s4)); padding-right: var(--s4);
  }
  .reel-stage .ui-chips::-webkit-scrollbar { display: none; }
  .reel-stage .ui-chip { flex: none; }
  .reel-stage .ui-chip.is-on {
    border-color: var(--info); background: var(--info); color: var(--ink-inverse);
  }
  .editor-section { padding-top: var(--s5); margin-top: var(--s5); }
  .editor-section > h3 { margin-bottom: var(--s4); }
  .editor-section .ui-field { margin-bottom: var(--s4); }
  .editor-section.is-caption textarea.ui-input { min-height: calc(4 * var(--control-h-mobile)); }
  .editor-section .ui-check {
    min-height: var(--control-h-mobile); margin: 0; padding: var(--s3);
    border: var(--border); border-radius: var(--r-lg); background: var(--panel);
  }
  .ui-modal.is-content-editor .ui-modal-head h2 { font-size: var(--t-lg); }
  .ui-modal.is-content-editor .ui-modal-head {
    min-height: calc(var(--tap) + (2 * var(--s1)));
    padding: calc(var(--s1) + env(safe-area-inset-top, 0px)) var(--s4) var(--s1);
  }
  .ui-modal.is-content-editor .ui-modal-head > [data-close] {
    width: var(--tap); min-height: var(--tap);
  }
  .ui-modal.is-content-editor .ui-modal-body { padding: var(--s3) var(--s4) var(--s5); }
  .ui-modal.is-content-editor .content-preview-hero {
    margin: calc(-1 * var(--s3)) calc(-1 * var(--s4)) var(--s3);
  }
  .ui-modal.is-content-editor .content-native-main,
  .ui-modal.is-content-editor .content-native-main img,
  .ui-modal.is-content-editor .content-native-main video { max-height: calc(4 * var(--navbar-h)); }
  .ui-modal.is-content-editor .content-preview-info { gap: var(--s1); padding: var(--s3) var(--s4); }
  .ui-modal.is-content-editor .content-native-name { padding-block: var(--s1); }
  .ui-modal.is-content-editor .content-asset-rail { padding-bottom: var(--s2); }
  .ui-modal.is-content-editor .content-asset-thumb {
    flex-basis: calc(var(--tap) + var(--s6)); width: calc(var(--tap) + var(--s6));
  }
  .ui-modal.is-content-editor .editor-section { padding-top: var(--s3); margin-top: var(--s3); }
  .ui-modal.is-content-editor .editor-section:first-child { padding-top: 0; margin-top: 0; }
  .ui-modal.is-content-editor .editor-section > h3 {
    margin-bottom: var(--s2); color: var(--ink); font-size: var(--t-md);
    letter-spacing: 0; text-transform: none;
  }
  .ui-modal.is-content-editor .ui-modal-body .ui-label {
    color: var(--ink-soft); font-size: var(--t-xs); letter-spacing: 0; text-transform: none;
  }
  .ui-modal.is-content-editor .editor-section .ui-field { margin-bottom: var(--s3); }
  .ui-modal.is-content-editor .editor-section.is-caption textarea.ui-input {
    min-height: calc(2 * var(--control-h-mobile));
  }
  .ui-modal.is-content-editor .editor-section.is-priority .ui-disclosure { margin-top: var(--s3); }
  .ui-modal.is-content-editor .content-drop .ui-drop {
    min-height: var(--tap); padding: var(--s3) var(--s4);
  }
  .ui-modal.is-content-editor .content-upload-note { margin-top: var(--s1); }
  .ui-modal.is-content-editor .editor-grid.is-content-plan,
  .ui-modal.is-content-editor .editor-grid.is-content-schedule {
    grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--s3);
  }
  .ui-modal.is-content-editor .editor-grid.is-content-plan > .ui-field:first-child { grid-column: 1 / -1; }
  .ui-modal.is-content-editor .editor-section.is-placement .ui-multi-options {
    display: flex; flex-wrap: nowrap; gap: var(--s2); max-width: 100%;
    padding: var(--s1); margin: calc(-1 * var(--s1)); overflow-x: auto;
    overscroll-behavior-inline: contain; scrollbar-width: none;
  }
  .ui-modal.is-content-editor .editor-section.is-placement .ui-multi-options::-webkit-scrollbar { display: none; }
  .ui-modal.is-content-editor .editor-section.is-placement .ui-multi-option {
    flex: none; min-height: var(--tap); padding: var(--s2) var(--s3);
  }
  .ui-modal.is-content-editor .content-editor-danger { margin-top: var(--s5); }
  .ui-modal.is-content-editor .ui-modal-foot {
    justify-content: stretch; min-height: 0;
    padding: var(--s1) var(--s4) calc(var(--s1) + env(safe-area-inset-bottom, 0px));
  }
  .ui-modal.is-content-editor .ui-modal-foot .ui-btn { min-height: var(--tap); }
  .ui-modal.is-content-editor .ui-modal-foot .ui-btn.is-secondary { display: none; }
  .ui-modal.is-content-editor .ui-modal-foot .ui-btn:last-child { flex: 1; }
}

/* The stage bar the phone navigates by. Desktop never shows it: six visible
   columns ARE the stage bar there. */
.rstagebar { display: none; }

/* The board's tool fold. On a desktop it does not exist: the toggle hides
   and the tools always show. On a phone the metrics and filters live behind
   one small button, because the board is the screen and the chrome was
   eating it (Harrison, round: "taking up a lot of real estate"). */
.btoolbar { display: none; }

@media (max-width: 800px) {
  /* The SWIPE stays: Harrison liked flicking across the columns, and he is
     right that a kanban's sideways pane is a deliberate gesture, not the
     accidental overflow the no-horizontal-scroll rule exists to kill. What
     the first cut got wrong was hiding five columns behind the edge with
     nothing saying where you were. The stage bar fixes THAT: pills with live
     counts that jump to a column, and the pill under the column you swiped
     to lights up, so the bar is both the map and the fast lane. */

  /* The board IS the screen. Measured before this rule: 609px of chrome
     above the first card, the board given a constant-minus calc that matched
     no scroll position, and 202 of 812px showing cards on load. The screen
     is a flex column now: chrome takes what it needs, the board takes every
     remaining pixel, and nothing needs a magic number to guess with. */
  main:has(> .rboard) {
    display: flex; flex-direction: column;
    height: calc(100dvh - var(--mobilebar-h) - var(--navbar-h) - env(safe-area-inset-bottom, 0px));
    overflow-y: auto;
  }
  /* :not(.rboard): this universal chrome rule carries (0,1,1) specificity
     through the :has(), so unexcluded it silently beat the board's own
     flex:1 below and handed the board 0 0 auto: measured as the board
     sitting below the fold at its content height. */
  main:has(> .rboard) > :not(.rboard) { flex: none; }
  .rboard {
    /* Basis 0, not auto: with auto the basis is the board's own content
       height, which is itself sized by the columns' 100% max-height, a
       circle that let the board grow to 608px and shove itself below the
       fold. Basis 0 makes flex:1 mean what it says: the space the chrome
       left over, floored at 300px. */
    flex: 1 1 0; min-height: 300px; height: auto;
    grid-auto-columns: 84%; scroll-snap-type: x mandatory; scrollbar-width: none;
  }
  .rboard::-webkit-scrollbar { display: none; }
  .rcol { scroll-snap-align: start; }

  /* One row, six pills, the lit one scrolled into view by the tracking
     code: wrapped to two rows the pill order stopped mirroring the swipe
     order, which is the one promise a map has to keep. This is a control
     rail like the board's own pane, not page content, so it may pan. */
  .rstagebar {
    display: flex; flex-wrap: nowrap; gap: var(--s2); padding-bottom: var(--s2);
    overflow-x: auto; scrollbar-width: none;
  }
  .rstagebar::-webkit-scrollbar { display: none; }
  .rstagebar .ui-chip { flex: none; }

  /* The subtitle is decoration on the one screen where every vertical
     pixel is a card: the title says Reel board, the columns say the rest. */
  main:has(> .rboard) .pagehead { margin-bottom: var(--s3); }
  main:has(> .rboard) .pagehead > div:first-child { display: none; }
  main:has(> .rboard) .pagehead .btoolbar { margin-bottom: 0; }

  /* The fold: one 40px button where 220px of metrics and chips stood. */
  .btoolbar { display: flex; margin-bottom: var(--s2); }
  .boardtools { display: none; }
  .boardtools.is-open { display: block; }

  /* A chip RAIL: one row that pans, for the calendar's client chips and the
     feed's filters, which kept desktop posture and stacked 196px of wrapped
     chips above the first day row (audit round). Same register as the stage
     bar: a control rail may pan; page content may not. */
  .rfilters.is-rail { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: var(--s1); }
  .rfilters.is-rail::-webkit-scrollbar { display: none; }
  .rfilters.is-rail .ui-chip { flex: none; }

  /* Four metrics in one row, not a 132px 2x2 block: on this screen they
     are a glance, and the board pays for every pixel they hold. */
  main:has(> .rboard) .strip { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s2); margin-bottom: var(--s3); }
  main:has(> .rboard) .strip .ui-metric-value { font-size: var(--t-lg); }

  /* A finger deserves a bigger card than a pointer. */
  .rcard { padding: var(--s3); }
  .ui-search { flex: 1 1 100%; }
}

/* The card, tightened at iteration 18 against the real reelboard. Theirs
   fits in about 55px; ours took 90 and wrapped, because the assignee was a
   name-shaped pill and the links were the words "Footage" and "Notes". An
   avatar is one glyph and a link is one glyph. */
.rcard-foot { display: flex; align-items: center; gap: var(--s1); margin-top: var(--s1); min-width: 0; }
.rcard-icons { display: flex; gap: 3px; margin-left: auto; flex: none; }
.ricon { font-size: var(--t-xs); color: var(--ink-faint); line-height: 1; }

/* Initials rather than a name. The full name is on the title attribute and
   in the card's aria-label, so nothing is lost to anyone. */

/* How far through the pipeline, at the foot of the card. Six stages, so a
   card in Editing reads as a third done without opening it. */
.rcard-progress {
  position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: var(--line);
}
.rcard-progress > i { display: block; height: 100%; background: var(--ink-faint); }
.rcard.is-overdue .rcard-progress > i { background: var(--bad); }

/* The column number, so the order of the pipeline is readable even when the
   board is scrolled and a header is half out of view. */
.rcol-n {
  /* Was 10px at .6 opacity on --ink-faint, which came out around 2.5:1 and
     under the 12px floor at the same time. It states the order of the
     pipeline, so it is information and has to be legible; the smallest token
     in the scale is the floor, and the weight does the receding instead. */
  font-size: var(--t-xs); font-weight: 700; color: var(--ink-faint);
  letter-spacing: .04em; flex: none;
}
/* Was min-height: 0, which made the add button 18px: under the 24px AA
   floor and sitting right beside the column heading. */
.rcol-head .ui-btn { margin-left: var(--s1); padding: 0 var(--s2); min-height: var(--tap-min); min-width: var(--tap-min); }
@media (max-width: 800px) {
  /* Restores the touch floor this rule unset. */
  .rcol-head .ui-btn { min-height: var(--tap); min-width: 40px; }
}

/* Pointer dragging (iteration 19). HTML5 drag never fired on touch, so on a
   phone the board could only be changed through a dropdown. */

/* pan-y lets a finger scroll the column vertically; the press-and-hold takes
   over only once a drag has actually started. Without this the browser eats
   the gesture and the card never moves. */
.rcard { touch-action: pan-y; -webkit-user-select: none; user-select: none; }

/* The card being dragged stays in place, dimmed, so the column does not
   reflow under the finger and the origin stays visible. */
.rcard.is-dragging { opacity: .3; }

/* What follows the pointer. Fixed, above everything, and transparent to
   hit-testing so elementFromPoint reports the column underneath. */
.rcard.is-ghost {
  position: fixed; z-index: 200; pointer-events: none;
  opacity: .95; transform: rotate(1.5deg);
  border-color: var(--line-strong);
}

/* While a card is in the air, nothing else should select or highlight. */
body.is-dragging-card { cursor: grabbing; user-select: none; }
body.is-dragging-card .rcol-drop.is-over { background: var(--panel-hover); }
/* Snap OFF while a card is in the air. Mandatory snap rejects programmatic
   scrollLeft writes outright (measured: a write of 120 read back 0 on the
   same line), so the edge auto-scroll that makes an off-screen column
   reachable on a phone scrolled nothing. Snap resumes on drop. */
body.is-dragging-card .rboard { scroll-snap-type: none; }
.rboard.is-card-swiping { scroll-snap-type: none; }

/* ===================================================================
   Wave M, iteration 20: designed for a phone, not reflowed onto one.
   Everything above this point reflows. This section changes the layout.
   =================================================================== */

@media (max-width: 800px) {
  /* ---- navigation: a bottom bar, not a scrolling strip -----------------
     The sidebar became a horizontal strip that scrolled sideways, so half
     the screens were off the edge and reaching Settings meant scrolling a
     nav bar, which is not a thing phones do. A fixed bottom bar puts every
     screen one thumb-reach away and never moves. */
  .app { grid-template-columns: minmax(0, 1fr); }

  .mobilebar {
    display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
    position: sticky; top: 0; z-index: 55; min-height: var(--mobilebar-h);
    padding: var(--s2) var(--s4); border-bottom: var(--border);
    background: var(--panel-sunken);
  }
  .mobilebrand {
    display: flex; align-items: center; gap: var(--s2); min-width: 0;
    color: var(--ink); text-decoration: none;
  }
  .mobilebar .brandwordmark { height: var(--control-h-sm); }
  .mobilewhere {
    min-width: 0; max-width: 45%; color: var(--ink-faint); font-size: var(--t-xs);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  .sidebar {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto; z-index: 60;
    height: auto; flex-direction: row; align-items: stretch; gap: 0;
    padding: 0 0 env(safe-area-inset-bottom, 0px);
    border-right: none; border-top: var(--border);
    background: var(--panel-sunken); overflow: visible;
  }
  .sidebar .brand { display: none; }
  .sidebar ul {
    flex-direction: row; flex: 1; gap: 0;
    /* Scrolls only if it truly cannot fit; with icons above labels it does. */
    overflow-x: auto; scrollbar-width: none;
  }
  .sidebar ul::-webkit-scrollbar { display: none; }
  .sidebar li { flex: 1 0 auto; }

  /* Icon above label, which is what every phone app does and what makes a
     six-item bar fit across 375px. */
  .navlink {
    flex-direction: column; justify-content: center; align-items: center; gap: 2px;
    padding: var(--s2) var(--s1); border-radius: 0;
    min-height: var(--navbar-h); min-width: 56px; text-align: center;
    font-size: var(--t-xs); white-space: nowrap;
  }
  .navlink .navicon { width: auto; font-size: var(--t-lg); opacity: .8; }
  .navlink[aria-current="page"] {
    background: none; color: var(--info);
    box-shadow: inset 0 2px 0 0 var(--info);
  }
  .navlink[aria-current="page"] .navicon { opacity: 1; }

  /* Theme and sign out are off the bar entirely.
     They were two more items at the end of the row, taking 118px of a 257px
     strip: 31% of a phone's primary navigation spent on the two least used
     controls in the app, one of which ends your session, both sitting under
     the right thumb. They live in the More sheet now, which is also where the
     destinations that do not fit go. renderNav builds the bar, so this is no
     longer a thing CSS has to solve. */
  .sidefoot { display: none; }
  .navlink.is-current { color: var(--info); box-shadow: inset 0 2px 0 0 var(--info); }
  /* A .sidefoot layout and a .sidefoot .ui-btn style used to follow, from when
     Theme and Sign out were items on the bar. The rule above hides the whole
     element, so both were styling something that is not rendered. */

  /* The bar is fixed, so the page needs room underneath it or the last row
     of every screen sits behind the navigation. */
  main {
    padding: var(--s4) var(--s4)
             calc(var(--s8) + 52px + env(safe-area-inset-bottom, 0px));
  }
  .pagehead { gap: var(--s3); margin-bottom: var(--s4); }
  .pagehead h1 { font-size: var(--t-xl); }
  .pagehead .actions {
    width: calc(100% + 2 * var(--s4)); margin-inline: calc(-1 * var(--s4));
    padding-inline: var(--s4); overflow-x: auto; flex-wrap: nowrap;
    scrollbar-width: none;
  }
  .pagehead .actions::-webkit-scrollbar { display: none; }
  .pagehead .actions .ui-btn { flex: none; }
  .ui-chips.is-gap-below {
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    width: calc(100% + 2 * var(--s4)); margin-inline: calc(-1 * var(--s4));
    padding-inline: var(--s4); padding-bottom: var(--s1);
  }
  .ui-chips.is-gap-below::-webkit-scrollbar { display: none; }
  .ui-chips.is-gap-below > * { flex: none; }
  .dash.is-queue { gap: var(--s4); }
  .lead { margin-bottom: var(--s3); padding-left: var(--s3); }
  .lead-line { font-size: var(--t-lg); }
  .today-summary .strip {
    grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s2);
    padding: var(--s3) 0; margin-bottom: var(--s4);
  }
  .today-summary .ui-metric { min-width: 0; }
  .today-summary .ui-metric-label { font-size: var(--t-xs); }
  .today-summary .ui-metric-value { font-size: var(--t-xl); }
  .today-summary .wkgrid {
    display: flex; overflow-x: auto; scrollbar-width: none;
    scroll-snap-type: x proximity;
  }
  .today-summary .wkgrid::-webkit-scrollbar { display: none; }
  .today-summary .wk { flex: 0 0 72px; scroll-snap-align: start; }
  .today-summary .rail-note { display: none; }
  .grouphead { margin-top: var(--s4); }
  /* And scrolling something into view needs the same allowance, or tabbing
     to the last control scrolls it exactly under the bar: scrollIntoView and
     focus-driven scrolling read scroll-padding, not the page's padding. */
  html {
    scroll-padding-bottom: calc(var(--navbar-h) + env(safe-area-inset-bottom, 0px));
  }

  /* ---- tables become cards -------------------------------------------
     A table in a sideways-scrolling box is the classic bad phone pattern:
     you cannot see a row and its headings at the same time. The markup
     stays one real table; only the presentation changes, and each cell
     prints its own column heading from the custom property Table sets. */
  /* The rows are cards with their own borders here, so the desktop panel
     box around the scroll container would be a border around borders. */
  .ui-table-scroll {
    overflow-x: visible; border: var(--border); border-radius: var(--r-lg);
    background: var(--panel);
  }
  /* No width: 100% here. A block element already fills its container, and
     100% PLUS the card padding is wider than the container. */
  .ui-table, .ui-table tbody, .ui-table tr, .ui-table td {
    display: block; max-width: 100%;
  }
  /* The header is CLIPPED, not display:none: the primitive now carries
     explicit table roles that survive the flattening, so the columnheaders
     are worth keeping in the accessibility tree - display:none would strip
     them back out. The clip pattern removes their layout width (the
     original sin: header cells pushing every list screen sideways) while
     leaving them announceable (round-8). */
  .ui-table thead {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
  .ui-table tr {
    border: none; border-bottom: var(--border); border-radius: 0;
    background: none; padding: var(--s3) var(--s4); margin: 0;
  }
  .ui-table tr:last-child { border-bottom: none; }
  .ui-table td {
    border-bottom: none; padding: 2px 0; text-align: left !important;
    display: grid; grid-template-columns: minmax(5rem, 34%) minmax(0, 1fr);
    align-items: baseline; gap: var(--s3); min-height: 0;
    /* A cell that cannot fit wraps. Without this the billing flags cell
       (several pills and a prefill note) pushed the page 226px sideways at
       375: the one failure mode a phone table must never have. */
    flex-wrap: wrap;
  }
  /* The heading, printed beside the value. A cell with nothing in it is
     noise on a card, so it collapses entirely rather than showing a label
     with a blank beside it. */
  .ui-table td::before {
    content: var(--label, '');
    font-size: var(--t-xs); text-transform: uppercase; letter-spacing: .06em;
    color: var(--ink-faint); font-weight: 600;
  }
  .ui-table td:has(> .ui-cell-value:empty) { display: none; }
  /* A link that is the whole value of a cell is a standalone target, and as a
     card row it is 17px of text with nothing around it. Turning tables into
     cards is what exposed this: as a table the cell padding made the target.
     Inline links inside a sentence stay exempt (WCAG 2.5.8). */
  .ui-table td > .ui-cell-value > a:only-child { display: inline-flex; align-items: center; min-height: var(--tap); }
  .ui-table td:nth-child(1) { --label: var(--col-1); }
  .ui-table td:nth-child(2) { --label: var(--col-2); }
  .ui-table td:nth-child(3) { --label: var(--col-3); }
  .ui-table td:nth-child(4) { --label: var(--col-4); }
  .ui-table td:nth-child(5) { --label: var(--col-5); }
  .ui-table td:nth-child(6) { --label: var(--col-6); }
  .ui-table td:nth-child(7) { --label: var(--col-7); }
  .ui-table td:nth-child(8) { --label: var(--col-8); }
  /* The first cell is the identity of the row: it leads, without a label. */
  .ui-table td:first-child {
    display: block; font-weight: 600; padding: 0 0 var(--s2); font-size: var(--t-md);
  }
  .ui-table td:first-child::before { content: none; }
  .ui-table tr.is-link:hover td { background: none; }
  .ui-table tr.is-link:active { background: var(--panel-hover); }

  /* ---- the calendar becomes an agenda ---------------------------------
     Seven columns across 375px gives each day about fifty pixels, which is
     wide enough to prove a day has something on it and too narrow to say
     what. Nobody plans a month on a phone; they check what is on. So the
     same markup relists itself: one row per day that has something, in
     order, empty days dropped. The month is still one tap away on a laptop,
     where the grid is genuinely the better shape. */
  .calgrid { display: block; border: none; border-radius: 0; background: none; gap: 0; }
  .calhead { display: none; }
  .calday.is-other, .calday.is-empty { display: none; }
  .calday {
    min-height: 0; padding: var(--s3) 0;
    border-bottom: var(--border); border-radius: 0;
    display: grid; grid-template-columns: 4.5rem minmax(0, 1fr);
    align-items: start; gap: var(--s3);
  }
  .calday:last-child { border-bottom: none; }
  /* The date, from the custom property the screen publishes per day. It sits
     in the first column of the first row only: spanning it down the whole row
     created that many implicit grid rows in every single day, which left
     roughly three hundred pixels of nothing under each date. */
  .calday::before {
    content: var(--d, ''); grid-column: 1; grid-row: 1;
    font-size: var(--t-xs); font-weight: 600; text-transform: uppercase;
    letter-spacing: .06em; color: var(--ink-faint); padding-top: 3px;
  }
  .calday > * { grid-column: 2; }
  /* Today keeps its band. In a month grid it marks one cell in forty; in an
     agenda it is the thing you are scrolling to find, so it stays. */
  /* The tint bleeds OUTWARD via negative margins: padding alone inset
     today's rows 8px off the shared left edge, making the one day you
     scrolled to find the only misaligned one (audit round). */
  .calday.is-today {
    background: var(--info-bg);
    padding-left: var(--s2); padding-right: var(--s2);
    margin-left: calc(-1 * var(--s2)); margin-right: calc(-1 * var(--s2));
  }
  .calday.is-today::before { color: var(--info); }

  /* The agenda, flattened (Harrison: "very spread out and not super nice").
     The faults measured: the day number printed TWICE (the ::before label
     says "Mon 25" and .dnum said "25" again beside it), and every item wore
     its own filled card, so a three-item day was three grey boxes floating
     in space. The whole .dnum row, number and per-day plan button both, is
     removed further down (the 7a9fe58 decision: the button set a 40px floor
     under every day and planning lives one tap away in the header), so the
     only rules that belong here are the item ones. An earlier cut of this
     block "revealed" the button and hid the number separately: both rules
     were dead on arrival under that later display:none, and the audit
     rightly called the comment a lie. */
  .calday .ui-calitem {
    /* display:block, against the old month-grid rule above that flexed the
       item and flattened its two declared lines into one squeezed run: 85%
       of items measured ellipsized mid-word (audit round). Block restores
       title-then-client, which is the design ui.css declares. */
    display: block;
    background: none; border-radius: 0;
    /* Padding carries the rhythm and the day's row-gap goes to zero, so
       the hairline between items sits centred instead of hugging the row
       below it. */
    padding: 10px var(--s2);
    font-size: var(--t-sm); line-height: var(--lh-tight);
  }
  .calday { row-gap: 0; }
  .calday .ui-calitem + .ui-calitem { border-top: var(--border); }
  .calday .ui-calitem:hover { background: var(--panel-hover); }
  .calday .ui-calitem .cal-sub { font-size: var(--t-xs); margin-top: 2px; }

  /* Header actions are one thumb-panning control rail. A full-width primary
     button on every list screen spent the first phone viewport on chrome.
     The rail preserves every action and keeps the page itself fixed. */
  /* The per-day add button comes off the phone. It cannot shrink below the
     40px target, so it set the floor for every row: a day with one entry was
     117px, of which 40 was a button and 24 was padding. Planning is still one
     tap away in the header, and that planner takes a real date now rather
     than asking you to type YYYY-MM-DD, so nothing is actually lost. On a
     laptop the button stays, where it costs nothing and hover reveals it. */
  .calday .dnum { display: none; }

  /* ---- inputs ---------------------------------------------------------
     Under 16px, iOS Safari zooms the whole viewport the moment a field is
     focused and does not zoom back out. That single behaviour makes a form
     feel broken, and it is a font size, not a bug in the form. */
  /* .ui-input's own size now lives with its touch floor further up. This is
     only the search field, which is more specific and would otherwise win. */
  .ui-search .ui-input { font-size: var(--t-lg); }

  /* The month-at-a-glance cells are square, so at 320px seven of them across
     a rail card come out about 34px and stop being hittable. Height stops
     following width; they go slightly rectangular rather than slightly
     unusable. `aspect-ratio` has to go with it: left on, the taller box asks
     for a matching width and pushed the whole page 20px sideways. */
  .ui-mm-day { min-height: var(--tap); aspect-ratio: auto; }

  /* The week rows and the billing rows are 32px targets on a laptop, which is
     fine with a mouse and under the floor with a thumb. */
  .wk, .bill { min-height: var(--tap); }
}

/* The notch and the home indicator. Fixed elements have to sit inside the
   safe area or the last thing on the screen is under the hardware. */
@supports (padding: env(safe-area-inset-bottom)) {
  .ui-toast { bottom: calc(var(--s5) + env(safe-area-inset-bottom, 0px)); }
}

/* The More sheet: the destinations that do not fit in the phone bar, plus the
   two utilities that used to sit in it. */
.sheet-list {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s2);
}
.sheet-list > li { min-width: 0; }
.sheet-row {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--s2); width: 100%;
  /* The touch floor plus a step: a full-width sheet row wants more air than
     a bar button, and the derived height guard rightly refused the bare 48. */
  min-height: calc(var(--tap) + var(--s7)); padding: var(--s2);
  border: none; background: none; border-radius: var(--r-md);
  color: var(--ink); font: inherit; font-size: var(--t-sm); text-align: center;
  text-decoration: none; cursor: pointer;
}
.sheet-row:hover { background: var(--panel-hover); }
.sheet-row[aria-current="page"] { background: var(--info-bg); color: var(--info); font-weight: 600; }
.sheet-sep { grid-column: 1 / -1; height: 1px; background: var(--line); margin: var(--s2) 0; }
.sheet-sep ~ li { grid-column: 1 / -1; }
.sheet-sep ~ li .sheet-row {
  min-height: calc(var(--tap) + var(--s2)); flex-direction: row;
  justify-content: flex-start; padding-inline: var(--s3); text-align: left;
}

/* The login screen's one-line explanation of what this is. It said nothing at
   all before: an h1, a field and a button, with no indication of whose system
   it is or what to do without a passcode. */
.loginsub { margin: calc(var(--s1) * -1) 0 var(--s5); color: var(--ink-faint); font-size: var(--t-sm); }
/* The error slot is reserved, so a failure does not grow the card and shove
   the swap link down the page. */
/* Reserved so a failure does not grow the card and shove the swap link down.
   Measured: without this the card went 427px to 439px on a wrong passcode. */
.logincard .ui-error { display: block; min-height: 32px; margin: 0; }

/* ------------------------------------------------------------- quote maker */
/* The editor's line grid: item block wide, then qty, unit, line total, remove.
   Labels are on every input for assistive tech; visually the header row above
   the grid carries them once, which is how a line editor stays scannable. */
.qlines-head, .qline {
  display: grid; grid-template-columns: minmax(0, 1fr) 72px 110px 90px 36px;
  gap: var(--s2); align-items: start;
}
.qlines-head {
  font-size: var(--t-xs); font-weight: 700; text-transform: uppercase;
  letter-spacing: .08em; color: var(--ink-faint); margin-bottom: var(--s2);
}
.qline { padding: var(--s2) 0; border-top: var(--border); }
.qline .ui-field label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.qline-main { display: grid; gap: var(--s1); min-width: 0; }
.qline-total {
  font-variant-numeric: tabular-nums; font-size: var(--t-sm); text-align: right;
  align-self: center; color: var(--ink-soft); min-width: 0;
}
.qtotals {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5); justify-content: flex-end;
  border-top: var(--border); margin-top: var(--s3); padding-top: var(--s3);
  font-size: var(--t-sm); color: var(--ink-soft);
}
.qtotals b { font-variant-numeric: tabular-nums; color: var(--ink); }
.qtotals .qgrand { font-size: var(--t-md); }
.qmeta { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); column-gap: var(--s4); }
@media (max-width: 680px) {
  .qlines-head { display: none; }
  .qline { grid-template-columns: minmax(0, 1fr) 64px 96px; }
  .qline-total { grid-column: 1 / 3; text-align: left; }
}

/* The document itself: a sheet, deliberately quieter than the app around it,
   because it is the one surface here a CLIENT's eyes will land on. */
.quotedoc {
  background: var(--panel); border: var(--border); border-radius: var(--r-lg);
  padding: var(--s7) var(--s7) var(--s6); max-width: 720px;
}
.qd-head { display: flex; justify-content: space-between; gap: var(--s4); flex-wrap: wrap; margin-bottom: var(--s6); }
.qd-head > div { display: grid; gap: 2px; }
.qd-brand { font-family: var(--font-head); font-size: var(--t-lg); }
.qd-meta { text-align: right; font-size: var(--t-sm); color: var(--ink-soft); }
.qd-meta b { color: var(--ink); font-size: var(--t-md); }
.qd-to { display: grid; gap: 2px; margin-bottom: var(--s5); font-size: var(--t-sm); }
.qd-totals {
  display: grid; gap: var(--s1); justify-items: end; margin-top: var(--s3);
  font-size: var(--t-sm); color: var(--ink-soft);
}
.qd-totals b { font-variant-numeric: tabular-nums; color: var(--ink); margin-left: var(--s3); }
.qd-totals .qd-grand { font-weight: 700; color: var(--ink); font-size: var(--t-md); border-top: 2px solid var(--ink); padding-top: var(--s1); }
.qd-notes { margin: var(--s5) 0 0; font-size: var(--t-sm); line-height: var(--lh-normal); white-space: pre-wrap; }

/* Marks working chrome around a printable document: ordinary in the flow on
   screen, removed by the print block below. */
.no-print { display: block; }

/* Printing a quote prints the SHEET: the sidebar, the header actions and the
   bottom bar are working chrome, not part of the document. */
@media print {
  .sidebar, .pagehead .actions, .no-print, .ui-toast, [data-navbar] { display: none !important; }
  /* On the quote document the WHOLE page head is working chrome: printing
     "Fenwick Legal, draft" above a client-facing sheet leaked the app's own
     voice onto their paper (panel round 1). */
  main:has(.quotedoc) .pagehead { display: none !important; }
  main { padding: 0 !important; max-width: none !important; }
  .quotedoc { border: 0; padding: 0; max-width: none; }
  body { background: #fff; }
}
