/* ═══════════════════════════════════════════════════════════════════════════
   components.css — the shared kit: lists, forms, charts.

   Loaded after theme-apple.css, so every colour here is a token the theme
   already owns (--sys-blue, --label-3, --separator-2 …) and a theme change
   moves the kit with it. Nothing in this file styles a screen; screens carry
   their own stylesheet.

   Contents
     1  tables: sticky head, scrollbars, sort, priorities, phone cards
     2  pager
     3  filters, facets
     4  bulk selection
     5  row menus
     6  cell conventions: copy, truncation, ids
     7  fields: hints, errors, adornments, disabled, switches, section index
     8  sheets
     9  charts
    10  timeline, breadcrumbs, diff
    11  polish pass (wave C)
    12  phone hit targets
    13  skeleton loading
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── 1. tables ──────────────────────────────────────────────────────────── */

/* Every .t-wrap is a containing block.
   A .visually-hidden cell is position:absolute; inside a scrolling .t-wrap it
   has no positioned ancestor, so its containing block is the page rather than
   the scroller and it is NOT clipped by it. Measured: the "Actions" span in the
   last header of an 839px-wide returns table sat at x=1075 and widened the
   document by 51px at 1024 even though the table itself was clipped. The
   settings page fixed its own copy of this (admin.css); this is the general one. */
.t-wrap{position:relative}

/* A tall list scrolls inside its own box and keeps its header in view. */
.t-wrap.tall{max-height:min(70vh,720px); overflow:auto}
.t-wrap.tall table.t thead th{position:sticky; top:0; z-index:1; background:var(--surface-2); box-shadow:0 1px 0 var(--separator-2, var(--hair))}

