/* Styles for the primitive layer. SPEC 4.1: flat, dense, hairline borders.
 * Every value here comes from tokens.css. No shadows, no gradients, no glows,
 * and no shadow token exists to reach for. */

.ui-sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Focus is always visible. Removing this outline is an accessibility
   regression, not a style choice. Bare links are included because back links
   and mailto links are real Tab stops: iteration 10 found the first Tab stop
   on the shoot page was invisible. */
.ui-btn:focus-visible, .ui-input:focus-visible, .ui-chip:focus-visible,
a:focus-visible {
  outline: 2px solid var(--line-focus);
  outline-offset: 1px;
}

/* `hidden` must actually hide. The UA rule is `[hidden] { display: none }` at
   specificity 0,0,1, so any class rule that sets display (`.app { display:
   grid }`) silently defeats it. That is why the whole app shell, sidebar and
   all, kept rendering behind the login card. Found at iteration 11. */
[hidden] { display: none !important; }

/* ---------------------------------------------------------------- Button */
.ui-btn {
  display: inline-flex; align-items: center; gap: var(--s2);
  /* One md control height (--control-h). A 36px button sat beside a 39px
     input in the board's filter row; six heights across the app read as
     drift, not decisions. box-sizing so min-height means the outer box. */
  box-sizing: border-box; min-height: var(--control-h); justify-content: center;
  font-family: var(--font-sans); font-size: var(--t-md); font-weight: 600;
  padding: var(--s2) var(--s4); border-radius: var(--r-md);
  border: var(--border-strong); background: var(--panel); color: var(--ink);
  cursor: pointer; transition: background var(--fast), opacity var(--fast);
  line-height: var(--lh-tight); white-space: nowrap;
  /* Button({href}) renders a real <a>, and the base `a` rule underlines it.
     Every anchor-button in the app carried its own text-decoration: none, in
     its own bespoke class, which is how the class survived the migration as
     dead CSS. It belongs to the primitive, not to the call site. */
  text-decoration: none;
}
.ui-btn.is-primary { background: var(--info); border-color: var(--info); color: var(--ink-inverse); }
.ui-btn.is-primary:hover:not(:disabled) { opacity: .86; }
/* One quiet blue action accent, shared with information state. A white slab
   in dark mode was the loudest object on every phone screen. */
.ui-btn.is-secondary {
  border-color: var(--line); background: transparent; color: var(--ink-soft); font-weight: 500;
}
.ui-btn.is-secondary:hover:not(:disabled) { background: var(--panel-hover); }
.ui-btn.is-ghost { border-color: transparent; background: transparent; font-weight: 500; color: var(--ink-soft); }
.ui-btn.is-ghost:hover:not(:disabled) { background: var(--panel-hover); color: var(--ink); }
.ui-btn.is-danger { border-color: var(--line-strong); background: var(--panel); color: var(--bad); }
/* An armed destructive control (armConfirm in public/js/ui/confirm.js): the
   button has become the question and the next activation answers it, so it
   wears the bad tone whatever variant it started as. Declared after every
   variant so it wins on equal specificity. */
.ui-btn.is-armed { border-color: var(--bad); background: var(--bad-bg); color: var(--bad); }
.ui-btn.is-danger:hover:not(:disabled) { background: var(--bad-bg); }
.ui-btn.is-sm { padding: var(--s1) var(--s3); font-size: var(--t-sm); min-height: var(--control-h-sm); }
.ui-btn.is-lg { padding: var(--s3) var(--s6); font-size: var(--t-lg); }
.ui-btn:disabled { opacity: .45; cursor: default; }
.ui-btn-icon { font-size: var(--t-md); line-height: 1; }

/* ----------------------------------------------------------------- Field */
.ui-field { margin-bottom: var(--s5); }
/* Checkbox: the label sits beside the box and is part of the target. The grid
   keeps a hint aligned under the label rather than under the box. */
.ui-check { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: var(--s2); margin: var(--s3) 0 var(--s2); }
/* 24px is the WCAG 2.5.8 minimum target, and the hand-rolled version was 16. */
.ui-check-box { width: var(--tap-min); height: var(--tap-min); margin: 0; accent-color: var(--ink); cursor: pointer; }
.ui-check-label { font-weight: 600; font-size: var(--t-md); cursor: pointer; }
.ui-check > .ui-hint { grid-column: 2; margin: 0; }

/* A field in a toolbar row, not in a stacked form. The bottom margin is what
   separates one field from the next down a form; inside a flex row it becomes
   20px of dead space under the control, and every sibling button lands 20px
   below the input it sits beside. */
.ui-field { min-width: 0; max-width: 100%; }
.ui-field.is-compact { margin-bottom: 0; }
.ui-label {
  display: block; font-size: var(--t-md); font-weight: 600;
  color: var(--ink); margin-bottom: var(--s1);
}
.ui-req { color: var(--bad); margin-left: var(--s1); }
.ui-hint {
  font-size: var(--t-sm); color: var(--ink-faint);
  margin-bottom: var(--s2); line-height: var(--lh-normal);
}
.ui-input {
  width: 100%; box-sizing: border-box;
  padding: var(--s2) var(--s3); border: var(--border-strong);
  border-radius: var(--r-md); background: var(--panel); color: var(--ink);
  font-family: var(--font-sans); font-size: var(--t-md); line-height: var(--lh-normal);
}
.ui-input:disabled { background: var(--panel-sunken); color: var(--ink-faint); }
.ui-input[aria-invalid="true"] { border-color: var(--bad); }
/* Single-line controls take the md control height exactly; the textarea is
   the one .ui-input that is legitimately taller. */
