/* WarehouseNow AP — shared app chrome for the flow mockups.
 *
 * Extracted from flow 4 while there was still exactly one copy of it. Every
 * flow links design-system.css (the tokens) then this (the app built from
 * them), and keeps only its own specifics inline.
 *
 * mockups/check.sh scans THIS FILE as well as each page's <style>. It has to:
 * moving CSS out of the <style> blocks is exactly how a guard goes blind while
 * still reporting OK.
 */

  /* FORM CONTROLS DO NOT INHERIT font-family. The UA sheet gives a <button>
     13.333px Arial, so every .btn, .chip, .lnk and rail glyph was rendering in
     Arial on a page that is otherwise Inter -- close enough in a screenshot to
     miss, wrong on every screen.
     The inputs all carry `font: inherit` because I wrote them one at a time
     and remembered; the buttons never got it. Stated once here rather than
     per-rule, so a new control cannot be born wrong.
     Found by controls.mjs, which measures the RENDERED page. Nothing static
     could see it: each of those rules is individually correct. */
  button, input, select, textarea { font-family: inherit; }

  /* ── shell ───────────────────────────────────────────────────────────── */
  body { height: 100vh; overflow: hidden; }
  .app { display: flex; height: 100vh; }

  /* ── the top nav ───────────────────────────────────────────────────────
     Eight views across the top, Stripe's shape. It replaced a 184px labelled
     sidebar, which had replaced a 56px icon rail that was silently evicting
     views -- flows 12 and 15 both dropped `Payment receipts` to fit something
     else in. board-00-sidebar.html keeps the middle version, drawn and
     measured, as the record of what this was chosen over.

     WHAT IT BUYS: 184px of board on every screen, and these boards are wide
     tables. The slide-overs get their width back. And the current item is a
     filled lozenge, which reads from across the room where a 2px edge bar
     does not.

     WHAT IT COSTS: the group. There is no horizontal place to put a label
     over four items, so `Open` stops existing and the four lists become a run
     between two hairlines -- a rule says where a group stops, where a label
     said which four belong together. Counts become suffixes rather than a
     scannable column, and 9 going to 10 nudges everything to its right.

     Measured: the row is 775px of the 1280 available, so there is room for a
     ninth item and a tenth. Stripe needs a `More` dropdown at eight because
     their chrome also carries search and an account menu; ours does not. */
  :root { --top-h: 48px; }

  .app { display: flex; flex-direction: column; height: 100vh; }

  .top { display: flex; align-items: center; gap: 4px; flex: none; height: var(--top-h);
         padding: 0 16px; background: var(--color-bg-elevated);
         border-bottom: 1px solid var(--color-border-subtle); }
  .top .mark { display: flex; align-items: center; gap: 8px; margin-right: 18px;
               font-size: 13px; font-weight: 600; color: var(--color-text-primary); }
  .top .mark .sq { width: 22px; height: 22px; border-radius: var(--radius-md);
                   background: var(--color-brand); color: var(--color-primary-foreground);
                   display: grid; place-items: center; font-size: 11px; }

  /* Brand, not accent. The current item is lit all day and a primary button
     is the one thing you are meant to press; they cannot be the same blue.
     White on brand measures 7.02:1, better than the 5.17:1 it replaces, and
     the same in both themes. */
  .top button { display: inline-flex; align-items: center; gap: 7px; height: 30px;
                padding: 0 12px; border: 0; border-radius: var(--radius-full);
                background: none; color: var(--color-text-secondary);
                font: inherit; font-size: 13px; white-space: nowrap; cursor: pointer;
                transition: background-color 75ms var(--ease-out-expo); }
  .top button:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
  .top button[aria-current] { background: var(--color-brand);
                              color: var(--color-primary-foreground); }
  .top button[aria-current]:hover { background: var(--color-brand-hover); }

  .top .n { font-size: 11px; color: var(--color-text-tertiary);
            font-variant-numeric: tabular-nums; }
  .top button[aria-current] .n { color: var(--color-primary-foreground); opacity: .8; }

  /* All that survives of the group. */
  .top .sep { width: 1px; height: 18px; margin: 0 8px; flex: none;
              background: var(--color-border-default); }

  /* GLOBAL, not per-board. It was floated right on the Payables title's
     baseline, nowhere near the table it appeared to filter -- and what it
     searches ("vendor, order, invoice") crosses every board, so it was never
     a Payables control in the first place. */
  .top .search { margin-left: auto; height: 28px; width: 260px; border-radius: var(--radius-md);
                 border: 1px solid var(--color-border-default);
                 background: var(--color-bg-base); color: var(--color-text-primary);
                 font: inherit; font-size: 12px; padding: 0 10px; }
  .top .search::placeholder { color: var(--color-text-tertiary); }
  .top .search:hover { border-color: var(--color-text-tertiary); }
  .top .search:focus { outline: 2px solid var(--color-accent); outline-offset: -1px; }

  /* Every command in the domain carries an actor, and until now no screen
     said whose name it was. */
  .top .right { margin-left: 14px; display: flex; align-items: center; gap: 10px;
                font-size: 12px; color: var(--color-text-tertiary); }

  main { flex: 1; min-width: 0; background: var(--color-bg-base); overflow: hidden; }

  /* ── board header + chips ───────────────────────────────────────────────
     32px above the title, not 16. The nav is chrome at the top and so is a
     page title; stacked 16px apart they read as one crowded block rather than
     as two things. */
  .head { padding: 32px 20px 0; }
  .head h1 { font-size: 20px; font-weight: 600; letter-spacing: -0.01em; margin: 0;
             color: var(--color-text-primary); display: inline; }
  .head .count { font-size: 12px; color: var(--color-text-tertiary); margin-left: 10px; }

  /* Visually hidden, still read aloud. clip-path over display:none because
     the second removes it from the accessibility tree along with the screen. */
  .sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
        overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }

  /* ── board metrics ─────────────────────────────────────────────────────
     Aggregates in place of a page title. The nav already names the board in a
     lit pill, so an <h1> repeating it spent the most valuable strip on the
     screen saying something the reader just clicked.

     FOUR AT MOST, and they are CUTS rather than a partition -- Open is
     everything, the other three are overlapping slices of it, and rows with
     no due date appear in none of them. A header that looked like it added up
     but did not would be worse than one that plainly does not.

     Colour follows the tags: overdue is asking, held is a decision nobody is
     acting on. Everything else stays monochrome, so the two that are coloured
     are the two worth seeing from the door. */
  .metrics { display: flex; gap: 34px; }
  .metrics .m .v { font-size: 20px; font-weight: 600; letter-spacing: -0.01em;
                   font-variant-numeric: tabular-nums; color: var(--color-text-primary); }
  .metrics .m .k { margin-top: 2px; font-size: 11px; font-weight: 500;
                   text-transform: uppercase; letter-spacing: .06em;
                   color: var(--color-text-tertiary); }
  .metrics .m .k .n { color: var(--color-text-secondary);
                      font-variant-numeric: tabular-nums; }
  /* NOT `.m.over`. `.over` is the slide-over -- position:fixed, full height --
     so a bare modifier of that name turns a metric into an invisible sheet
     across the screen. Fourth time that class has bitten; the guard catches it
     every time now. */
  .metrics .m.overdue .v, .metrics .m.overdue .k { color: var(--color-status-progress); }
  .metrics .m.onhold .v, .metrics .m.onhold .k { color: var(--color-status-blocked); }

  .chips { display: flex; gap: 6px; margin: 14px 0 0; padding-bottom: 14px;
           border-bottom: 1px solid var(--color-border-subtle); }
  .chip { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px;
          border-radius: var(--radius-md); border: 1px solid var(--color-border-default);
          background: var(--color-bg-elevated); color: var(--color-text-secondary);
          font-size: 12px; font-weight: 500; cursor: pointer;
          transition: background-color 80ms var(--ease-out-expo); }
  .chip:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
            border-color: var(--color-border-strong); }
  .chip .n { font-variant-numeric: tabular-nums; color: var(--color-text-tertiary); }
    .chip.zero { opacity: .55; }

  /* ── table ─────────────────────────────────────────────────────────────
     THREE RULES, and they are rules rather than preferences.

     1. A TABLE DOES NOT STRETCH. width:100% put Vendor and Tags at opposite
        ends of a 1440px screen with acres between them, so reading one row
        meant crossing the whole display. Columns are sized, the table sits
        left, and the space to its right is space.

     2. NO CONDITIONAL SECOND ROW IN A CELL. The diff used to render as a line
        under the amount on the rows that had one, which changed the height of
        exactly the rows you are scanning past and broke the rhythm of the
        list. If a fact needs its own line it needs its own column; if it does
        not earn a column it belongs in the detail. controls.mjs asserts every
        row in a board table is the same height.

     3. A TAG IS A STATE, NEVER A PAYLOAD. No `held 61d`, no `waiting ·
        Michelle`, no `held · damage claim`. Tags are a closed vocabulary of
        conditions and a row may carry several; the moment one carries data it
        stops being comparable with the tag beside it. Ages, people and
        reasons live in columns or in the detail. */
  table { width: auto; border-collapse: collapse; }
  table.board { table-layout: fixed; }
  th { padding: 10px 12px; text-align: left; font-size: 11px; font-weight: 500;
       text-transform: uppercase; letter-spacing: .06em; color: var(--color-text-tertiary);
       white-space: nowrap; border-bottom: 1px solid var(--color-border-default); }
  th.r, td.r { text-align: right; }
  td { padding: 9px 12px; font-size: 13px; color: var(--color-text-primary);
       border-bottom: 1px solid var(--color-border-subtle); }
  tbody tr { cursor: pointer; transition: background-color 75ms var(--ease-out-expo); }
  tbody tr:hover { background: var(--color-bg-hover); }
  tbody tr[aria-selected="true"] { background: var(--color-bg-active); }
  .vendor { font-size: 13px; }
  /* Identity, not a link. text-accent is reserved for things you can click --
     the row itself is the target, so colouring the id blue promises a second
     destination that does not exist. */
  .order { font-size: 12px; color: var(--color-text-secondary); font-variant-numeric: tabular-nums; }
  .num { font-variant-numeric: tabular-nums; }
  .muted { color: var(--color-text-secondary); }
  .dim { color: var(--color-text-tertiary); }

  /* Kept for the DETAIL panels, which have room for it and where the number
     is the subject. It is no longer used on any board: two columns of numbers
     side by side already show that they disagree, and a third rendering of
     the same fact cost a broken row rhythm to say it again. */
  .diff { display: block; font-size: 11px; font-weight: 500; font-variant-numeric: tabular-nums;
          margin-top: 1px; color: var(--color-status-progress); }
  .diff.under { color: var(--color-text-tertiary); }

  /* A tag is coloured only when it is ASKING for something. "waiting" is
     parked and wants nothing from you; "doesn't match" wants a decision. All
     tags painted alike made the branch below read as one outcome with two
     causes -- the flatness only showed once three sat side by side, because
     a table row never displays more than one. */
  /* Several per row is the point of tags -- a payable can be held AND not
     match -- so the cell wraps and they sit in a row. */
  .tags { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
  .tag { display: inline-flex; align-items: center; gap: 6px; height: 20px; padding: 0 8px;
         border-radius: var(--radius-full); font-size: 11px; font-weight: 500;
         background: var(--color-bg-active); color: var(--color-text-secondary); }
  .tag .pip { height: 6px; width: 6px; border-radius: var(--radius-full);
              background: var(--color-text-tertiary); }
  .tag.asks { background: color-mix(in srgb, var(--color-status-progress) 10%, transparent);
              color: var(--color-status-progress); }
  .tag.asks .pip { background: var(--color-status-progress); }

  /* HELD. Loud on purpose: a held payable is the one thing here somebody can
     spend a morning on without noticing, and neutral grey was too quiet to
     stop them. status-blocked is the token built for this shape -- it is
     defined to be read as TEXT on a ~10% tint of ITSELF, which is exactly
     what a tag and a banner are, and it carries measured ratios on every
     surface in both themes.
     It is the one deliberate exception to "colour only when a tag is asking".
     Held asks nothing; it announces. And it buys back something the flow
     wanted anyway: waiting and held no longer have to be told apart by a face
     against no face, because one is red and one is not.
     NOT --color-alert-solid, which is the other red here. That one is for
     white text on a solid fill and is the opposite trade. */
  .tag.held { background: color-mix(in srgb, var(--color-status-blocked) 12%, transparent);
              color: var(--color-status-blocked); }
  .tag.held .pip { background: var(--color-status-blocked); }

  /* someone else is in it — the collision signal, on the ROW */
  .here { display: inline-flex; align-items: center; gap: 5px; font-size: 11px;
          color: var(--color-text-tertiary); }
  /* 18px, not 16 — an initial at 11px (the floor) will not fit a 16px circle.
     The type scale sets the container, not the other way round. */
  .here .av { height: 18px; width: 18px; border-radius: var(--radius-full);
              background: var(--color-bg-active); color: var(--color-text-secondary);
              font-size: 11px; line-height: 18px; text-align: center; }

  /* ── slide-over ──────────────────────────────────────────────────────── */
  /* Deliberately stops short of the nav. An inspector may dim the board
     behind it, but it must never take away the way OUT of the section you are
     in -- the primary nav is the one thing on screen that is never someone
     else's to block. On the rail that meant a left inset; on a top nav it is
     a top one, and the rule is the same rule. */
  .scrim { position: fixed; inset: var(--top-h) 0 0 0; background: rgba(0,0,0,.5); }
  /* Starts below the nav for the same reason the scrim does.
     ENOUGH BOARD LEFT SHOWING TO READ AS AN OVERLAY. Against the old rail the
     panel was 84% of the width and the 184px of rail beside it was visibly
     other chrome, so it read as a sheet over something. On a top nav there is
     no rail, and at 1280 the same 24px gutter left 100px of scrim -- 92% of
     the screen, which reads as the page having changed rather than as
     something laid over it. 180px is the floor at which the board behind is
     still legible as a board.
     The top-left corner is rounded for the same reason: a square corner butted
     flush against a flat bar looks like a layout fault, not a sheet. 12px is
     the modal radius. */
  .over { position: fixed; top: var(--top-h); right: 0; bottom: 0;
          width: min(1180px, calc(100vw - 180px));
          border-top-left-radius: var(--radius-xl);
          background: var(--color-bg-base); border-left: 1px solid var(--color-border-default);
          display: flex; flex-direction: column; box-shadow: var(--shadow-lg); }

  /* An UNMATCHED invoice is not a payable and must not be mistaken for one
     before it is read. The stripe is pre-attentive -- you know which kind of
     thing this is from the corner of your eye. Shared, because flow 2 opens
     it from email and flow 9 from an upload, and a second copy is how the two
     drift into different colours. */
  .over.unmatched { border-top-left-radius: var(--radius-xl); border-top: 2px solid var(--color-status-progress); }

  .over-head { display: flex; align-items: center; gap: 10px; padding: 12px 16px;
               border-top-left-radius: var(--radius-xl);
               border-bottom: 1px solid var(--color-border-subtle); }
  .over-head .id { font-size: 14px; font-weight: 600; color: var(--color-text-primary);
                   font-variant-numeric: tabular-nums; }
  .over-head .name { font-size: 13px; color: var(--color-text-secondary); }
  .pill { display: inline-flex; align-items: center; gap: 6px; height: 20px; padding: 0 8px;
          border-radius: var(--radius-full); font-size: 11px; font-weight: 500;
          background: var(--color-bg-active); color: var(--color-text-secondary); }
  .links { margin-left: auto; display: flex; gap: 6px; }
  .lnk { height: 26px; padding: 0 10px; display: inline-flex; align-items: center; gap: 6px;
         border-radius: var(--radius-md); border: 1px solid var(--color-border-default);
         background: transparent; color: var(--color-text-secondary); font-size: 12px;
         cursor: pointer; }
  .lnk:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }

  .over-body { flex: 1; display: grid; grid-template-columns: 1fr 420px; min-height: 0; }
  .doc { overflow: auto; border-right: 1px solid var(--color-border-subtle);
         background: var(--color-bg-app); }
  .doc .from { position: sticky; top: 0; z-index: 1; padding: 9px 16px;
               background: var(--color-bg-elevated);
               border-bottom: 1px solid var(--color-border-subtle);
               font-size: 12px; color: var(--color-text-tertiary); }
  .doc .sheet-wrap { padding: 20px 16px 28px; display: flex; justify-content: center; }
  /* The invoice pane renders a DOCUMENT, so it carries document colours rather
     than app tokens -- it is a picture of paper, not a themed surface. Letter
     proportions and a page edge, because a real one is a scanned or generated
     PDF and the mockup should not flatter it. */
  .paper { flex: none; background: #ffffff; color: #1a1a1f; width: 612px; min-height: 792px;
           padding: 46px 48px; box-shadow: 0 2px 14px rgba(0,0,0,.45);
           font-size: 11px; line-height: 1.55; }
  /* Sub-11px lives ONLY inside .paper. Our floor governs the interface; the
     sheet is a picture of the vendor's document, and enlarging their type
     would hide the reason a viewer needs zoom. */
  .paper .lh { display: flex; justify-content: space-between; align-items: flex-start; }
  .paper .lh h3 { margin: 0 0 4px; font-size: 16px; font-weight: 700; letter-spacing: .02em; }
  .paper .lh .addr { color: #5a5a63; font-size: 11px; }
  .paper .lh .word { font-size: 22px; font-weight: 300; letter-spacing: .18em;
                     color: #5a5a63; text-align: right; }
  .paper .facts { margin-top: 10px; font-size: 11px; }
  .paper .facts div { display: flex; gap: 14px; justify-content: flex-end; }
  .paper .facts span:first-child { color: #5a5a63; }
  .paper .facts span:last-child { min-width: 88px; text-align: right;
                                  font-variant-numeric: tabular-nums; }
  .paper .billto { margin-top: 34px; }
  .paper .billto .k { font-size: 10px; letter-spacing: .1em; color: #5a5a63; }
  .paper .ref { margin-top: 18px; font-size: 11px; color: #5a5a63; }
  .paper table.inv { width: 100%; border-collapse: collapse; margin-top: 26px; }
  .paper table.inv th { text-align: left; font-size: 10px; letter-spacing: .08em;
        color: #5a5a63; border-bottom: 1px solid #1a1a1f; padding: 0 0 6px;
        text-transform: uppercase; font-weight: 600; background: none; }
  .paper table.inv th.r, .paper table.inv td.r { text-align: right; }
  .paper table.inv td { padding: 8px 0; border-bottom: 1px solid #e4e4e0; font-size: 11px;
        color: #1a1a1f; }
  .paper .totals { margin-top: 14px; margin-left: auto; width: 240px; font-size: 11px; }
  .paper .totals div { display: flex; justify-content: space-between; padding: 4px 0; }
  .paper .totals .due { border-top: 1.5px solid #1a1a1f; margin-top: 6px; padding-top: 8px;
        font-weight: 700; font-size: 13px; }
  .paper .foot { margin-top: 42px; padding-top: 12px; border-top: 1px solid #e4e4e0;
        font-size: 10px; color: #5a5a63; }
  .paper .n { font-variant-numeric: tabular-nums; }

  .rail-r { overflow: auto; padding: 14px; }
  .sect { font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: .06em;
          color: var(--color-text-tertiary); margin: 0 0 7px; }
  .sect + .card { margin-bottom: 16px; }
  .card { border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg);
          background: var(--color-bg-elevated); }

  /* the amount pair — the whole decision payload */
  .pair { display: grid; grid-template-columns: 1fr 1fr auto; gap: 1px;
          background: var(--color-border-subtle); border-radius: var(--radius-lg);
          overflow: hidden; margin-bottom: 16px; }
  .cell { background: var(--color-bg-elevated); padding: 10px 12px; }
  .cell .k { font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: .06em;
             color: var(--color-text-tertiary); }
  .cell .v { font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums;
             color: var(--color-text-primary); margin-top: 3px; }
  /* provenance sits UNDER Expected — adjacent to the decision, not in the log */
  .cell .src { font-size: 11px; color: var(--color-text-secondary); margin-top: 4px; }
  .cell .src.weak { color: var(--color-status-progress); }
  .cell.d { background: color-mix(in srgb, var(--color-status-progress) 10%, var(--color-bg-elevated)); }
  .cell.d .v { color: var(--color-status-progress); }
  .cell.d .k { color: var(--color-status-progress); }

  .acts { display: flex; flex-direction: column; gap: 6px; padding: 10px;
          background: var(--color-bg-elevated); justify-content: center; }
  .btn { height: 30px; padding: 0 14px; border-radius: var(--radius-md); border: 0;
         font-size: 12px; font-weight: 500; cursor: pointer; white-space: nowrap;
         transition: background-color 75ms var(--ease-out-expo); }
  .btn:hover { background: var(--color-bg-hover); }
  .btn.primary { background: var(--color-accent); color: var(--color-primary-foreground); }
  .btn.primary:hover { background: var(--color-accent-hover); }
  .btn.second { background: transparent; color: var(--color-text-secondary);
                border: 1px solid var(--color-border-default); }
  .btn.second:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }

  /* Two dispositions side by side rather than stacked. Three flows use it and
     only two defined it, so flow 11's pair silently stacked -- a layout fault
     neither guard can see, because both buttons are correctly styled and
     clear the touch floor. Exactly the parallel-surface drift the repo has a
     rule about: a modifier used in three places and owned by none. */
  .acts.two { flex-direction: row; }
  .acts.two .btn { flex: 1; }

  /* An action completed in place, where nothing departs the view. Flow 1 and
     flow 9 each had a copy. */
  .done { display: flex; align-items: center; gap: 8px; padding: 10px;
          grid-column: 1 / -1; background: var(--color-bg-elevated);
          border-top: 1px solid var(--color-border-subtle);
          font-size: 12px; color: var(--color-text-secondary); }
  .done .tick { color: var(--color-status-done); }

  .kv { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 11px 12px; }
  .kv .k { font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: .06em;
           color: var(--color-text-tertiary); }
  .kv .v { font-size: 12px; color: var(--color-text-primary); margin-top: 2px;
           font-variant-numeric: tabular-nums; }
  .sect-row { display: flex; align-items: baseline; justify-content: space-between; }
  .sect-row a { font-size: 12px; color: var(--color-text-accent); text-decoration: none; }
  .sect-row a:hover { text-decoration: underline; }
  .sect-links { display: inline-flex; gap: 12px; text-transform: none;
                 letter-spacing: 0; font-weight: 400; }
  .sect-links a { color: var(--color-text-accent); text-decoration: none; }
  .sect-links a:hover { text-decoration: underline; }
  /* Not a destination -- it corrects the match, so it does not wear link blue. */
  .sect-links a.fix { color: var(--color-text-tertiary); }
  .sect-links a.fix:hover { color: var(--color-text-secondary); }
  .src-lnk { color: var(--color-text-accent); text-decoration: none; }
  .src-lnk:hover { text-decoration: underline; }
  /* Every fact in the log came from somewhere — the source is the way back to it. */
  .log .s a { color: var(--color-text-accent); text-decoration: none; }
  .log .s a:hover { text-decoration: underline; }

  .lines { border-top: 1px solid var(--color-border-subtle); }
  .lines .h, .lines .r { display: grid; grid-template-columns: 1fr auto auto; gap: 12px;
                         padding: 7px 12px; font-size: 12px; }
  .lines .h { font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
              color: var(--color-text-tertiary); }
  .lines .r { border-top: 1px solid var(--color-border-subtle); }
  .lines .r.sum { font-weight: 600; }
  .lines .n { font-variant-numeric: tabular-nums; }

  .log { display: grid; grid-template-columns: 54px 1fr auto; gap: 10px; padding: 7px 12px;
         font-size: 12px; border-top: 1px solid var(--color-border-subtle); }
  .log:first-child { border-top: 0; }
  .log .d { color: var(--color-text-tertiary); font-variant-numeric: tabular-nums; }
  .log .s { color: var(--color-text-tertiary); font-size: 11px; }

  .over-foot { display: flex; padding: 10px 16px; border-top: 1px solid var(--color-border-subtle); }

  [hidden] { display: none !important; }
  .btn[disabled] { opacity: .45; cursor: not-allowed; }
  .btn.primary[disabled]:hover { background: var(--color-accent); }

  /* ── composer ──────────────────────────────────────────────────────────
     The composer IS the message. Everything the system knows renders as the
     Slack message Michelle will read, and the ONE editable slot sits at the
     position her eye will land on -- so the empty field is a visible hole in
     what she receives, which is why it needs no "required" label. */
  .comp { overflow: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px; }
  /* flex-shrink defaults to 1, so in a column of fixed height the message card
     shrank below its own content and painted OVER the Send button -- it was
     unclickable while looking perfectly fine. Nothing static catches that. */
  .comp > * { flex: none; }
  .comp-head { display: flex; align-items: center; gap: 8px; }
  .comp-head .back { height: 26px; width: 26px; flex: none; border: 0; border-radius: var(--radius-md);
        background: transparent; color: var(--color-text-secondary); cursor: pointer; font-size: 13px; }
  .comp-head .back:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
  .comp-head .t { font-size: 13px; font-weight: 600; color: var(--color-text-primary); }

  /* Destination is stated before the message, because posting to a channel is
     a different act from sending a DM and the reader should know which. */
  .dest { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
          font-size: 12px; color: var(--color-text-secondary); }
  .dest .ch { color: var(--color-text-primary); font-weight: 500; }
  .dest .who { color: var(--color-text-accent); }
  .dest .pub { margin-left: auto; font-size: 11px; color: var(--color-text-tertiary); }

  .msg { border: 1px solid var(--color-border-default); border-radius: var(--radius-lg);
         background: var(--color-bg-elevated); padding: 12px; }
  .msg-top { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
  .msg-top .av { height: 18px; width: 18px; border-radius: var(--radius-sm);
        background: var(--color-accent); color: var(--color-primary-foreground);
        font-size: 11px; line-height: 18px; text-align: center; font-weight: 600; }
  .msg-top .nm { font-size: 12px; font-weight: 600; color: var(--color-text-primary); }
  .msg-top .tm { font-size: 11px; color: var(--color-text-tertiary); }
  .msg .mention { color: var(--color-text-accent); }
  .msg .ln { font-size: 12px; color: var(--color-text-secondary); margin-bottom: 2px; }
  .msg .ln b { color: var(--color-text-primary); font-weight: 500; }

  /* The three numbers are FACTS -- rendered, never editable. Letting AP retype
     them would let the message disagree with the payable it came from. */
  .msg .nums { display: grid; grid-template-columns: auto 1fr; gap: 1px 12px;
               margin: 8px 0; font-size: 12px; font-variant-numeric: tabular-nums; }
  .msg .nums dt { color: var(--color-text-tertiary); }
  .msg .nums dd { margin: 0; color: var(--color-text-primary); }
  /* NOT .over -- that is the slide-over panel's class, and a bare modifier of
     the same name inherits position:fixed and covers the whole screen. It is
     invisible (no background), so it reads as a dead button, not a layout bug. */
  .msg .nums dd.up { color: var(--color-status-progress); font-weight: 600; }

  .slot { width: 100%; min-height: 62px; resize: vertical; border-radius: var(--radius-md);
          border: 1px solid var(--color-accent); background: var(--color-bg-base);
          color: var(--color-text-primary); font: inherit; font-size: 12px; line-height: 1.5;
          padding: 8px 10px; margin: 8px 0; }
  .slot::placeholder { color: var(--color-text-tertiary); }
  .slot:hover { border-color: var(--color-accent); }
  .slot:focus { outline: 2px solid var(--color-accent); outline-offset: -1px; }

  .msg .cta { display: inline-block; margin-top: 4px; font-size: 12px; font-weight: 500;
              color: var(--color-text-accent); text-decoration: none; }
  .msg .att { display: inline-flex; align-items: center; gap: 6px; margin-top: 10px;
              padding: 5px 9px; border-radius: var(--radius-md);
              border: 1px solid var(--color-border-subtle); background: var(--color-bg-base);
              font-size: 11px; color: var(--color-text-secondary); }

  /* An action that moves work OUT of your view has to say what brings it back.
     Not where it goes -- the anxiety is "will I see this again", and a payable
     rotting silently in Waiting is the exact failure AP is trying to avoid.
     Label-and-value in the panel's existing vocabulary, so it stays a fact
     rather than becoming a sentence explaining the system. */
  /* A BRANCH, not two triggers. The label-and-value version read as one
     outcome with two causes, and they are different payables: cost changes
     and it comes back resolved; Oct 14 arrives and it comes back carrying the
     same problem plus a deadline. Every node is a real tag -- no state is
     invented for the diagram, which is what keeps it a model of the system
     rather than a picture of one. */
  .after { padding: 11px 12px; border-radius: var(--radius-md);
           background: var(--color-bg-elevated); font-size: 12px; }
  .after .node { display: flex; align-items: center; gap: 6px; }
  .after .stem { margin: 2px 0 2px 5px; padding-left: 13px;
                 border-left: 1px solid var(--color-border-default);
                 color: var(--color-text-tertiary); font-size: 11px; line-height: 17px; }
  .after .fork { margin-left: 5px; }
  /* wraps rather than overflows -- the arm carries a date and a rail name,
     and it cleared its container by ten pixels before this. */
  .after .arm { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; padding-left: 13px;
                position: relative; min-height: 26px; }
  /* elbow: the trunk drops to the arm's centre, then turns right */
  .after .arm::before { content: ""; position: absolute; left: 0; top: 0; width: 13px;
        height: 13px; border-left: 1px solid var(--color-border-default);
        border-bottom: 1px solid var(--color-border-default);
        border-bottom-left-radius: 4px; }
  .after .arm:not(:last-child)::after { content: ""; position: absolute; left: 0; top: 13px;
        bottom: 0; border-left: 1px solid var(--color-border-default); }
  .after .cond { color: var(--color-text-secondary); white-space: nowrap; }
  .after .to { color: var(--color-text-tertiary); }

  /* The control sits UNDER the branch it rewrites, so flipping it redraws the
     picture directly above -- you see what you chose without reading a label
     that describes it. The whole row is the target: a bare switch is 20x12 and
     fails the 24px floor in both dimensions. */
  .auto { display: flex; align-items: center; gap: 9px; width: 100%; height: 30px;
          margin-top: 9px; padding: 0 2px; border: 0; border-top: 1px solid var(--color-border-subtle);
          background: none; cursor: pointer; font: inherit; font-size: 12px;
          color: var(--color-text-secondary); text-align: left; }
  .auto:hover { color: var(--color-text-primary); }
  .auto .sw { flex: none; height: 14px; width: 24px; border-radius: var(--radius-full);
              background: var(--color-bg-active); position: relative;
              transition: background-color 80ms var(--ease-out-expo); }
  .auto .sw::after { content: ""; position: absolute; top: 2px; left: 2px; height: 10px; width: 10px;
              border-radius: var(--radius-full); background: var(--color-text-tertiary);
              transition: transform 80ms var(--ease-out-expo), background-color 80ms; }
  .auto[aria-pressed="true"] .sw { background: var(--color-accent); }
  .auto[aria-pressed="true"] .sw::after { transform: translateX(10px);
              background: var(--color-primary-foreground); }

  .comp-acts { display: flex; gap: 6px; }
  .comp-acts .btn { flex: 1; }

  /* Tooltip on any element carrying data-tip. Lives here rather than in a
     flow because a glyph control is unreadable without one, and there will be
     more of them. Shows on focus as well as hover -- a control whose only
     label appears on hover has no label for a keyboard. */
  [data-tip] { position: relative; }
  [data-tip]::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 6px);
      right: 0; z-index: 30; padding: 5px 8px; border-radius: var(--radius-md);
      background: var(--color-bg-active); color: var(--color-text-primary);
      border: 1px solid var(--color-border-default); font-size: 11px; line-height: 1.3;
      white-space: nowrap; opacity: 0; pointer-events: none;
      transition: opacity 80ms var(--ease-out-expo); }
  [data-tip]:hover::after, [data-tip]:focus-visible::after { opacity: 1; }



  /* ── shared modifiers ──────────────────────────────────────────────────
     Each of these was defined in exactly one flow and used in several, so
     every use outside the defining flow rendered UNSTYLED and silent:
     .src.firm in three flows, .pair.even in three, .tag.part in two, .drop in
     two. None of them is visible to check.sh (a missing rule has nothing to
     check) or to controls.mjs (they are not controls). Found by scanning for
     class names used in a flow's markup that no stylesheet mentions, which is
     now part of check.sh. */

  /* The amount pair with no diff cell. Absent, not zeroed. */
  .pair.even { grid-template-columns: 1fr 1fr; }

  /* A settled provenance. The COLOUR is the message: amber means the expected
     number is weak, this means it is agreed. */
  .cell .src.firm { color: var(--color-text-secondary); }
  /* A deduction under an amount. */
  .cell .src.minus { color: var(--color-status-done); }

  /* Part paid, and money that moved but is not confirmed. Both are the done
     colour because money DID move; what is unsettled is the rest of it. */
  .tag.part { background: color-mix(in srgb, var(--color-status-done) 12%, transparent);
              color: var(--color-status-done); }
  .tag.part .pip { background: var(--color-status-done); }

  /* An unmatched invoice's header keeps the WN-XXXX slot and states the
     absence, because position is what people have learned, not format. */
  .noord { font-size: 14px; font-weight: 600; color: var(--color-text-tertiary); }
  .invno { font-size: 12px; color: var(--color-text-secondary);
           font-variant-numeric: tabular-nums; }
  /* "no document" said where a document would be. */
  .nodoc { display: inline-flex; align-items: center; height: 20px; padding: 0 8px;
           border-radius: var(--radius-full); background: var(--color-bg-active);
           font-size: 11px; color: var(--color-text-tertiary); }

  /* Somewhere to put a file. */
  .drop { display: grid; place-items: center; gap: 6px; padding: 26px 16px;
          border: 1px dashed var(--color-border-default); border-radius: var(--radius-lg);
          background: var(--color-bg-elevated); cursor: pointer; text-align: center; }
  .drop:hover { border-color: var(--color-text-tertiary); background: var(--color-bg-hover); }
  .drop .t { font-size: 13px; color: var(--color-text-primary); }
  .drop .s { font-size: 11px; color: var(--color-text-tertiary); }

  /* ── the need-list ─────────────────────────────────────────────────────
     WHAT WE NEED, when v1 cannot read a document and a person must type what
     it says. Shared: flow 3 transcribes an invoice with no text layer, flow 9
     transcribes one that arrived as paper. Same job, same shape, one copy --
     a second treatment for "here are the facts still missing" is one more
     thing to learn for no gain.

     EVERY ROW IS RESOLVED OR IT IS NOT: filled, or explicitly not on the
     invoice. Blank is neither, and Save waits for all of them, so nothing
     downstream has to guess which kind of empty it is looking at. The dot is
     the state -- solid when known, dashed when known to be ABSENT, hollow
     when nobody has said. */
  .need { border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg);
          overflow: hidden; }
  .need .n { display: grid; grid-template-columns: 14px 1fr auto; gap: 10px;
             align-items: center; padding: 10px 12px; font-size: 12px;
             border-bottom: 1px solid var(--color-border-subtle); }
  .need .n:last-child { border-bottom: 0; }
  .need .dot { height: 8px; width: 8px; border-radius: var(--radius-full);
               border: 1px solid var(--color-text-tertiary); }
  .need .n.got .dot { border-color: var(--color-status-done);
                      background: var(--color-status-done); }
  .need .n.got { background: var(--color-bg-elevated); }
  .need .lbl { color: var(--color-text-secondary); }
  .need .n.got .lbl { color: var(--color-text-primary); }
  .need .val { color: var(--color-text-tertiary); font-variant-numeric: tabular-nums; }
  .need .n.got .val { color: var(--color-text-primary); }
  .need .n.known .dot { border-style: dashed; }
  .need .src-note { font-size: 11px; color: var(--color-text-tertiary); }
  .need input { height: 28px; width: 112px; border-radius: var(--radius-md);
                border: 1px solid var(--color-border-default);
                background: var(--color-bg-base); color: var(--color-text-primary);
                font: inherit; font-size: 12px; font-variant-numeric: tabular-nums;
                text-align: right; padding: 0 10px; }
  /* A DATE IS PICKED, NOT TYPED. "Sep 00" in a free-text box accepts
     anything and resolves nothing: 9/12 is two different days depending on
     who reads it, and a transposed Sep 12 for Sep 21 is invisible in text and
     obvious in a calendar. It also makes the stored value unambiguous without
     a parser, which is the half that leaks downstream.
     Wider than the other inputs because the control has a picker button of
     its own, and left-aligned because a date input lays its fields out from
     the left and cannot be pushed right. The dark calendar indicator comes
     free from color-scheme: dark on :root. */
  .need input[type="date"] { width: 138px; text-align: left; }
  .need input:hover { border-color: var(--color-text-tertiary); }
  .need input:focus { outline: 2px solid var(--color-accent); outline-offset: -1px; }
  /* A RECORDED ABSENCE, not a blank. Without this a null invoice_date cannot
     be told from a field nobody filled, and every future reader resolves that
     ambiguity differently -- one back-fills from the email date, one treats it
     as an error, one skips the row. Say it once, here, and it is a fact rather
     than a gap. */
  /* A glyph chip, labelled by its tooltip. The written "not on it" was a
     sentence fragment competing with the field it sits beside, and it read as
     an instruction rather than a control. */
  /* 24 square. It was 22 high, which is the shape of a control sized by eye
     against its 12px glyph rather than against the floor. */
  .need .miss { justify-self: end; height: 24px; width: 24px; padding: 0; line-height: 1;
                border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md);
                background: transparent; color: var(--color-text-tertiary);
                font: inherit; font-size: 12px; cursor: pointer; }
  .need .miss:hover { background: var(--color-bg-hover); color: var(--color-text-primary);
                      border-color: var(--color-border-default); }
  .need .n.absent .dot { border-style: dashed; border-color: var(--color-text-tertiary);
                         background: none; }
  .need .n.absent .lbl { color: var(--color-text-tertiary); }
  .need .miss-gap { width: 24px; }
  .need .said { justify-self: end; font-size: 11px; color: var(--color-text-tertiary); }
  .need .ctx { display: block; font-size: 11px; color: var(--color-text-tertiary); }
  .need .n { grid-template-columns: 14px 1fr auto auto; }

  /* A row whose value is chosen rather than read off the page. No "not on the
     invoice" escape, because the answer does not live on the document at all
     -- so it takes the gap, not the glyph. */
  .need select { height: 28px; border-radius: var(--radius-md);
                 border: 1px solid var(--color-border-default);
                 background: var(--color-bg-base); color: var(--color-text-primary);
                 font: inherit; font-size: 12px; padding: 0 8px; }
  .need select:hover { border-color: var(--color-text-tertiary); }
  .need select:focus { outline: 2px solid var(--color-accent); outline-offset: -1px; }

  /* ── stepper ───────────────────────────────────────────────────────────
     A SEQUENCE. Shared, because two flows want it and a second visual
     treatment for "here is an ordered list of things" is one more thing to
     learn for no gain.
     The division with the branch diagram is not style, it is shape: a stepper
     is a line, a diagram is a fork. Flows 4 and 2 keep the tree because they
     genuinely branch and a stepper cannot say "either this or that".
     MARKERS CARRY AGENCY. A number means she does it. A dashed ring means it
     happens to her -- QuickBooks pays, the read-back clears it -- and
     numbering those would promise a control that is not there. Same dashed
     ring flow 3 uses for a field she does not supply. */
  .steps { border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg);
           overflow: hidden; }

  /* A disabled button with no stated reason is a dead end -- you press it,
     nothing happens, and nothing on screen says why. The line names the one
     unmet requirement and clears the moment it is met. */
  .step { display: grid; grid-template-columns: 18px 1fr auto; gap: 10px;
          align-items: center; padding: 10px 12px; min-height: 30px;
          border-bottom: 1px solid var(--color-border-subtle); }
  .step:last-child { border-bottom: 0; }
  .step .n { height: 18px; width: 18px; border-radius: var(--radius-full);
             background: var(--color-bg-active); color: var(--color-text-tertiary);
             font-size: 11px; line-height: 18px; text-align: center; }
  .step .lbl { font-size: 12px; color: var(--color-text-tertiary); }
  .step .sub { font-size: 11px; color: var(--color-text-tertiary); margin-top: 1px; }
  .step.now { background: var(--color-bg-elevated); }
  .step.now .n { background: var(--color-accent); color: var(--color-primary-foreground); }
  .step.now .lbl { color: var(--color-text-primary); font-weight: 500; }
  .step.done .n { background: none; color: var(--color-status-done); }
  .step.done .lbl { color: var(--color-text-secondary); }
  /* A step that was SKIPPED, not done. In the settled state the missing tick
     on step 2 is the whole finding -- the gap is the evidence. */
  .step.gap { background: color-mix(in srgb, var(--color-status-progress) 10%, transparent); }
  .step.gap .n { background: none; color: var(--color-status-progress); }
  .step.gap .lbl, .step.gap .sub { color: var(--color-status-progress); }
  .step.system .n { background: none; border: 1px dashed var(--color-text-tertiary);
                    color: transparent; }
  .step.system .lbl { color: var(--color-text-secondary); }

  /* The ten-second answer to a chasing vendor, and it should land before she
     reads anything. A Paid cell in the pair is a number among numbers; this is
     a statement, at the top, in the done colour, with the stripe to match. */
  .over.settled { border-top-left-radius: var(--radius-xl); border-top: 2px solid var(--color-status-done); }
  .paid-banner { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
                 padding: 12px 14px; margin-bottom: 14px; border-radius: var(--radius-lg);
                 background: color-mix(in srgb, var(--color-status-done) 12%, transparent);
                 border: 1px solid color-mix(in srgb, var(--color-status-done) 35%, transparent); }
  .paid-banner .big { font-size: 16px; font-weight: 600; color: var(--color-status-done); }
  .paid-banner .when { font-size: 13px; color: var(--color-text-primary);
                       font-variant-numeric: tabular-nums; }
  .tag.paid-tag { background: color-mix(in srgb, var(--color-status-done) 12%, transparent);
                  color: var(--color-status-done); }
  .tag.paid-tag .pip { background: var(--color-status-done); }
  .paid-banner .how { margin-left: auto; font-size: 11px; color: var(--color-text-tertiary); }
  /* When the evidence is a photo and a Slack reaction rather than the books,
     the banner says the same thing in the same place but does not claim
     settled. Money moved -- that is green and certain. Whether QuickBooks
     knows is a separate fact and gets its own line. */
  .paid-banner.unsure { background: color-mix(in srgb, var(--color-status-done) 8%, transparent);
                        border-color: color-mix(in srgb, var(--color-status-done) 28%, transparent); }
  .paid-banner .gap { flex-basis: 100%; font-size: 11px; color: var(--color-status-progress); }

  /* Evidence, not entries. Winch records no payment of its own -- it shows
     every trace it can see across every rail, and the empty rails carry WHY
     they might be empty. Measured on 254 pay-link ACH payments: median 5 days
     to appear in QuickBooks, p90 7 days, and ZERO of them appeared the same
     day. On the day you would check, silence is guaranteed. */
  .ev { border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg);
        overflow: hidden; }
  .ev .r { display: grid; grid-template-columns: 14px 1fr auto auto; gap: 10px;
           align-items: baseline; padding: 9px 12px; font-size: 12px;
           border-bottom: 1px solid var(--color-border-subtle); }
  .ev .r:last-child { border-bottom: 0; }
  .ev .mk { font-size: 11px; text-align: center; color: var(--color-text-tertiary); }
  .ev .who { color: var(--color-text-tertiary); }
  .ev .amt { font-variant-numeric: tabular-nums; color: var(--color-text-tertiary); }
  .ev .lagged { font-size: 11px; color: var(--color-text-tertiary); min-width: 96px;
                text-align: right; }
  .ev .r.hit { background: var(--color-bg-elevated); }
  .ev .r.hit .mk { color: var(--color-status-done); }
  .ev .r.hit .who, .ev .r.hit .amt { color: var(--color-text-primary); }
  .ev .sub { display: block; font-size: 11px; color: var(--color-text-tertiary); }
  .ev-foot { padding: 8px 12px; background: var(--color-bg-app);
             border-top: 1px solid var(--color-border-subtle);
             font-size: 11px; color: var(--color-text-tertiary); }

  /* A labelled control row. Two flows type payment details into one. */
  .fld { display: grid; grid-template-columns: 96px 1fr; gap: 10px; align-items: center;
         margin-top: 10px; font-size: 12px; }
  .fld.tall { align-items: start; }
  .fld.tall > span:first-child { line-height: 24px; }
  .fld > span { color: var(--color-text-tertiary); }
  .fld input { height: 28px; border-radius: var(--radius-md);
               border: 1px solid var(--color-border-default); background: var(--color-bg-base);
               color: var(--color-text-primary); font: inherit; font-size: 12px;
               font-variant-numeric: tabular-nums; padding: 0 10px; }
  .fld input:hover { border-color: var(--color-text-tertiary); }
  /* A select in a .fld row needs this or it renders BROWSER DEFAULT: 19px
     tall, Arial, grey, square. Nineteen pixels is below the 24px touch floor,
     and nothing static can see it -- an unstyled control has no rule to check,
     so check.sh reads the file and finds nothing wrong. That is what
     controls.mjs is for. */
  .fld select { height: 28px; border-radius: var(--radius-md);
                border: 1px solid var(--color-border-default);
                background: var(--color-bg-base); color: var(--color-text-primary);
                font: inherit; font-size: 12px; padding: 0 8px; }
  .fld select:hover { border-color: var(--color-text-tertiary); }
  .fld select:focus { outline: 2px solid var(--color-accent); outline-offset: -1px; }

  .fld input:focus { outline: 2px solid var(--color-accent); outline-offset: -1px; }

  .toast { position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
           display: flex; align-items: center; gap: 12px; padding: 9px 14px;
           border-radius: var(--radius-md); background: var(--color-bg-elevated);
           border: 1px solid var(--color-border-default); box-shadow: var(--shadow-lg);
           font-size: 12px; color: var(--color-text-primary); z-index: 20; }
  .toast a { color: var(--color-text-accent); text-decoration: none; }

  .note { position: fixed; left: 14px; bottom: 12px; max-width: 320px; font-size: 11px;
          line-height: 1.5; color: var(--color-text-tertiary); }
  .note b { color: var(--color-text-secondary); font-weight: 500; }

  /* ══ board chrome ══════════════════════════════════════════════════════
     Shared by every register board. It was written inside board-01's <style>
     and the second board would have copied it -- five copies of a heading
     whose spacing took three rounds to settle is five things to keep in step.
     Column widths stay with the board; nothing else does.

     .reg, .reg-foot, .facet, .chk and .mute are renamed from the names they
     had while this was local (.work, .foot, .grp, .box, .q). Each of those is
     defined locally by a flow, and moving one here unscoped would have reached
     into three flows that have nothing to do with boards. check.sh's collision
     baseline is what surfaced it. */
  main { overflow: auto; }

  /* The rail lines up with the TABLE's top edge, not the page header, so the
     filters read as belonging to the table rather than to the screen. */
  .reg { display: flex; align-items: flex-start; gap: 20px; padding: 0 20px 40px; }
  .table-col { min-width: 0; }


  /* The summary sticks and the column header sticks beneath it, so scrolling
     eighty rows takes away neither the labels nor the filters that produced
     the rows under them. */
  /* 20px semibold: big enough to anchor the page, small enough not to read
     as a dashboard number. Tabular figures so 80 becoming 9 does not shift
     everything after it as the filters change. */
  .summary { position: sticky; top: 0; z-index: 2; display: flex;
             align-items: center; gap: 10px;
             /* A heading belongs to the thing under it. Measured as INK rather
                than boxes -- what you read is the glyphs -- it sat 26px below
                the nav and 57px above the column labels, so it looked like a
                page header floating on its own. Now 40 above and 28 below.
                The space above lives HERE and not on .reg, so it survives
                scrolling: when the block sticks, the heading keeps its air.
                Nothing is left under the text -- the column header's own 10px
                of padding plus the line slack already come to 28.
                12px of left padding is the cell padding, so the count lines up
                with the Vendor column's text and not with the table's edge. */
             height: 70px; margin: 0; padding: 26px 12px 0;
             background: var(--color-bg-base);
             font-size: 20px; font-weight: 600; letter-spacing: -0.01em;
             font-variant-numeric: tabular-nums;
             color: var(--color-text-primary); }
  /* Every child is height-BOUNDED, and the heading does not wrap. A WRAPPING
     flex line grows to its tallest item and the heading centres against that
     line, so an unbounded chips box (it measured 45px while nothing inside it
     exceeded 30) dropped the number 8px the instant a filter landed.
     It takes BOTH to break -- mutation-verified: wrap with bounded children is
     fine, and unbounded children without wrap is fine. Either half alone is
     therefore the fix, which is exactly why the first four mutations all came
     back clean and nearly had me believing the bug was elsewhere.
     Both are kept: the bound is the rule a new child must follow, nowrap is
     honest about a box with a fixed 44px height. controls.mjs re-checks it. */
  /* Plain text, with ordinary word spaces. A flex gap and a min-width box on
     the count held the line perfectly still, and the cost was a 40px hole
     after the number that read as a label rather than a phrase. Tabular
     figures alone keep the digits from jittering; the line reflows when 96
     becomes 5, and the chips shifting slightly is the cheaper problem.
     inline-BLOCK, not inline: height still has to be bounded or the heading
     drifts down when a chip lands (see the note above). */
  .summary .title { display: inline-flex; align-items: center; height: 30px;
                    margin: 0; font: inherit; }
  .summary .lede { display: inline-block; height: 30px; line-height: 30px; }
  /* The denominator and the separators recede so the eye lands on the count
     and the money. */
  .summary .mute { font-weight: 400; color: var(--color-text-tertiary); }
  /* NOT .chips. app.css already defines .chips as a bordered row of filter
     buttons, and a local rule that sets only display/gap inherits the rest --
     margin, padding-bottom and a border-bottom. That border rendered as a
     1px fragment under the chips, running the width of the chips box and no
     further, which read as a broken table rule. Fifth class collision of this
     shape; check.sh now refuses a new one. */
  /* margin-right:auto is what pushes everything AFTER the chips to the right
     edge -- a page action, the Filters button, or both. It lives here rather
     than on those, because two auto margins split the free space between them
     and park the action in the middle of the line. */
  .summary .fset { display: inline-flex; align-items: center; gap: 6px;
                   height: 30px; margin-left: 6px; margin-right: auto; }
  .summary .fset .fc { display: inline-flex; align-items: center; gap: 5px; height: 24px;
                 padding: 0 4px 0 9px; border-radius: var(--radius-full);
                 background: var(--color-bg-active); color: var(--color-text-primary);
                 font-size: 11px; font-weight: 500; letter-spacing: 0; }
  .summary .fset .fc button { height: 18px; width: 18px; border: 0;
                        border-radius: var(--radius-full); background: none;
                        color: var(--color-text-tertiary); font: inherit;
                        font-size: 11px; line-height: 1; cursor: pointer; }
  .summary .fset .fc button:hover { background: var(--color-bg-hover);
                              color: var(--color-text-primary); }
  .summary .clear { height: 24px; padding: 0 8px; border: 0;
                    border-radius: var(--radius-md); background: none;
                    color: var(--color-text-tertiary);
                    font: inherit; font-size: 12px; font-weight: 400;
                    cursor: pointer; }
  .summary .clear:hover { color: var(--color-text-primary); }

    /* The heading and the column header stick as ONE block: 70 is the heading's
     full height, so the header lands directly under it with the 28px gap
     preserved. The shadow appears only once something is actually scrolling
     underneath -- a rule drawn at rest would be a second horizontal line 28px
     from the one the header already has, which is the heaviness the gap was
     meant to avoid. */
  .board thead th { position: sticky; top: 70px; z-index: 1;
                    background: var(--color-bg-base); }
  main.scrolled .board thead th { box-shadow: 0 6px 10px -8px rgba(0, 0, 0, .55); }
  th.sorted { color: var(--color-text-secondary); }
  th.sorted::after { content: " ↑"; color: var(--color-text-tertiary); }

  tr.parked td { background: color-mix(in srgb, var(--color-bg-app) 60%, transparent); }
  tr.divider td { padding: 7px 12px; background: var(--color-bg-app);
                  font-size: 11px; color: var(--color-text-tertiary);
                  border-bottom: 1px solid var(--color-border-default);
                  position: sticky; top: 107px; z-index: 1; }
  /* 107 = the 70px heading block plus the 37px column header, both measured
     rather than guessed. An earlier guess slid the divider four pixels under
     the header every time it stuck. */

  /* ── the rail ──────────────────────────────────────────────────────────
     Sticky, because scrolling eighty rows should not take the filters away
     with them. Status and Due first: they are what drives the day. */
  /* 40px so SCOPE centres on the heading's text line rather than starting
     above it: the rail and the table then read as two columns under the nav.
     It tracks the heading's own top padding (26) plus the slack inside its
     44px line (14), so it moves when that does. */
  .filters { position: sticky; top: 0; flex: none; width: 216px; padding-top: 40px;
             max-height: calc(100vh - var(--top-h) - 20px); overflow-y: auto;
             padding-bottom: 20px; }
  /* The section header is the control. A caret of its own would be a second
     target for one job, and the whole row is already the width of the rail.
     26px so it clears the touch floor, which a bare 11px label would not. */
  .filters .facet { display: flex; align-items: center; gap: 7px; width: 100%;
                  height: 26px; margin: 0 0 4px; padding: 0 6px;
                  border: 0; border-radius: var(--radius-md); background: none;
                  font: inherit; font-size: 11px; font-weight: 500;
                  text-transform: uppercase; letter-spacing: .06em;
                  color: var(--color-text-tertiary); text-align: left;
                  cursor: pointer; }
  .filters .facet:hover { background: var(--color-bg-hover);
                        color: var(--color-text-secondary); }
  /* The first version measured 8.0 x 8.8 -- an 11px glyph scaled to .8, which
     is under the type floor twice over and read as a speck. A solid triangle
     at 12px is 9.2 x 12 and carries far more visual mass than the small
     outline one at any size, so the size went up and the transform went away:
     a glyph shrunk by a transform is a rule dodged rather than followed. */
  .filters .facet .car { flex: none; width: 12px; font-size: 12px; line-height: 1;
                       color: var(--color-text-tertiary);
                       transition: transform 80ms var(--ease-out-expo); }
  .filters .facet[aria-expanded="false"] .car { transform: rotate(-90deg); }
  /* A COLLAPSED SECTION STILL SAYS WHETHER IT IS DOING ANYTHING. Folding a
     facet away that is silently filtering the board is the "where did my
     payables go" problem the chip line exists to prevent, one level up. */
  .filters .facet .on { margin-left: auto; height: 16px; min-width: 16px;
                      padding: 0 5px; border-radius: var(--radius-full);
                      background: var(--color-accent);
                      color: var(--color-primary-foreground);
                      font-size: 11px; line-height: 16px; text-align: center;
                      letter-spacing: 0; }
  .filters .facet + .facet { margin-top: 0; }
  .filters .sect2 { margin-top: 18px; }
  .filters .sect2:first-child { margin-top: 0; }

  .opt { display: flex; align-items: center; gap: 8px; width: 100%; height: 26px;
         padding: 0 6px; border: 0; border-radius: var(--radius-md); background: none;
         color: var(--color-text-secondary); font: inherit; font-size: 12px;
         text-align: left; cursor: pointer; }
  .opt:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
  .opt .chk { height: 13px; width: 13px; flex: none; border-radius: var(--radius-sm);
              border: 1px solid var(--color-text-tertiary); }
  .opt[aria-pressed="true"] { color: var(--color-text-primary); }
  .opt[aria-pressed="true"] .chk { border-color: var(--color-accent);
        background: var(--color-accent); box-shadow: inset 0 0 0 2px var(--color-bg-base); }
  /* LIVE, within everything else that is selected. A facet's own options are
     counted with that facet ignored, or selecting one would zero its
     siblings and the rail would stop telling you what else is there. */
  /* Locked reads as ON and as not-yours-to-change: the tick is there, the
     hover is not, and the cursor does not promise anything. Greying it out
     would say "unavailable", which is the opposite of what it means. */
  .opt.locked { cursor: default; color: var(--color-text-primary); }
  .opt.locked:hover { background: none; }
  .opt .chk.on { border-color: var(--color-accent); background: var(--color-accent);
                 box-shadow: inset 0 0 0 2px var(--color-bg-base); }
  /* Not a .facet: the scope is not a section you can fold, because it is the
     board's definition rather than a filter anybody chose. */
  .scope-h { margin: 0 0 4px; padding: 0 6px; font-size: 11px; font-weight: 500;
             text-transform: uppercase; letter-spacing: .06em;
             color: var(--color-text-tertiary); }
  .scope-note { padding: 2px 6px 0 27px; font-size: 11px; line-height: 1.5;
                color: var(--color-text-tertiary); }

  .opt .n { margin-left: auto; font-size: 11px; color: var(--color-text-tertiary);
            font-variant-numeric: tabular-nums; }
  .opt[aria-pressed="true"] .n { color: var(--color-text-secondary); }
  /* An option that would empty the list still renders. A count that vanishes
     at zero cannot be told from one that was never there. */
  .opt.empty { opacity: .45; }

  .vfind { height: 26px; width: 100%; border-radius: var(--radius-md);
           border: 1px solid var(--color-border-default); background: var(--color-bg-base);
           color: var(--color-text-primary); font: inherit; font-size: 12px; padding: 0 8px; }
  .vfind::placeholder { color: var(--color-text-tertiary); }
  .vfind:hover { border-color: var(--color-text-tertiary); }
  .vfind:focus { outline: 2px solid var(--color-accent); outline-offset: -1px; }


  /* ── narrow ────────────────────────────────────────────────────────────
     Below 1200 the rail would squeeze the columns, so it becomes a button
     and a popover. The count on the button is the only thing that has to
     survive the collapse. */
  .fbtn { display: none; height: 28px; padding: 0 12px; border-radius: var(--radius-md);
          border: 1px solid var(--color-border-default); background: var(--color-bg-elevated);
          color: var(--color-text-secondary);
          font: inherit; font-size: 12px; font-weight: 400; letter-spacing: 0;
          cursor: pointer; }
  .fbtn:hover { color: var(--color-text-primary); }
  @media (max-width: 1200px) {
    .fbtn { display: inline-flex; align-items: center; gap: 7px; }
    .filters { position: fixed; top: calc(var(--top-h) + 8px); right: 16px; z-index: 25;
               width: 240px; max-height: calc(100vh - var(--top-h) - 32px);
               padding: 12px; border-radius: var(--radius-lg);
               background: var(--color-bg-elevated);
               border: 1px solid var(--color-border-default);
               box-shadow: var(--shadow-lg); }
  }

  /* An absence renders as the same mark on every board, so an empty cell is
     legible as "there is nothing here" rather than as a rendering fault. */
  .none { color: var(--color-text-tertiary); }
  /* Past the last age band, where the number itself is the problem. */
  .aged { color: var(--color-status-progress); font-variant-numeric: tabular-nums; }

  /* A PAGE ACTION. It CREATES a record rather than changing a listed one,
     which is the only reason a register is allowed to carry one at all -- a
     bulk action on rows nobody has opened is the wrong affordance. It sits
     after .fset, whose auto margin pushes it to the right edge. */
  .make { height: 28px; padding: 0 12px; flex: none;
          border: 0; border-radius: var(--radius-md);
          background: var(--color-accent); color: var(--color-primary-foreground);
          font: inherit; font-size: 12px; font-weight: 500; letter-spacing: 0;
          cursor: pointer; }
  .make:hover { background: var(--color-accent-hover); }

  /* THE EXCLUSIONS ARE THE CREDIBILITY. A coverage list is only worth opening
     if its contents are TRUE, and the only way to believe one is to see what
     it threw away and why. Each count is a way into that set: auditing what
     was removed is NAVIGATION, not a row action, so it does not break the rule
     that the board lists and the detail acts.
     From flow 11, which is where this was argued. Aligned to the cell text
     rather than the page, so it reads as the table's own footnote.
     Separation comes from PADDING, not margin, and there is no radius: flow 11
     restates padding, border-top and background but neither of those two, so a
     shared rule carrying them would reach in and change a page that has
     nothing to do with boards. check.sh's collision baseline said so. */
  .excl { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap;
          padding: 16px 12px 12px;
          border-top: 1px solid var(--color-border-subtle);
          background: var(--color-bg-app);
          font-size: 11px; color: var(--color-text-tertiary); }
  .excl .k { text-transform: uppercase; letter-spacing: .06em; }
  .excl a { color: var(--color-text-accent); text-decoration: none;
            font-variant-numeric: tabular-nums; }
  .excl a:hover { text-decoration: underline; }

  .empty2 { padding: 40px 12px; text-align: center; font-size: 12px;
            color: var(--color-text-tertiary); }
  .reg-foot { padding: 10px 20px 0; font-size: 11px; color: var(--color-text-tertiary); }