/* Thin scrollbars everywhere a panel scrolls. */
.t-wrap,.board,.more-nav,dialog.sheet,dialog.sheet .card-body{scrollbar-width:thin; scrollbar-color:var(--sys-gray4, #D1D1D6) transparent}

/* Sortable column heads. The caret is drawn, not an icon font. */
table.t th a.sort{color:inherit; text-decoration:none; display:inline-flex; align-items:center; gap:4px; min-height:24px}
table.t th a.sort::after{
  content:""; width:8px; height:8px; flex:none; opacity:.35;
  background:currentColor;
  -webkit-mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'><path d='M1 2.8 4 5.8 7 2.8' fill='none' stroke='black' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/></svg>") center/8px no-repeat;
  mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'><path d='M1 2.8 4 5.8 7 2.8' fill='none' stroke='black' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/></svg>") center/8px no-repeat;
}
table.t th a.sort:hover{text-decoration:underline; text-underline-offset:2px}
table.t th[aria-sort] a.sort{color:var(--accent-ink, var(--sys-blue-2)); font-weight:600}
table.t th[aria-sort] a.sort::after{opacity:1}
table.t th[aria-sort="ascending"] a.sort::after{transform:rotate(180deg)}

/* An empty list says what would appear here, and never pretends to be a row. */
table.t tr.empty-row td{color:var(--ink-3); padding:22px 14px; text-align:center}

/* Column priorities: 3 goes first, then 2. Priority 1 is never hidden. */
@media (max-width:920px){
  table.t th[data-p="3"],table.t td[data-p="3"]{display:none}
}

/* ── phone: each row becomes a card, labelled from its column header ────── */
@media (max-width:600px){
  table.t.cards{min-width:0 !important}
  .t-wrap:has(table.t.cards){overflow-x:visible}
  table.t.cards thead{display:none}
  table.t.cards tbody tr{display:grid; grid-template-columns:1fr auto; gap:2px 12px; padding:10px 14px; height:auto; border-bottom:1px solid var(--hair-2)}
  table.t.cards tbody tr:last-child{border-bottom:0}
  table.t.cards td,table.t.cards th{display:block; height:auto; padding:0; border:0}
  table.t.cards th.who{grid-column:1; font-size:14px}
  table.t.cards td.r{text-align:right}
  table.t.cards td[data-label]:not(.who)::before{content:attr(data-label) " "; color:var(--label-3, var(--ink-3)); font-size:11px}
  table.t.cards td.act{grid-column:2; grid-row:1; justify-self:end}
  /* A cell carrying a SENTENCE takes the whole row. The card grid is
     `1fr auto`, and an auto track sized by "Call the merchant, then log the
     contact" crushed the 1fr one to a single character per line — measured on
     /app/collections at 390 the moment the worklist's Next step column was
     promoted to priority 1. Prose spans; short values pair up. */
  table.t.cards td.wide,table.t.cards th.wide{grid-column:1 / -1}
  /* a card row that can be selected: checkbox first, name beside it, menu last */
  table.t.cards tbody tr:has(td.pick){grid-template-columns:auto minmax(0,1fr) auto}
  table.t.cards td.pick{grid-column:1; grid-row:1; align-self:center}
  table.t.cards tbody tr:has(td.pick) th.who{grid-column:2}
  table.t.cards tbody tr:has(td.pick) td:not(.pick):not(.act){grid-column:2 / -1}
  table.t.cards tbody tr:has(td.pick) td.act{grid-column:3; grid-row:1}
  table.t.cards tr.empty-row{display:block; padding:22px 14px}
  /* last, and specific enough to beat the display:block above */
  table.t th[data-p="2"],table.t td[data-p="2"],table.t.cards th[data-p="2"],table.t.cards td[data-p="2"],
  table.t th[data-p="3"],table.t td[data-p="3"],table.t.cards th[data-p="3"],table.t.cards td[data-p="3"]{display:none}
}

/* A wide numeric table that stays tabular tells you there is more to the right. */
.t-wrap.more{position:relative}
/* app.js gives a .t-wrap that actually overflows and holds nothing focusable
   tabindex="0" and role="group", because otherwise a keyboard-only operator has
   no way to move the scroll port at all (WCAG 2.1.1) — the vintages cohort table
   is 714px of numbers in a 360px box with no link or button in it. */
.t-wrap:focus-visible{outline:none; box-shadow:var(--focus-ring); border-radius:var(--r-c, 8px)}
.t-wrap.more::after{
  content:""; position:absolute; top:0; right:0; bottom:0; width:28px; pointer-events:none;
  background:linear-gradient(to right, rgba(255,255,255,0), var(--surface));
}

/* ── 2. pager ───────────────────────────────────────────────────────────── */

.t-foot{display:flex; align-items:center; gap:12px; flex-wrap:wrap; padding:10px 16px; border-top:1px solid var(--hair-2); font-size:12px; color:var(--ink-3)}
.t-foot .range{white-space:nowrap}
.t-foot .pages{margin-left:auto; display:flex; align-items:center; gap:6px; flex-wrap:wrap}
.t-foot .pages .btn[aria-disabled="true"]{opacity:.4; cursor:default; pointer-events:none}
.t-foot .pg{
  min-width:30px; min-height:30px; padding:0 8px; border-radius:var(--r-c);
  display:inline-flex; align-items:center; justify-content:center;
  color:var(--ink-2); text-decoration:none; border:1px solid transparent; font-size:12px;
}
.t-foot .pg:hover{background:var(--surface-2); border-color:var(--hair)}
.t-foot .pg.on{background:var(--navy-tint); color:var(--navy); font-weight:600; border-color:transparent}
.t-foot .per{margin-left:6px}

@media (max-width:600px){
  .t-foot{padding:12px 14px}
  .t-foot .pages{width:100%; margin-left:0; justify-content:space-between}
  .t-foot .pages .btn,.t-foot .pg{min-height:44px; min-width:44px}
  .t-foot .per select{min-height:44px}
}

/* ── 3. filters ─────────────────────────────────────────────────────────── */

.card > .filters{padding:10px 16px; border-bottom:1px solid var(--hair-2)}
.filters .seg{flex-wrap:wrap}
.filters .facet{display:inline-flex; margin:0}
.filter-acts{margin-left:auto; display:flex; align-items:center; gap:8px}

.chip-select{
  appearance:none; -webkit-appearance:none;
  border:1px solid var(--hair); background:var(--surface); color:var(--ink);
  border-radius:20px; padding:5px 28px 5px 11px; font:inherit; font-size:12px; min-height:30px;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><path d='M3 4.5 6 7.5 9 4.5' fill='none' stroke='%236E6E73' stroke-width='1.6' stroke-linecap='round'/></svg>");
  background-repeat:no-repeat; background-position:right 9px center; background-size:12px;
}
.chip-select:hover{border-color:var(--line)}
.chip-select:focus-visible{border-color:var(--navy); box-shadow:0 0 0 3px var(--navy-tint); outline:none}

@media (max-width:600px){
  .filters{gap:8px; overflow-x:auto; scrollbar-width:none}
  .card > .filters{padding:10px 14px}
  .filters::-webkit-scrollbar{display:none}
  /* 16px, not just 44px tall: iOS Safari zooms the whole page in when a focused
     control is under 16px, and it does not zoom back out. CONVENTIONS hard rule
     2 — every .field control already complies; the filter chips did not. */
  .filters .seg a,.chip-select{min-height:44px}
  .chip-select{font-size:16px}
  .filter-acts{margin-left:0}
}

/* ── 4. bulk selection ──────────────────────────────────────────────────── */

.bulk{display:flex; align-items:center; gap:12px; padding:8px 16px; border-bottom:1px solid var(--hair-2); background:var(--surface-2); font-size:12px}
.bulk-n{font-weight:600; color:var(--ink)}
.bulk-note{color:var(--ink-3)}
.bulk-acts{margin-left:auto; display:flex; align-items:center; gap:8px}
table.t th.pick,table.t td.pick{width:1%; white-space:nowrap}
/* A date, a reference or a short enum never wraps: `1fr` tracks floor at
   min-content, so a wrapping 90px date column keeps its width and costs the row
   two extra lines while the narrative column beside it sits on hundreds of
   pixels of slack. Rows measured 53-103px against the theme's 40px contract on
   /app/audit, /app/compliance, /app/schedules, /app/collections and the deal
   ledger. `.mono` is a reference by definition (EN-572CF9 was breaking onto
   three lines), so it is covered without tagging every column. */
table.t th.nowrap,table.t td.nowrap,table.t td.mono,table.t th.mono{white-space:nowrap}
/* On a phone the row is a card and each cell owns a line, so wrapping is right
   again — a nowrap reference in a 130px card cell would overflow instead. */
@media (max-width:600px){
  table.t.cards td.nowrap,table.t.cards th.nowrap,table.t.cards td.mono{white-space:normal}
}
.pick-l{display:inline-flex; align-items:center; justify-content:center; min-width:28px; min-height:28px; cursor:pointer}
table.t input[type="checkbox"]{accent-color:var(--sys-blue)}

@media (max-width:920px){
  table.t input[type="checkbox"]{width:22px; height:22px}
  .pick-l{min-width:44px; min-height:44px}
}
@media (max-width:600px){
  table.t.cards td.pick{grid-row:1; grid-column:1}
  /* The bar is rendered ABOVE the list (one per page, at document offset 621 in
     a 2,768px page on /app/returns), and `position:sticky` with `bottom:` only
     holds an element that would otherwise scroll off the BOTTOM — once you
     scroll past it going down, it is gone. Measured: after ticking a row at
     viewport y 400 the bar sat at top -922, off screen, while its live count
     silently changed to "1 selected". So while a selection exists it comes out
     of flow entirely; with nothing ticked it stays in flow, where it belongs. */
  .bulk{flex-wrap:wrap; border-top:1px solid var(--hair)}
  .bulk.has-sel{
    position:fixed; left:0; right:0; bottom:calc(58px + env(safe-area-inset-bottom)); z-index:25;
    box-shadow:0 -2px 10px rgba(0,0,0,.10);
  }
  /* so the last row is never underneath it */
  .page:has(.bulk.has-sel){padding-bottom:calc(150px + env(safe-area-inset-bottom))}
  .bulk-acts{margin-left:0; width:100%; flex-wrap:wrap}
  .bulk-acts .btn{flex:1 1 140px; justify-content:center}
  .bulk .btn{min-height:44px}
}

/* The phone's sort control. It exists only where the header row does not:
   `table.t.cards thead{display:none}` takes every sort link with it. */
.sort-compact{display:none}
@media (max-width:600px){
  .sort-compact{display:flex; padding:8px 14px; border-bottom:1px solid var(--hair-2); background:var(--surface)}
  .sort-compact > details{width:100%}
  /* `> details > summary`, not `summary`: the row-menu rule (§5) is written at
     the same specificity and comes later in the file, so it was winning — this
     disclosure rendered as a 44x44 kebab button with "Sorted by Funded, last to
     first" overflowing it (measured: 44px wide, scrollHeight 106 against a 42px
     box, the text painting over the first row of every list at 390). */
  .sort-compact > details > summary{
    display:flex; align-items:center; gap:6px; min-height:44px; cursor:pointer;
    width:auto; height:auto; flex:1 1 auto; min-width:0; justify-content:flex-start;
    font-size:13px; color:var(--ink-2); list-style:none;
  }
  .sort-compact summary::-webkit-details-marker{display:none}
  .sort-compact summary b{color:var(--ink); font-weight:600}
  .sort-compact .menu-panel{position:absolute; left:14px; right:14px; width:auto; z-index:30}
  .sort-compact .menu-i{min-height:44px; display:flex; align-items:center}
}

/* ── 5. row menus ───────────────────────────────────────────────────────── */

table.t td.act{width:1%; white-space:nowrap; text-align:right}
.menu{position:relative; display:inline-block}
.menu > summary{
  list-style:none; display:inline-grid; place-items:center;
  width:32px; height:32px; border-radius:var(--r-c); border:1px solid transparent;
  color:var(--ink-3); cursor:pointer;
}
.menu > summary::-webkit-details-marker{display:none}
.menu > summary:hover{background:var(--surface-2); color:var(--ink); border-color:var(--hair)}
.menu > summary:focus-visible{outline:none; box-shadow:var(--focus-ring)}
.menu > summary svg{width:14px; height:14px}
.menu[open] > summary{background:var(--surface-2); color:var(--ink); border-color:var(--hair)}
/* a closed menu is really closed: no stray hit target under the row */
.menu:not([open]) .menu-pop{display:none}
/* .t-wrap scrolls, which would clip an open row menu: let it out while one is open */
.t-wrap:has(.menu[open]){overflow:visible}
.menu-pop{
  /* Above the phone tab bar (fixed, z-index 30): a row menu opened near the
     bottom of a 390 viewport used to open UNDER it, and "Open customer"
     hit-tested as the Portfolio tab — a dead control on /app/authorizations. */
  position:absolute; right:0; top:calc(100% + 4px); z-index:40; min-width:196px;
  background:var(--surface); border:1px solid var(--hair); border-radius:var(--r-c);
  box-shadow:var(--shadow-2); padding:4px; display:flex; flex-direction:column;
}
.menu-i{
  display:block; width:100%; text-align:left; background:transparent; border:0;
  padding:8px 10px; border-radius:6px; font:inherit; font-size:12.5px; color:var(--ink);
  text-decoration:none; cursor:pointer; min-height:32px;
}
.menu-i:hover{background:var(--surface-2)}
.menu-i:focus-visible{outline:none; box-shadow:var(--focus-ring)}
.menu-i.danger{color:var(--bad)}
.menu-i.off{color:var(--ink-3); cursor:not-allowed}
.menu-f{display:block}
.menu-f + .menu-f .menu-i.sep,.menu-i.sep{border-top:1px solid var(--hair-2); border-radius:0; margin-top:4px; padding-top:10px}
.menu-f:first-child .menu-i.sep,.menu-pop > .menu-i.sep:first-child{border-top:0; margin-top:0; padding-top:8px}

@media (max-width:920px){
  .menu > summary{width:44px; height:44px}
  .menu-i{min-height:44px; display:flex; align-items:center; font-size:14px}
}

/* ── 6. cell conventions ────────────────────────────────────────────────── */

/* A long name is truncated by CSS, never by the query: the full value is in the title. */
table.t .trunc{display:inline-block; max-width:26ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; vertical-align:bottom}
table.t td.mut .trunc{max-width:20ch}
.idc{display:inline-flex; align-items:center; gap:4px}
.btn.icon{padding:4px; min-height:32px; min-width:32px; background:transparent; border-color:transparent; color:var(--ink-3)}
.btn.icon:hover{background:var(--surface-2); border-color:var(--hair); color:var(--ink)}
.btn.icon svg{width:13px; height:13px}
.quiet-link{display:inline-flex; align-items:center; gap:3px; color:var(--accent-ink, var(--sys-blue-2)); text-decoration:none}
.quiet-link:hover{text-decoration:underline; text-underline-offset:2px}
.quiet-link svg{width:11px; height:11px}
.fd .meta .kind{color:var(--ink-3)}
/* the gauge label is a link to the returns behind it: keep the text size, grow the hit area */
.gauge-h .l a{color:inherit; text-decoration:none; display:inline-block; padding:8px 0; margin:-8px 0}
.gauge-h .l a:hover{text-decoration:underline; text-underline-offset:2px}
@media (max-width:920px){
  .gauge-h .l a{padding:16px 0; margin:-16px 0}
}
.card-foot.warn-foot{color:var(--hold); align-items:flex-start; gap:8px}
.card-foot.warn-foot svg{width:14px; height:14px; flex:none; margin-top:2px}

@media (max-width:920px){
  .btn.icon{min-height:44px; min-width:44px}
}

/* ── 7. fields ──────────────────────────────────────────────────────────── */

.field .hint[hidden]{display:none}
.field .err:empty{display:none}
.field .err{font-size:11.5px; color:var(--bad)}
.field input:disabled,.field select:disabled,.field textarea:disabled{
  background:var(--sys-gray6, #F2F2F7); color:var(--label-3, var(--ink-3));
  border-color:var(--sys-gray5, var(--hair)); cursor:not-allowed;
}
.field input:read-only:not([type="date"]){background:var(--surface-2); color:var(--ink-2)}
.field select{
  appearance:none; -webkit-appearance:none; padding-right:30px;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><path d='M3 4.5 6 7.5 9 4.5' fill='none' stroke='%236E6E73' stroke-width='1.6' stroke-linecap='round'/></svg>");
  background-repeat:no-repeat; background-position:right 10px center; background-size:12px;
}
.pct-in{position:relative}
.pct-in::after{content:"%"; position:absolute; right:12px; top:50%; transform:translateY(-50%); color:var(--ink-3); font-size:13px; pointer-events:none}
.pct-in input{padding-right:26px}
.check-row{display:flex; align-items:flex-start; gap:9px; font-size:13px; cursor:pointer}
.check-row input{width:16px; height:16px; margin-top:2px; accent-color:var(--sys-blue); flex:none}
.check-row .hint{display:block; color:var(--ink-3); font-size:11.5px}

/* label-and-control rows: the settings shape */
.form-rows{display:grid; grid-template-columns:minmax(160px,220px) minmax(0,1fr); gap:14px 20px; align-items:center}
.form-rows > span{font-size:13px; color:var(--label-2, var(--ink-2))}
@media (max-width:600px){
  .form-rows{grid-template-columns:1fr; gap:6px}
}

/* a real switch: a checkbox with role=switch, not a div */
.switch-row{display:flex; align-items:center; gap:14px; padding:8px 0; border-bottom:1px solid var(--hair-2)}
.switch-row:last-child{border-bottom:0}
.switch-l{display:flex; flex-direction:column; gap:2px; cursor:pointer; min-width:0}
.switch-t{font-size:13px; font-weight:500; color:var(--ink)}
.switch-row .hint{font-size:11.5px; color:var(--ink-3)}
input.switch{
  appearance:none; -webkit-appearance:none; margin:0 0 0 auto; flex:none;
  width:40px; height:24px; border-radius:20px; background:var(--sys-gray4, #D1D1D6);
  position:relative; cursor:pointer; transition:background var(--t-fast,.15s) var(--ease,ease);
}
input.switch::after{
  content:""; position:absolute; top:2px; left:2px; width:20px; height:20px; border-radius:50%;
  background:#fff; box-shadow:0 1px 2px rgba(0,0,0,.2); transition:transform var(--t-fast,.15s) var(--ease,ease);
}
input.switch:checked{background:var(--sys-blue)}
input.switch:checked::after{transform:translateX(16px)}
input.switch:focus-visible{outline:none; box-shadow:var(--focus-ring)}
input.switch:disabled{opacity:.5; cursor:not-allowed}

/* a segmented control inside a form is radios, so it works without script */
.seg-field{border:0; margin:0; padding:0; display:flex; flex-direction:column; gap:5px}
.seg-field legend{font-size:12px; font-weight:500; color:var(--ink); padding:0}
.seg-o{position:relative; display:inline-flex}
.seg-o input{position:absolute; opacity:0; width:100%; height:100%; margin:0; cursor:pointer}
.seg-o span{
  display:inline-flex; align-items:center; justify-content:center; min-height:28px; padding:0 12px;
  border-radius:6px; font-size:12px; color:var(--ink-2); cursor:pointer;
}
.seg-o input:checked + span{background:#fff; color:var(--ink); font-weight:500; box-shadow:var(--shadow-1)}
.seg-o input:focus-visible + span{box-shadow:var(--focus-ring)}

/* an in-page index of a long form */
.sec-index{display:flex; flex-direction:column; gap:2px}
/* The sticky element has to be the GRID ITEM, not the nav inside it.
   `.sec-index-w` is the child of `.settings-page` (align-items:start), so it is
   only as tall as its content — 448px measured, inside a 6,257px grid — and a
   sticky element can never travel past the bottom of its own containing block.
   The index therefore scrolled away after 448px and never came back: measured
   at scrollY 1290 its top was -1153, with 5,000px of page still to go. */
.sec-index-w{align-self:start; position:sticky; top:76px}
.sec-index a{
  display:block; padding:6px 10px; border-radius:6px; font-size:12.5px;
  color:var(--ink-2); text-decoration:none;
}
.sec-index a:hover{background:var(--surface-2); color:var(--ink)}
/* The current section, measured: --navy on --navy-tint is systemBlue on a 10%
   systemBlue wash = 3.18:1 at 12.5px, under 4.5:1. --info-ink is the ink the
   theme defines FOR that tint (6.3:1 light, 6.2:1 dark) and .st.info already
   uses it; same one blue, one step darker so the text is readable. */
.sec-index a[aria-current]{background:var(--navy-tint); color:var(--info-ink, var(--navy)); font-weight:500}
@media (max-width:920px){
  /* Sticky, not static: /app/settings is 15,379px tall on a phone and this strip
     is its only way between the fourteen sections. Static, it left the viewport
     after ~180px of scrolling and never came back. The rail is sticky at the top
     of the same width (48px), so this sits under it. */
  .sec-index{
    position:sticky; top:48px; z-index:10; flex-direction:row; gap:6px;
    overflow-x:auto; padding:6px 0 8px; margin-bottom:2px; scrollbar-width:none;
    background:var(--ground); box-shadow:0 1px 0 var(--hair-2);
  }
  .sec-index::-webkit-scrollbar{display:none}
  /* the same right-edge fade a scrolling table gets, so the clipped chips read
     as clipped rather than as the end of the list. `position:relative` here
     replaces the desktop sticky: the strip itself is what sticks at this width. */
  .sec-index-w{position:relative; top:auto}
  .sec-index-w::after{
    content:""; position:absolute; top:0; right:0; bottom:8px; width:24px; pointer-events:none;
    background:linear-gradient(to right, rgba(0,0,0,0), var(--ground));
  }
  .sec-index a{white-space:nowrap; border:1px solid var(--hair); border-radius:20px; min-height:44px; display:flex; align-items:center}
}

/* ── 8. sheets ──────────────────────────────────────────────────────────── */

/* The head and the actions stay put; only the body scrolls. */
dialog.sheet{display:none; flex-direction:column; max-height:92vh; overflow:hidden}
dialog.sheet[open]{display:flex}
dialog.sheet.wide{max-width:min(760px,calc(100vw - 32px))}
dialog.sheet .card-head{flex:none; padding-right:56px}
/* the Close button is last in the DOM (so focus lands in the form) and first in the corner */
dialog.sheet .x{position:absolute; top:9px; right:12px; margin:0}
dialog.sheet .sheet-lede{flex:none; margin:0; padding:0 16px; font-size:12.5px; color:var(--ink-3); max-width:60ch}
dialog.sheet .card-body{flex:1 1 auto; overflow:auto; min-height:0}
dialog.sheet .sheet-foot{flex:none; padding:10px 16px; border-top:1px solid var(--hair-2); background:var(--surface); display:flex; justify-content:flex-end; gap:8px}
dialog.sheet .sheet-foot .form-acts{padding-top:0}
@media (max-width:920px){
  dialog.sheet .x{width:44px; height:44px}
  dialog.sheet .x svg{width:13px; height:13px}
  dialog.sheet .form-acts .btn,dialog.sheet .sheet-foot .btn{min-height:44px}
}

/* the shared confirmation sheet: facts, then the action */
dialog.sheet.confirm .facts{grid-template-columns:1fr auto; gap:0}
dialog.sheet.confirm dl{margin:0; display:grid; grid-template-columns:1fr auto; gap:6px 16px; font-size:12.5px}
dialog.sheet.confirm dt{color:var(--ink-3)}
dialog.sheet.confirm dd{margin:0; text-align:right; font-weight:500; font-variant-numeric:tabular-nums}

/* ── 9. charts ──────────────────────────────────────────────────────────── */

.chart svg{display:block; width:100%; height:auto}
.chart .grid line{stroke:var(--hair-2); stroke-width:1}
/* THEME-9: an axis label is read like any other label, so it sits on the 11px
   type floor too — the four documented exceptions are monograms, the keycap,
   count pips and the iOS tab bar. */
.chart .grid text,.chart .xl{fill:var(--ink-3); font-size:11px; font-family:var(--ui)}
.chart .pt{pointer-events:none}
.chart-empty{padding:18px 16px; color:var(--ink-3); font-size:12.5px}
.legend{display:flex; flex-wrap:wrap; gap:12px; margin:8px 0 0; padding:0; list-style:none; font-size:11.5px; color:var(--ink-3)}
.legend li{display:inline-flex; align-items:center; gap:6px}
.legend i{display:inline-block; width:8px; height:8px; border-radius:2px; flex:none}
.legend b{color:var(--ink); font-weight:600; font-variant-numeric:tabular-nums}

.bars{display:flex; flex-direction:column}
.bar{display:grid; grid-template-columns:minmax(88px,1.1fr) minmax(0,2fr) auto; align-items:center; gap:12px; padding:9px 16px; border-bottom:1px solid var(--hair-2); color:inherit; text-decoration:none}
.bar:last-child{border-bottom:0}
a.bar:hover{background:var(--surface-2)}
.bar-l{font-size:12.5px; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.bar-n{display:block; font-size:11px; color:var(--ink-3)}
.bar-t{height:7px; border-radius:20px; background:var(--hair-2); overflow:hidden}
.bar-t i{display:block; height:100%; border-radius:20px}
.bar-v{font-size:12.5px; font-variant-numeric:tabular-nums; text-align:right}
@media (max-width:600px){
  .bar{grid-template-columns:1fr auto; gap:4px 12px; padding:10px 14px}
  .bar-t{grid-column:1/-1}
}

.donut{display:flex; align-items:center; gap:16px; padding:14px 16px; flex-wrap:wrap}
.donut svg{width:104px; height:104px; flex:none}
.donut .bg{fill:none; stroke:var(--hair-2); stroke-width:9}
.donut-t .pc{font:600 26px/1 var(--ui); margin:0; letter-spacing:-.02em}
.donut-t .lbl{font-size:11.5px; color:var(--ink-3); margin:4px 0 0}

/* ── 10. timeline, breadcrumbs, diff ────────────────────────────────────── */

.crumbs ol{list-style:none; display:flex; flex-wrap:wrap; gap:6px; margin:0; padding:0; font-size:12px; color:var(--ink-3)}
.crumbs li + li::before{content:"/"; padding-right:6px; color:var(--ink-3)}
.crumbs a{color:var(--ink-2); text-decoration:none}
.crumbs a:hover{text-decoration:underline; text-underline-offset:2px}

.tl{list-style:none; margin:0; padding:6px 16px 12px}
.tl-i{position:relative; padding:0 0 14px 20px}
.tl-i::before{content:""; position:absolute; left:4px; top:14px; bottom:-2px; width:1px; background:var(--hair)}
.tl-i:last-child{padding-bottom:2px}
.tl-i:last-child::before{display:none}
.tl-d{position:absolute; left:0; top:5px; width:9px; height:9px; border-radius:50%; background:var(--sys-gray4, #D1D1D6)}
.tl-i.ok .tl-d{background:var(--ok)} .tl-i.bad .tl-d{background:var(--bad)} .tl-i.warn .tl-d{background:var(--hold)} .tl-i.info .tl-d{background:var(--navy)}
.tl-t{margin:0; font-size:12.5px; font-weight:500}
.tl-b{margin:2px 0 0; font-size:12.5px; color:var(--ink-2)}
.tl-m{margin:3px 0 0; font-size:11px; color:var(--ink-3)}

table.t.diff td,table.t.diff th{font-size:12px}
table.t.diff .mono,table.t.diff td{font-variant-numeric:tabular-nums}

/* ── 11. polish pass (wave C) ────────────────────────────────────────────────
   Every rule here closes a defect scripts/measure.js reported by number; the
   comment says which, so none of them can be "tidied away" by eye later. */

/* An alert's meta line used to run under the Acknowledge form on the right, and
   the "Open" link at the end of a long line hit-tested as form.ack — a dead
   control on /app/console at every width. The line wraps instead. */
.fd .meta{flex-wrap:wrap; row-gap:2px}

/* A link inside a tinted banner takes the banner's own ink (already contrast
   checked for that tint) and is underlined, so it is not a colour-only signal
   sitting at 4.39:1 on an orange ground. */
.banner a{color:inherit; text-decoration:underline; text-underline-offset:2px}
.banner a:hover{text-decoration-thickness:2px}

/* A code chip explains itself: codeChip() puts "R01 · Insufficient funds · Two
   attempts permitted…" in title for a pointer and in a .visually-hidden span
   for a screen reader. Neither needs a rule here — this comment is the record
   that a CSS bubble was tried and removed: it needed a tab stop on a role-less
   span, which measured 33x20 on a phone. */

/* A relative time keeps its absolute value one hover or one screen-reader stop
   away, so "5 min ago" is never the only stamp an operator can read. */
time[title]{text-decoration:none}

/* ── 12. phone hit targets (MOBILE-2, measured) ──────────────────────────────
   Apple HIG asks 44pt of every control a thumb uses. These four were measured
   under it by scripts/measure.js at 390: a 13px checkbox, a 24px sort link, a
   24px switch and the sheet's search input. The VISIBLE size does not change —
   only the box the finger lands on. */
@media (max-width:600px){
  /* A checkbox: 22px of box you can see, 44px of box you can hit. Native
     appearance cannot be grown without growing the tick, so the box is drawn. */
  table.t input[type="checkbox"],.pick-l input[type="checkbox"],.check-row input[type="checkbox"]{
    appearance:none; -webkit-appearance:none;
    width:44px; height:44px; margin:0; padding:0; flex:none; border:0;
    background:transparent; cursor:pointer; display:grid; place-items:center;
  }
  table.t input[type="checkbox"]::before,.pick-l input[type="checkbox"]::before,.check-row input[type="checkbox"]::before{
    content:""; width:22px; height:22px; border-radius:6px;
    border:1.5px solid var(--line); background:var(--surface);
  }
  table.t input[type="checkbox"]:checked::before,.pick-l input[type="checkbox"]:checked::before,.check-row input[type="checkbox"]:checked::before{
    background:var(--sys-blue) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M3.5 8.5 6.5 11.5 12.5 5' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/></svg>") center/16px no-repeat;
    border-color:var(--sys-blue);
  }
  table.t input[type="checkbox"]:indeterminate::before,.pick-l input[type="checkbox"]:indeterminate::before,.check-row input[type="checkbox"]:indeterminate::before{
    background:var(--sys-blue) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M4 8h8' fill='none' stroke='white' stroke-width='2' stroke-linecap='round'/></svg>") center/16px no-repeat;
    border-color:var(--sys-blue);
  }
  table.t input[type="checkbox"]:focus-visible::before,.pick-l input[type="checkbox"]:focus-visible::before,.check-row input[type="checkbox"]:focus-visible::before{box-shadow:var(--focus-ring)}
  .pick-l{min-width:44px; min-height:44px}
  /* the box grew to 44px, so the row centres on it instead of on its first line */
  .check-row{align-items:center; gap:4px}
  .check-row input[type="checkbox"]{margin:0 0 0 -11px}

  /* A sortable header on a table that stays tabular on a phone. */
  table.t th a.sort{min-height:44px}

  /* The switch keeps its 40x24 track — the track becomes a ::before so the
     input's own box can be the 44pt one a thumb lands on. */
  input.switch{width:64px; height:44px; background:transparent; border-radius:0}
  input.switch::before{
    content:""; position:absolute; top:10px; left:12px; width:40px; height:24px; border-radius:20px;
    background:var(--sys-gray4, #D1D1D6); transition:background var(--t-fast,.15s) var(--ease,ease);
  }
  input.switch::after{top:12px; left:14px}
  input.switch:checked{background:transparent}
  input.switch:checked::before{background:var(--sys-blue)}
  input.switch:focus-visible{box-shadow:none}
  input.switch:focus-visible::before{box-shadow:var(--focus-ring)}

  /* The search sheet's own input. (The kanban column link is fixed in
     origination.css, whose own rule is loaded after this file.) */
  dialog.sheet .search input{min-height:44px}

  /* The actions cell keeps the card's top-right corner only when it IS a row
     menu — one 44px kebab fits there. Anything wider (two buttons, an inline
     form, five stipulation actions) takes a line of its own, or it hangs off
     the right edge: measured at 390, Download/Remove ran 101px over. */
  /* …but a cell the column model drops on a phone stays dropped: this rule is
     more specific than the [data-p] one above and would un-hide it. */
  table.t.cards td.act:has(*):not(:has(details.menu)):not([data-p="2"]):not([data-p="3"]){
    grid-column:1 / -1; grid-row:auto; justify-self:stretch; margin-top:6px;
    display:flex; flex-wrap:wrap; gap:6px;
  }
}

/* ── phone: the record link in a row heading is a real target ──────────────
   In card mode a row's own <a> stayed text-height (15px), so the one control
   that opens the record was the smallest thing on the screen. The row is
   clickable through data-href, but a link a thumb cannot land on is not a
   target. Give it the row's width and the 44pt height, without moving it. */
@media (max-width:600px){
  table.t th.who > a:first-child,
  table.t td.who > a:first-child{
    display:flex; align-items:center; min-height:44px;
  }
}

/* ── 13. skeleton loading ────────────────────────────────────────────────────
   Placeholder shapes for a region whose content is genuinely on its way: the
   reload that follows an action, and a slow in-page fetch. src/web/components.ts
   (skeletonRows, skeletonBlock) emits the markup; public/js/app.js decides when
   showing it is honest — never under ~150ms, and never in place of an empty
   state, which is a fact rather than a wait.

   Two properties these rules have to keep:

   · NOTHING MOVES when the content lands. The overlay the client paints is
     absolutely positioned inside the box it covers, and every clone is sized
     from the row it stands in for, so the page's own layout is never touched —
     measured before and after: identical bounding boxes.
   · ONE RULE FOR BOTH THEMES. The bar is the hairline grey, and the sheen is
     the surface itself sweeping across it — lighter than the bar in light mode,
     darker in dark mode, correct in both without a component rule branching on
     the theme. */

/* the box the client covers becomes the overlay's containing block */
[data-sk]{position:relative}

.sk-overlay{
  position:absolute; inset:0; z-index:7; overflow:hidden;
  background:var(--surface); border-radius:inherit;
}
/* main.page sits on the window's own ground, not on a card */
.sk-overlay.on-ground{background:var(--ground)}

.sk-block{display:flex; flex-direction:column; gap:12px; padding:14px 16px}

.sk-line,.sk-bar{
  display:block; position:relative; overflow:hidden;
  height:12px; border-radius:var(--r-s);
  background:var(--hair);
}
.sk-line{width:100%}
.sk-line.short{width:46%}
/* The stand-in for a list is deliberately NOT `table.t`: it must not inherit a
   sticky header, a selected-row tint, the phone card layout or a hover state —
   it stands in for rows, it is not a table of data, and it never scrolls, so it
   needs no .t-wrap around it either. It copies the two measurements that make a
   row a row (app.css:259) and the client sizes each clone from the row it is
   covering, which is what keeps the two aligned. */
.sk-table{width:100%; border-collapse:collapse}
.sk-table td{padding:0 12px; height:40px; border-bottom:1px solid var(--hair-2)}
.sk-table tr:last-child td{border-bottom:0}

/* inside a row: a bar sits in the middle of the cell it stands in for, and the
   last column (actions, money) gets a right-aligned one */
.sk-bar{width:min(72%, 130px)}
.sk-table .sk-c{vertical-align:middle}
.sk-table .sk-c:last-child:not(.wide) .sk-bar{margin-left:auto}
/* below 600px a row is a card, so its stand-in is one cell of two stacked
   lines — the name and the value the card actually shows */
.sk-table .sk-c.wide{padding:0 14px}
.sk-table .sk-c.wide .sk-bar{width:62%; max-width:none; margin:0}
.sk-table .sk-c.wide .sk-bar + .sk-bar{width:38%; margin-top:10px}

/* the sheen: the surface travelling across the bar, once every 1.5s */
.sk-line::after,.sk-bar::after{
  content:""; position:absolute; inset:0;
  background:linear-gradient(90deg, transparent 0%, var(--surface) 55%, transparent 100%);
  opacity:.85; transform:translateX(-100%);
  animation:sk-sheen 1.5s linear infinite;
}
@keyframes sk-sheen{to{transform:translateX(100%)}}

/* Reduce Motion: the placeholder stays, the movement goes. (app-ext.css already
   caps every animation at .01ms, which would leave the sheen parked mid-bar —
   this removes it outright and keeps the flat tint.) */
@media (prefers-reduced-motion:reduce){
  .sk-line::after,.sk-bar::after{display:none; animation:none}
  .sk-line,.sk-bar{background:var(--hair)}
}

/* Paper never waits for anything. */
@media print{
  .sk-overlay{display:none !important}
}