input.ui-input, select.ui-input {
  height: var(--control-h); padding-block: 0;
  width: 100%; min-width: 0; max-width: 100%;
  inline-size: 100%; min-inline-size: 0; max-inline-size: 100%;
}
textarea.ui-input { min-height: 88px; resize: vertical; }
.ui-error { font-size: var(--t-sm); color: var(--bad); margin-top: var(--s1); }

/* ----------------------------------------------------------------- Panel */
.ui-panel {
  border: var(--border); border-radius: var(--r-lg);
  background: var(--panel); margin-bottom: var(--s4);
}
/* Wraps. A head is a title plus however many actions the screen gives it, and
   `nowrap` meant three of them could not fit a phone: the Onboarding panel
   measured 444px inside a 341px column and pushed the whole PAGE 86px
   sideways, which is the one thing a phone layout must never do. The actions
   drop to their own line instead. */
.ui-panel-head {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: var(--s2) var(--s3);
  padding: var(--s3) var(--s4); border-bottom: var(--border);
}
/* The section-heading voice, declared once.
   .ui-panel-head h2 and .group-title are the same level of heading: both
   introduce a run of dense rows inside a screen. They were drawn as
   opposites (14px bold tight-tracked ink against 11px uppercase letterspaced
   faint), which read as two products. Today's quiet kicker is the deliberate
   design ("group headings are quiet; rows are dense"), so panels adopt it,
   and the voice lives in this one rule: app.css keeps .group-title's layout
   and must not restate any of this. */
.ui-panel-head h2, .group-title {
  font-family: var(--font-head); font-size: var(--t-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: .1em;
  color: var(--ink-faint); margin: 0;
}
.ui-panel-actions { display: flex; gap: var(--s2); flex-wrap: wrap; }
/* An empty state INSIDE a panel is not its own card: the panel is already the
   card, so the border, the background and the outer margin drew a box inside
   an identical box with a gap between them. Three empty sections on a client
   page spent 660px of a phone screen saying nothing is here. */
.ui-panel-body > .ui-state {
  border: 0; background: none; padding: 0; margin: 0; max-width: none;
}
.ui-panel-body { padding: var(--s4); }
.ui-panel.is-flush .ui-panel-body { padding: 0; }

/* ----------------------------------------------------------------- Table */
/* Wide tables scroll inside this box, never by pushing the page sideways.
   A table standing on its own wears the panel box, so a bare table on
   Clients and a panelled one on the client detail share one geometry: the
   round-3 panel measured bare tables reading 16px misaligned, because the
   cell padding indented their text from the content edge while panelled
   tables' padding sat inside a border that explained it. Inside a Panel the
   box comes from the panel and this resets. */
.ui-table-scroll {
  overflow-x: auto; max-width: 100%;
  border: var(--border); border-radius: var(--r-lg);
  background: var(--panel); margin-bottom: var(--s4);
}
.ui-panel .ui-table-scroll { border: none; border-radius: 0; background: none; margin-bottom: 0; }
.ui-table-scroll:focus-visible { outline: 2px solid var(--line-focus); outline-offset: -2px; }
.ui-table { width: 100%; border-collapse: collapse; font-size: var(--t-md); }
.ui-cell-value { display: contents; }
.ui-table th {
  text-align: left; font-size: var(--t-xs); font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-faint);
  padding: var(--s2) var(--s4); border-bottom: var(--border); white-space: nowrap;
}
.ui-table td {
  padding: var(--s3) var(--s4); border-bottom: var(--border);
  color: var(--ink); vertical-align: middle;
}
.ui-table tr:last-child td { border-bottom: none; }
.ui-table td.is-muted { color: var(--ink-faint); font-size: var(--t-sm); }
.ui-row.is-link { cursor: pointer; }
.ui-row.is-link:hover td { background: var(--panel-hover); }
.ui-row.is-selected td { background: var(--panel-sunken); }
/* The row's real control (see Row in public/js/ui): an <a> or <button> that
   dresses as the cell content it wraps, because the ROW is the visual unit
   and the control is the semantic one. Inline, not contents: an element with
   display: contents generates no box, so its focus outline never paints. */
.ui-rowa {
  display: inline; font: inherit; color: inherit; text-align: inherit;
  background: none; border: 0; padding: 0;
  text-decoration: none; cursor: pointer;
}
.ui-rowa:focus-visible { outline: 2px solid var(--line-focus); outline-offset: 2px; border-radius: var(--r-sm); }

/* ------------------------------------------------------------ Pill, chip */
.ui-pill {
  display: inline-block; font-size: var(--t-xs); font-weight: 600;
  padding: 2px var(--s2); border-radius: var(--r-full);
  border: 1px solid currentColor; line-height: var(--lh-normal);
  white-space: nowrap;  /* a status wrapping inside its own pill reads as broken */
}
.ui-pill.is-neutral { color: var(--ink-faint); }
.ui-pill.is-ok { color: var(--ok); background: var(--ok-bg); }
.ui-pill.is-warn { color: var(--warn); background: var(--warn-bg); }
.ui-pill.is-bad { color: var(--bad); background: var(--bad-bg); }
.ui-pill.is-info { color: var(--info); background: var(--info-bg); }

.ui-chip {
  border: var(--border-strong); background: var(--panel);
  box-sizing: border-box; min-height: var(--control-h-sm);
  border-radius: var(--r-full); padding: var(--s1) var(--s3);
  font-family: var(--font-sans); font-size: var(--t-sm);
  color: var(--ink-soft); cursor: pointer; transition: background var(--fast);
}
.ui-chip:hover { background: var(--panel-hover); }
.ui-chip.is-on { background: var(--ink); border-color: var(--ink); color: var(--ink-inverse); }
/* A client-identity Dot inside a chip needs a breath of space before the
   name; everywhere else the dot's flex parent provides the gap. */
.ui-chip > .ui-dot { margin-right: var(--s1); }

.ui-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: var(--r-full);
  background: var(--panel-sunken); border: var(--border);
  font-size: var(--t-xs); font-weight: 600; color: var(--ink-soft);
}
/* 10px initials were under the 12px floor this project sets for itself, and
   the floor had never actually been measured. 22px carries 12px initials
   without crowding them. */
.ui-avatar.is-sm { width: 22px; height: 22px; font-size: var(--t-xs); }
.ui-avatar.is-lg { width: 36px; height: 36px; font-size: var(--t-sm); }

/* ------------------------------------------------------------------ Tabs */

/* ---------------------------------------------------------- Async states */
/* The skeleton row matches --row-h, the real row height.
   It rendered at 28px against real rows of 58.5px, so six of them stood in for
   a table nearly four times taller and the page jumped 73px every load. A
   skeleton that does not match the shape of what arrives is a grey box, not a
   loading state. */
.ui-skel-row {
  display: flex; align-items: center; gap: var(--s4);
  /* The same padding as .ui-table td, not a smaller one. The skeleton was
     padded --s2 against a cell padded --s3, so even the padding disagreed. */
  min-height: var(--row-h); padding: var(--s3) var(--s4); border-bottom: var(--border);
}
/* A row whose first cell is a name over a second, smaller line. Clients renders
   every row that way, so its skeleton has to as well or the list grows 10px a
   row when the data lands. */
.ui-skel-row.is-2line { min-height: var(--row-h-2line); }
.ui-skel-row:last-child { border-bottom: none; }
.ui-skel {
  /* --line measured 1.23:1 against the panel, which is invisible. A loading
     state you cannot see is the same as no loading state. --skel, not
     --line-strong: that token is a 3:1 control boundary now, and a skeleton
     riding it turns a quiet state into the darkest thing on the page. */
  display: block; height: 12px; border-radius: var(--r-sm);
  background: var(--skel); animation: ui-pulse 1.4s ease-in-out infinite;
}
/* A skeleton shaped like the table that is coming: bars sit in the column
   positions rather than all starting at the left edge. */
.ui-skel-row.is-cols { gap: var(--s4); }
.ui-skel-row.is-cols > .ui-skel { flex: none; }
@keyframes ui-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { .ui-skel { animation: none; } }

/* Left aligned, at the content edge, like everything else on the screen.
   It was a centred card floating in the middle of the canvas: the only
   centred thing in an app of left-aligned tables and forms, so it read as
   a different product's component dropped in. The error variant was already
   overriding text-align back to left, which is the tell. */
.ui-state {
  border: var(--border); border-radius: var(--r-lg); background: var(--panel);
  padding: var(--s6); text-align: left; max-width: 560px;
  margin: var(--s5) 0;
}
.ui-state.is-error { border-left: 3px solid var(--bad); }
.ui-state-icon { font-size: var(--t-2xl); margin-bottom: var(--s2); color: var(--ink-faint); }
/* The empty-state icon used to be a Unicode character sized by font-size. It
   is an SVG now, which font-size does not touch, so it needs its own size or
   it renders at the 16px the navigation uses and looks like a mistake. Not
   large: this marks an empty screen, it is not the subject of it. */
/* No `margin: 0 auto`. .ui-icon is display: block for the navigation, so an
   auto margin centred it inside a full-width row while the heading and the
   body copy sat on the left edge: the icon alone was 241px out. */
.ui-state-icon .ui-icon { width: 28px; height: 28px; stroke-width: 1.2; }
.ui-state h2 { font-family: var(--font-head); font-size: var(--t-lg); margin: 0 0 var(--s1); }
.ui-state p { margin: 0; color: var(--ink-soft); font-size: var(--t-md); line-height: var(--lh-normal); }
.ui-state-actions { display: flex; gap: var(--s2); justify-content: flex-start; margin-top: var(--s4); flex-wrap: wrap; }

.ui-toast {
  position: fixed; bottom: var(--s5); left: 50%; transform: translateX(-50%);
  z-index: 200;
  background: var(--ink); color: var(--ink-inverse);
  border-radius: var(--r-lg); padding: var(--s2) var(--s5); font-size: var(--t-sm);
  /* One show mechanism. The app shell toggled `hidden`, which cannot fade
     because the UA sets display: none, and the two client pages toggled a
     class and did fade. A class for all three, so the behaviour is the same
     everywhere and the transition still honours prefers-reduced-motion
     through the token. */
  opacity: 0; pointer-events: none; transition: opacity var(--normal);
}
.ui-toast.is-show { opacity: 1; }

/* ----------------------------------------------------------------- Modal */
.ui-modal-backdrop {
  /* The old value was rgba(31,31,30,.35), which in dark mode is LIGHTER than
     the page it covers: it lifted #131313 to #171717, so the backdrop lightened
     the page instead of dimming it and a dialog never read as sitting above
     anything. */
  position: fixed; inset: 0; background: var(--scrim);
  display: flex; align-items: center; justify-content: center; padding: var(--s4);
  z-index: 50;
}
.ui-modal {
  background: var(--panel-raised); border: var(--border); border-radius: var(--r-lg);
  /* 680, up from 520: a single 520px column stacked every field into a
     scroll ("can fit way more info in the view height", Harrison). The
     body below lays fields two-up at this width, so a seven-field editor
     fits without scrolling. */
  width: 100%; max-width: 680px; max-height: 88vh; display: flex; flex-direction: column;
}
.ui-modal-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--s4); border-bottom: var(--border);
}
.ui-modal-head h2 { font-family: var(--font-head); font-size: var(--t-lg); margin: 0; }
/* overflow-x hidden is the sheet's CONTRACT, not a mask: a form sheet has
   no meaningful horizontal pan in any engine, and native controls (date
   inputs, selects, fieldsets) carry intrinsic widths that differ per
   browser. The guards still hunt real overflow in emulation; this makes the
   promise hold on engines the harness cannot drive. */
.ui-modal-body { padding: var(--s4); overflow-y: auto; overflow-x: hidden; }
/* Dense dialogs: fields flow two-up when they fit, and anything that needs
   the room (textareas, fieldsets, prose, errors, custom panes) spans both
   columns. Grid auto-placement keeps source order = tab order = reading
   order, so nothing moves for assistive tech. */
.ui-modal-body { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); column-gap: var(--s4); align-content: start; }
.ui-modal-body > .ui-field { margin-bottom: var(--s4); }
.ui-modal-body > .ui-field:has(textarea),
.ui-modal-body > fieldset,
.ui-modal-body > p,
.ui-modal-body > div,
.ui-modal-body > ul,
.ui-modal-body > [role="alert"] { grid-column: 1 / -1; }
.ui-modal-foot { display: flex; gap: var(--s2); justify-content: flex-end; padding: var(--s4); border-top: var(--border); }

/* ------------------------------------------- composite form groups
 * A label cannot name a group of buttons or a dropzone, so composite
 * controls use a real fieldset and legend instead. */
/* min-inline-size: a fieldset's UA default is min-content, which Safari
   honours hard: one wide child and the whole sheet body pans sideways under
   an iOS scroll indicator (Harrison's screenshot; Chrome emulation never
   reproduced it because Blink shrinks fieldsets). Zeroed at the layer. */
.ui-fieldset { border: none; margin: 0; padding: 0; min-inline-size: 0; }
.ui-fieldset > .ui-label { padding: 0; }

.ui-multi { margin-bottom: var(--s4); max-width: 100%; }
.ui-multi-options {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--s2); min-width: 0;
}
.ui-multi-option {
  position: relative; display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: var(--s2); min-height: var(--control-h);
  padding: var(--s2) var(--s3); border: var(--border-strong);
  border-radius: var(--r-md); background: var(--panel); color: var(--ink-soft);
  cursor: pointer;
}
.ui-multi-input { position: absolute; opacity: 0; pointer-events: none; }
.ui-multi-option:has(.ui-multi-input:focus-visible) {
  outline: 2px solid var(--line-focus); outline-offset: 1px;
}
.ui-multi-option:has(.ui-multi-input:checked) {
  border-color: var(--info); background: var(--info-bg); color: var(--ink);
}
.ui-multi-meta {
  min-width: var(--tap-min); color: var(--ink-faint); font-size: var(--t-xs);
  font-weight: 700; letter-spacing: .06em;
}
.ui-multi-option:has(.ui-multi-input:checked) .ui-multi-meta { color: var(--info); }
.ui-multi-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.ui-multi-mark {
  width: var(--s3); height: var(--s3); border: var(--border-strong);
  border-radius: var(--r-full); background: var(--panel); box-sizing: border-box;
}
.ui-multi-input:checked ~ .ui-multi-mark { border-color: var(--info); background: var(--info); }

.ui-chips { display: flex; flex-wrap: wrap; gap: var(--s2); }

.ui-listrow { display: flex; gap: var(--s2); align-items: center; margin-bottom: var(--s2); }
.ui-listrow .ui-input { flex: 1; }
.ui-listadd { margin-top: var(--s2); }
.ui-swatch {
  /* Height comes from the text input it sits beside, not from a number. A
     fixed 36px left the swatch 7px shorter than the field next to it, and any
     change to the input's padding or type scale would move that gap again. */
  /* height: auto is not redundant. A colour input takes a height from the UA
     stylesheet, and align-self: stretch cannot grow a box whose height is not
     auto, so without this the swatch stayed at 27px beside a 43px field. */
  width: 40px; padding: 2px; flex: none; align-self: stretch; height: auto;
  border: var(--border-strong); border-radius: var(--r-md);
  background: var(--panel); cursor: pointer;
}

/* The dropzone is a real button so Tab and Enter reach it. Before this it was
   a div with a click handler, which meant keyboard-only clients could not
   upload at all. */
.ui-drop {
  /* flex, not block. .ui-icon is display:block for the navigation, so the
     upload arrow formed its own line, hard left with no auto margin, while the
     label formed an anonymous block below it and centred. Two lines, one of
     them misaligned, on the control every client has to use. This is the same
     failure the empty-state icon has a fix for eight rules up. */
  display: flex; align-items: center; justify-content: center; gap: var(--s2);
  width: 100%; text-align: center; cursor: pointer;
  border: 1.5px dashed var(--line-strong); border-radius: var(--r-md);
  background: var(--panel); color: var(--ink-soft);
  padding: var(--s5) var(--s4); font-family: var(--font-sans); font-size: var(--t-sm);
  transition: background var(--fast), border-color var(--fast);
}
.ui-drop:hover, .ui-drop.is-over { border-color: var(--ink); color: var(--ink); background: var(--panel-hover); }
.ui-drop:focus-visible { outline: 2px solid var(--line-focus); outline-offset: 1px; }

.ui-filelist { display: flex; flex-direction: column; gap: var(--s2); margin-top: var(--s3); }
.ui-filerow {
  display: flex; align-items: center; gap: var(--s3);
  border: var(--border); border-radius: var(--r-md);
  padding: var(--s2) var(--s3); background: var(--panel); font-size: var(--t-sm);
}
.ui-thumb { width: 32px; height: 32px; object-fit: contain; border-radius: var(--r-sm); background: var(--panel-sunken); flex: none; }
.ui-thumb.is-doc { display: inline-flex; align-items: center; justify-content: center; font-size: var(--t-lg); }
.ui-fname { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ui-fsize { color: var(--ink-faint); font-size: var(--t-xs); }

/* Touch tap targets. Measured at 375px: small ghost buttons rendered 26px
   tall, well under the 40px a finger needs.
   By WIDTH, not pointer type: app.css already records the lesson this block
   had not learned ("pointer: coarse alone is invisible to a resized desktop
   window", and to any measuring harness). The wizard loads ui.css without
   app.css, so its floors have to hold here; the mobile sweep found the
   stepper at 23px on the one page a client uses. */
@media (max-width: 800px) {
  /* EVERY button meets the 40px floor on a phone, not only the small ones:
     the wizard loads this sheet without app.css, and its default-size
     "Review my answers" measured 36px on the one page a client touches
     (panel round 1). app.css carried this rule for the admin app; the floor
     belongs to the layer both load. */
  .ui-btn { min-height: var(--tap); }
  .ui-btn.is-sm { min-height: var(--tap); min-width: var(--tap); justify-content: center; }
  /* min-height, not height: the swatch now stretches to the field beside it,
     and a fixed height would shrink it back on a phone, where the field is
     taller than 40px and the two would stop lining up. */
  .ui-swatch { min-height: var(--tap); }
  .ui-chip { min-height: var(--tap); }
  /* The checkbox row measured 24px on a phone against the 40px floor every
     other small control in this block already keeps (panel round 1). */
  .ui-check { min-height: var(--tap); align-items: center; }

  /* -------------------------------------------- tables become card stacks
     The declared mobile phase, finally built. Table() has published every
     column label as --col-N on the table element since round-8, precisely so
     this stylesheet could print labels beside values when a table flattens;
     the flattening itself was never written, so every table on a phone was a
     sideways scroller: contacts, the content queue, billing, the team list,
     and any table inside a MODAL, which is where Harrison hit it. Roles are
     pinned in the markup (role=table/row/cell), so display:block here does
     not cost the semantics it would normally strip. */
  .ui-table-scroll { overflow-x: visible; }
  .ui-table, .ui-table tbody { display: block; }
  /* Visually hidden, not display:none: the columnheaders stay in the tree
     for assistive tech while the sighted reader gets per-cell labels. */
  .ui-table thead {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
  .ui-table tr {
    display: block; padding: var(--s3) var(--s4); border-bottom: var(--border);
  }
  .ui-table tr:last-child { border-bottom: none; }
  .ui-table td { display: block; border: none; padding: 0 0 var(--s2); font-size: var(--t-sm); }
  .ui-table td:last-child { padding-bottom: 0; }
  /* The label the column header carried, printed above the value. The first
     cell is the row's identity and gets no label: "Name" over every card is
     noise, and the identity cell already reads at full weight. */
  .ui-table td::before {
    display: block; font-size: var(--t-xs); font-weight: 600;
    text-transform: uppercase; letter-spacing: .06em; color: var(--ink-faint);
    margin-bottom: 1px;
  }
  .ui-table td:nth-child(2)::before { content: var(--col-2, ''); }
  .ui-table td:nth-child(3)::before { content: var(--col-3, ''); }
  .ui-table td:nth-child(4)::before { content: var(--col-4, ''); }
  .ui-table td:nth-child(5)::before { content: var(--col-5, ''); }
  .ui-table td:nth-child(6)::before { content: var(--col-6, ''); }
  .ui-table td:nth-child(7)::before { content: var(--col-7, ''); }
  .ui-table td:nth-child(8)::before { content: var(--col-8, ''); }
  /* A cell a row chose to leave blank is not a card line. */
  .ui-table td:has(> .ui-cell-value:empty) { display: none; }
  .ui-table .ui-cell-value { display: block; min-width: 0; overflow-wrap: anywhere; }
  .ui-table td:first-child { font-size: var(--t-md); }
  /* The row's control fills the card's whole first line, so the tap target
     is the card's width, not the word's. */
  .ui-table .ui-rowa { display: block; width: 100%; }

  /* ------------------------------------------------ mobile editors
     These are working records, not confirmation dialogs. A nearly full-height
     bottom sheet left the app visible behind a desktop form and made the
     editor feel temporary even when it held fifteen fields. On a phone the
     dialog becomes its own screen: one top bar, one scrolling canvas and one
     action dock. The same markup and focus trap continue to serve desktop. */
  .ui-modal-backdrop { align-items: stretch; padding: 0; background: var(--bg); }
  .ui-modal {
    max-width: none; width: 100%; height: 100%; height: 100dvh;
    border: 0; border-radius: 0; background: var(--bg);
    max-height: none;
  }
  .ui-modal-head {
    flex: none; min-height: calc(var(--control-h-mobile) + 2 * var(--s2));
    padding: calc(var(--s2) + env(safe-area-inset-top, 0px)) var(--s4) var(--s2);
    background: var(--panel); position: relative; z-index: 1;
  }
  .ui-modal-head h2 {
    font-size: var(--t-xl); overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; min-width: 0;
  }
  .ui-modal-head > [data-close] {
    flex: none; width: var(--control-h-mobile); min-height: var(--control-h-mobile);
    padding: 0; font-size: 0;
  }
  .ui-modal-head > [data-close] .ui-btn-icon { display: inline-flex; font-size: var(--t-md); }
  .ui-modal-body {
    display: block; flex: 1; min-height: 0;
    padding: var(--s5) var(--s4) var(--s8); background: var(--bg);
  }
  .ui-modal-body > .ui-field { margin-bottom: var(--s5); }
  .ui-modal-body .ui-label {
    color: var(--ink-faint); font-size: var(--t-xs); text-transform: uppercase;
    letter-spacing: .08em; margin-bottom: var(--s2);
  }
  .ui-modal-body input.ui-input,
  .ui-modal-body select.ui-input { height: var(--control-h-mobile); }
  .ui-modal-body .ui-field,
  .ui-modal-body .ui-fieldset { min-width: 0; max-width: 100%; inline-size: 100%; }
  /* iOS gives date and time inputs a UA min-content width larger than the
     grid track. Width and max-width alone do not beat it, which is the box
     Harrison photographed running under the right edge. Removing only the
     painted appearance keeps the native date and time picker semantics while
     making the control obey the same box contract as every other field. */
  .ui-modal-body input[type="date"].ui-input,
  .ui-modal-body input[type="time"].ui-input {
    -webkit-appearance: none; appearance: none; display: block;
    width: 100%; min-width: 0; max-width: 100%;
    inline-size: 100%; min-inline-size: 0; max-inline-size: 100%;
    padding-block: 0; line-height: var(--control-h-mobile);
  }
  .ui-modal-body input[type="date"].ui-input::-webkit-date-and-time-value,
  .ui-modal-body input[type="time"].ui-input::-webkit-date-and-time-value {
    display: flex; align-items: center; height: 100%;
    text-align: left; min-width: 0; line-height: normal;
  }
  .ui-modal-body .ui-multi-option { min-height: var(--control-h-mobile); }
  .ui-modal-body .ui-input { background: var(--panel); border-radius: var(--r-lg); }
  .ui-modal-body textarea.ui-input { min-height: calc(3 * var(--control-h-mobile)); }
  .ui-modal-foot {
    flex: none; min-height: calc(var(--control-h-mobile) + 2 * var(--s3));
    padding: var(--s3) var(--s4) calc(var(--s3) + env(safe-area-inset-bottom, 0px));
    background: var(--panel); position: relative; z-index: 1;
  }
  .ui-modal-foot .ui-btn { min-height: var(--control-h-mobile); }
  .ui-modal-foot .ui-btn.is-secondary { border-color: transparent; background: transparent; }
  .ui-modal-foot .ui-btn.is-danger { margin-right: auto; }
  .ui-modal-foot .ui-btn.is-record-action { margin-right: auto; }
  body.is-modal-open .ui-toast {
    bottom: calc(var(--control-h-mobile) + var(--s5) + env(safe-area-inset-bottom, 0px));
  }
}

.ui-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s3); }

/* The login card, once. The shell and the legacy admin both render it, and
   each stylesheet carried its own geometry: 340/14vh/s6 against
   360/12vh/28px, one class, two products (round-4). */
.logincard {
  max-width: 340px; margin: 14vh auto 0; border: var(--border);
  background: var(--panel); border-radius: var(--r-lg); padding: var(--s6);
}
.logincard h1 { font-family: var(--font-head); font-size: var(--t-xl); margin: 0 0 var(--s4); }
/* The mode-swap line under the form. Rendered for as long as the two-mode
   login has existed and styled nowhere: the page-styles guard's first catch. */
.loginswap { margin: var(--s4) 0 0; }
/* Content-driven, not the phone split: this is the width where two side by
   side name/company fields stop fitting inside a DIALOG, whose own width is
   independent of the viewport class. Named here so the breakpoint agreement
   test's 700-900 band excludes it by decision rather than by accident
   (round-5). */
@media (max-width: 620px) { .ui-grid2 { grid-template-columns: 1fr; } }
.ui-sub { color: var(--ink-faint); font-size: var(--t-sm); }
.ui-btn[aria-current="page"] { font-weight: 700; background: var(--panel-hover); }

/* Step navigator: real buttons, so they need button resets. These rules were
   defined here AND in style.css, with different font sizes and colours, and
   which one won depended on load order. One definition now. */
.ui-stepper { display: flex; flex-wrap: wrap; gap: var(--s1) var(--s4); margin-top: var(--s3); }
.ui-step {
  background: none; border: none; font-family: var(--font-sans);
  font-size: var(--t-xs); color: var(--ink-faint); cursor: pointer;
  padding: var(--s1) 0; border-radius: var(--r-sm);
}
.ui-step:hover { color: var(--ink); }
.ui-step.is-now { color: var(--ink); font-weight: 600; }
.ui-step.is-done { color: var(--ok); }
.ui-step:focus-visible { outline: 2px solid var(--line-focus); outline-offset: 2px; }
@media (max-width: 800px) {
  .ui-stepper {
    flex-wrap: nowrap; gap: var(--s2); max-width: 100%; overflow-x: auto;
    scrollbar-width: none; overscroll-behavior-inline: contain;
  }
  .ui-stepper::-webkit-scrollbar { display: none; }
  .ui-step { flex: none; min-height: var(--tap); padding: var(--s2); }
}
/* A heading focused programmatically must not draw a focus ring: the user did
   not tab to it, so a ring reads as a bug. */
h1[tabindex="-1"]:focus { outline: none; }


/* The current brand-pack wordmark is dark ink. Invert it on a dark surface,
   while an explicit Light choice must outrank a dark operating system. */
:root[data-theme="dark"] .brandlogo { filter: invert(1); }
:root[data-theme="light"] .brandlogo { filter: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .brandlogo { filter: invert(1); }
}


/* Client colour dot, progress bar and calendar entry. */
.ui-dot { display: inline-block; width: 8px; height: 8px; border-radius: var(--r-full); flex: none; }
.ui-dot.is-lg { width: 10px; height: 10px; }

.ui-progress {
  display: inline-block; height: 6px; width: 100px; background: var(--line);
  border-radius: var(--r-full); overflow: hidden; vertical-align: middle;
}
.ui-progress > i { display: block; height: 100%; background: var(--ok); }
.ui-progress.is-behind > i { background: var(--warn); }

/* 24px is the WCAG 2.5.8 AA floor, and a calendar entry was 20px stacked 2px
   from the next one, which is also too close for the spacing exception to
   apply. A month cell is 96px, so three entries at 24 still fit: the density
   this was protecting was never actually at risk. */
.ui-calitem {
  display: block; min-height: var(--tap-min);
  width: 100%; text-align: left; border: none;
  border-left: 3px solid var(--ink-faint); cursor: pointer;
  font-family: var(--font-sans); font-size: var(--t-xs); line-height: 16px;
  padding: var(--s1) var(--s1); border-radius: var(--r-sm);
  background: var(--panel-hover); color: var(--ink);
  overflow: hidden;
}
/* Two lines now: time + platform tag + title, then the client. Each line
   ellipsizes on its own instead of the whole chip being one nowrap run. */
.ui-calitem .cal-l1 { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ui-calitem .cal-sub { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-faint); }
.ui-calitem .cal-time { font-weight: 700; margin-right: var(--s1); }
.ui-calitem .cal-tag {
  display: inline-block; font-size: 9px; font-weight: 700; letter-spacing: .06em;
  border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  padding: 0 3px; margin-right: var(--s1); vertical-align: 1px; color: var(--ink-soft);
}

/* A link that is the entire value of a table cell is a standalone target.
   Contact emails were 17px. The phone layer raises the same rule to 40. */
.ui-table td > a:only-child {
  display: inline-flex; align-items: center; min-height: var(--tap-min);
}
.ui-calitem:hover { background: var(--panel-sunken); }
.ui-calitem:focus-visible { outline: 2px solid var(--line-focus); outline-offset: 1px; }
/* Opacity was doing the receding, and it took the text with it: a posted
   entry read at 3.49:1, under the 4.5:1 AA floor, and most entries in a
   past month are posted. The strikethrough already says done, so the
   colour recedes to a token that is still legible instead. */
/* --ink-faint is rated against --panel, and the entry sits on --panel-hover,
   which is lighter: 4.49:1 in dark, missing AA by a hundredth. A done entry
   drops to the flatter background, which both clears the floor and is the
   right visual answer, since receding is the whole point of the state. */
.ui-calitem.is-done {
  color: var(--ink-faint); background: var(--panel); text-decoration: line-through;
}
/* The edge colour rides the element itself (the CalItem call passes
   colour: '--warn'), because the data-css border always beats a class rule:
   the border-left-color declared here was dead from birth. */
.ui-calitem.is-billing { background: var(--warn-bg); }
/* The dollar glyph is drawn by the chip, not pasted into the label: as label
   text it sat visibly off the baseline beside ellipsized company names
   (Harrison's screenshot), and a glyph the CSS owns is one the type scale
   can align. */
.ui-calitem.is-billing::before {
  content: '$'; display: inline-block; margin-right: 3px;
  font-weight: 700; line-height: inherit; color: var(--warn);
}


/* Links inherit the browser default otherwise, which is a blue that fails
   contrast on a dark panel and is not in the palette. */
a { color: var(--info); text-decoration-color: color-mix(in srgb, var(--info) 40%, transparent); }
a:hover { text-decoration-thickness: 2px; }

/* ===================================================================
   Data display, added at iteration 14 from DESIGN_REFERENCE.md.
   Flat and hairline like everything else (SPEC 4.1). The only colour is
   the data itself: a ring's fill, a bar, a delta's direction.
   =================================================================== */

/* Ring: goal against actual. */

/* Metric: kicker, value, delta. The delta carries as much as the value. */
.ui-metric { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ui-metric-label {
  font-size: var(--t-xs); color: var(--ink-faint); text-transform: uppercase;
  letter-spacing: .06em; font-weight: 600; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.ui-metric-row { display: flex; align-items: baseline; gap: var(--s2); }
.ui-metric-value {
  font-family: var(--font-head); font-size: var(--t-xl); font-weight: 700;
  line-height: 1; letter-spacing: -0.02em;
}
.ui-metric.is-bad .ui-metric-value { color: var(--bad); }
.ui-metric.is-warn .ui-metric-value { color: var(--warn); }
.ui-metric-delta { font-size: var(--t-xs); font-weight: 600; white-space: nowrap; }
.ui-metric-delta.is-up { color: var(--ok); }
.ui-metric-delta.is-down { color: var(--bad); }
.ui-metric-delta.is-flat { color: var(--ink-faint); }
.ui-metric-sub { font-size: var(--t-sm); color: var(--ink-faint); }

/* Sparkbars: a short history as shape. */

/* Chart: bars and a line on a shared axis. */

/* MiniMonth: the month as shape, not as entries. */
.ui-mm { width: 100%; }
.ui-mm-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.ui-mm-head {
  font-size: var(--t-xs); color: var(--ink-faint); text-align: center; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em; padding-bottom: 2px;
}
.ui-mm-pad { display: block; }
.ui-mm-day {
  aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
  border: 1px solid transparent; border-radius: var(--r-sm);
  background: var(--panel-sunken); color: var(--ink-faint);
  font-family: var(--font-sans); font-size: var(--t-xs); cursor: pointer; padding: 0;
}
.ui-mm-day:hover { border-color: var(--line-strong); color: var(--ink); }
.ui-mm-day.is-l1 { background: var(--panel-hover); color: var(--ink-soft); }
.ui-mm-day.is-l2 { background: var(--info-bg); color: var(--ink-soft); }
.ui-mm-day.is-l3 { background: var(--info-bg); color: var(--ink); border-color: var(--info); }
.ui-mm-day.is-l4 { background: var(--info); color: var(--panel); font-weight: 700; }
/* The today marker used `outline`, at rest, which an author outline wins over
   the UA focus ring: so the one cell most likely to be tabbed to was the only
   tabbable control in the default view with no focus indicator at all. The
   marker moves to an inset shadow and the outline goes back to meaning focus.
   The other 30 cells had no :focus-visible rule and fell back to Chrome's 1px
   amber ring, which is neither the app's ring nor consistent across browsers. */
.ui-mm-day.is-today { box-shadow: inset 0 0 0 2px var(--ink); font-weight: 700; color: var(--ink); }
.ui-mm-day:focus-visible { outline: 2px solid var(--line-focus); outline-offset: 1px; }

/* SentenceCard: icon, kicker, sentence, chevron. */

/* Segments: progress through a fixed number of steps. */
/* Segments share the width they are given rather than each claiming a
   fixed 26px. A client promising 16 items rendered 461px of segments and
   pushed every phone screen 184px sideways: found at iteration 14 by
   measuring, not by looking. */
.ui-segs { display: flex; gap: 2px; align-items: center; flex: 1; min-width: 0; }
.ui-seg {
  display: block; flex: 1; min-width: 2px; max-width: 26px; height: 4px;
  border-radius: var(--r-full); background: var(--line);
}

/* SearchInput: a toolbar search box. The label is hidden, never absent. */
.ui-search { display: inline-flex; min-width: 0; flex: 0 1 280px; }
.ui-search .ui-input { width: 100%; }

/* Skip to content. Off screen until it has focus, then a real target.
   Not display:none, which would take it out of the tab order and defeat the
   entire point. */
.ui-skip {
  position: absolute; left: var(--s3); top: -100px; z-index: 200;
  background: var(--panel); color: var(--ink); border: var(--border-strong);
  border-radius: var(--r-md); padding: var(--s2) var(--s4);
  font-size: var(--t-md); font-weight: 600; text-decoration: none;
  transition: top var(--fast);
}
.ui-skip:focus { top: var(--s3); outline: 2px solid var(--line-focus); outline-offset: 2px; }

/* Command palette. SPEC 4.3 and 4.5.
   Sits on the shared modal chrome so it inherits the backdrop, the radius and
   the border rather than inventing a second dialog shape. */
.ui-cmd { max-width: 560px; padding: 0; overflow: hidden; }
.ui-cmd-field { padding: var(--s3); border-bottom: var(--border); }
.ui-cmd-field .ui-input { border: none; padding: var(--s2) var(--s2); font-size: var(--t-lg); }
.ui-cmd-field .ui-input:focus { outline: none; }
.ui-cmd-list { list-style: none; margin: 0; padding: var(--s1); max-height: 46vh; overflow-y: auto; }
.ui-cmd-item {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s2) var(--s3); border-radius: var(--r-md); cursor: pointer;
  min-height: var(--control-h);
}
.ui-cmd-item.is-active { background: var(--panel-hover); box-shadow: inset 2px 0 0 0 var(--ink); }
.ui-cmd-kind {
  flex: none; min-width: 58px; font-size: var(--t-xs); font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em; color: var(--ink-faint);
}
.ui-cmd-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ui-cmd-hint { flex: none; font-size: var(--t-sm); color: var(--ink-faint); }
.ui-cmd-empty { padding: var(--s5) var(--s3); color: var(--ink-faint); font-size: var(--t-sm); }
.ui-cmd-foot {
  display: flex; gap: var(--s4); padding: var(--s2) var(--s4);
  border-top: var(--border); font-size: var(--t-xs); color: var(--ink-faint);
}
@media (max-width: 800px) { .ui-cmd-foot { display: none; } }

/* Line icons. currentColor, so they inherit whatever the nav sets, and a fixed
   line box so a row height cannot depend on which glyph a fallback font
   supplied. That mismatch is what made four nav rows 34px and four 35px. */
.ui-icon { display: block; width: 16px; height: 16px; flex: none; }

/* With no results there is no active option carrying the focus indication, so
   the input takes it back. Otherwise a sighted keyboard user is looking at a
   dialog with no focus anywhere on it. */
.ui-cmd.is-empty .ui-cmd-field .ui-input:focus { outline: 2px solid var(--line-focus); outline-offset: -2px; }
/* The kind label sits on --panel-hover when its row is active, where
   --ink-faint measures 4.34:1 light and 4.49:1 dark. It is the label telling
   you whether you are about to open a screen or a client. */
.ui-cmd-item.is-active .ui-cmd-kind { color: var(--ink-soft); }
/* The placeholder is the combobox's only visible description, and it was
   rendering in Chrome's default #757575: 3.78:1 on the dark panel. */
.ui-cmd-field .ui-input::placeholder { color: var(--ink-faint); opacity: 1; }

/* Native disclosure, flattened into the same hairline record language. */
.ui-disclosure { border-top: var(--border); }
.ui-disclosure:first-child { border-top: 0; }
.ui-disclosure summary {
  min-height: var(--control-h); display: flex; align-items: center;
  padding: var(--s2) 0; cursor: pointer; color: var(--ink-soft);
  font-size: var(--t-sm); font-weight: 600;
}
.ui-disclosure summary:focus-visible { outline: 2px solid var(--line-focus); outline-offset: 2px; }
.ui-disclosure-body { padding: 0 0 var(--s3); }
