/* ============================================================================
   Quest Connect â€” portal design system  Â·  v2
   ----------------------------------------------------------------------------
   OWNED BY THE ORCHESTRATOR. Page agents must not edit this file.

   v2 direction, after "looks old school, not clean":
     Â· White surfaces on a near-white canvas. No grey app background.
     Â· Light sidebar with a soft active pill â€” not a navy slab with white text.
     Â· Separation from SPACE first, hairlines second, borders almost never.
     Â· 15px base, taller line height, medium weights. No bold-everything.
     Â· Uppercase letterspaced micro-labels removed except where a table needs them.
     Â· One soft shadow level. 10-12px radii.
     Â· Navy is an ACCENT, not a background. Orange appears once per screen at most.

   Class names are unchanged from v1 on purpose â€” this is a restyle, not a rename.
   ========================================================================== */

:root{
  --navy-900:#00293F; --navy-800:#003C60; --navy-700:#0A4E77;
  --blue-600:#00609C; --blue-500:#3D8CC4; --blue-100:#E3EFF7;
  --orange-500:#F0552F; --orange-700:#C43C15; --orange-50:#FEF0EB;

  /* ---- v3, the reference retheme (design/REDESIGN_SPEC.md Â§2) -------------
     The inks lose their blue cast and go neutral. D-123 still binds, so the two
     quiet tiers were RECOMPUTED against the NEW grounds rather than copied from
     the spec - and the spec's own #6B7280 does NOT survive that computation:
     it is 4.40:1 on the new --hover/--tint #F4F4F6, under the 4.5 floor that
     test_elegance parametrises over paper/sunken/hover/canvas. Three units
     darker clears it, at a difference no eye can see.
       --ink-3 #687078 : 5.03 paper, 5.03 canvas, 4.73 sunken, 4.58 hover  (>=4.5 âœ“)
       --ink-4 #8C8C91 : 3.35 paper, 3.35 canvas, 3.15 sunken, 3.05 hover  (>=3.0 âœ“)
     Same tradeoff as before and for the same reason: --ink-4 keeps the 3:1 UI
     floor and NOT the 4.5:1 body floor, because the only greys that clear 4.5
     everywhere are --ink-3 itself, and collapsing the two deletes a tier.
     Spec's #8E8E93 was also moved: 2.97:1 on --hover, which is the ground the
     active nav item's count sits on. */
  --ink:#18181B; --ink-2:#3F3F46; --ink-3:#687078; --ink-4:#8C8C91;
  /* THE hairline. This token now does the work the shadow ring used to (D-122
     part-superseded): every reference separates surfaces with a visible 1px
     edge, not with elevation. */
  --line:#E9EAEC; --line-2:#DCDDE0;
  /* The page is WHITE now and the sidebar is the grey one - the references
     invert what v2 did. A white card on a white page is only legible because
     .card / .kpi / .auth-card gained a 1px --line border IN THE SAME EDIT.
     Never change one of those without the other. */
  --paper:#FFFFFF; --canvas:#FFFFFF; --sunken:#F7F8F9; --hover:#F4F4F6;
  --tint:#F4F4F6;
  /* THE PASTEL PALETTE (REDESIGN_SPEC Â§3a, owner 2026-09-01: "use beautiful
     pleasant pastel colors intelligently"). Seven grounds, seven names, no
     eighth - these are the SAME tokens the document templates and the PDF
     renderer already consume, which is why they are retuned rather than
     replaced. Retuned, not rewritten: every one was a little grey against the
     references, so each gains a few points of chroma and they are levelled to
     the same lightness band (L* 93-95) so a row of them reads as one family
     rather than as seven unrelated washes.
     They tint a GROUND ONLY. The foreground beside each is the darker
     same-hue ink on the .chip-* rules below, so no contrast floor moves. */
  --c-navy:#E5EDF7; --c-blue:#DEEBFB; --c-teal:#DAF1ED; --c-amber:#FDEFD9;
  --c-rose:#FCE7E2; --c-violet:#EBE9FB; --c-green:#E0F3E7;

  --ok-fg:#116B45; --ok-bg:#E7F5EE;
  --warn-fg:#8A5A12; --warn-bg:#FDF3E3;
  --bad-fg:#A8342A; --bad-bg:#FDEEEC;
  --info-fg:#0A5787; --info-bg:#E9F2F9;
  --neutral-fg:#5B6876; --neutral-bg:#F0F2F5;

  --sans:'QC Sans',system-ui,-apple-system,'Segoe UI',Arial,sans-serif;
  --serif:'QC Serif',Georgia,'Times New Roman',serif;
  --mono:'QC Mono',ui-monospace,Consolas,monospace;
  /* The reference scale (spec Â§4). The typeface does NOT change in this pass -
     swapping it means new files, a new subset and a re-proof of the PDF path -
     so the "clean" impression is carried by the size scale, the weights
     (450/500/550/600, never 700) and the letter-spacing instead. */
  --t-2xs:11px; --t-xs:12px; --t-sm:13px; --t-md:14px; --t-lg:16px;
  --t-xl:20px; --t-2xl:26px; --t-3xl:32px;

  /* DENSITY PASS (2026-09-01). The owner reviewed the whole product and said
     every screen is "not dense enough, too much scrolling". The large end of
     this scale is where the height actually went, so the large end is what
     moved: s4 16>14, s5 20>16, s6 24>20, s8 32>26, s12 48>34.
     s1-s3 are DELIBERATELY UNTOUCHED. They set intra-component gaps - a label
     to its input, an icon to its text - where 2px buys almost no page height
     and is exactly where "denser" turns into "cramped".
     The scale is still strictly monotonic, so no two rungs collapsed into one.
     Two things this cannot break, checked rather than assumed:
       - .form-band is repeat(auto-fit,minmax(128px,1fr)) with gap:var(--s4).
         A SMALLER gap gives each track MORE width, so the eight-cell band that
         was fitted to a measured 1168px cannot clip because of this.
       - .lrow's tracks are fixed px and its gap is var(--s3), which did not
         move, so the rebuilt line-item row is untouched.
     Measured, not asserted: documents list 12072 > 9852px, document create
     1412 > 1274px, at a fixed 1280x900 in Chrome. */
  --s1:4px; --s2:8px; --s3:12px; --s4:14px; --s5:16px; --s6:20px; --s8:26px; --s12:34px;
  --r-sm:8px; --r-md:12px; --r-lg:16px; --r-pill:999px;
  /* SUPERSEDED (2026-09-01, REDESIGN_SPEC Â§7): the 0-blur spread ring is gone
     and the 1px border is back. The ring reads as a shadow rather than an edge,
     which is exactly the "no edge at all" fault the owner reported. Shadows are
     now almost invisible - cards barely lift, and --sh-md/--sh-lg exist only for
     things that genuinely float (menus, modals). */
  --sh:0 1px 2px rgba(16,24,40,.04);
  --sh-md:0 4px 12px -2px rgba(16,24,40,.08);
  --sh-lg:0 16px 40px -12px rgba(16,24,40,.18);
  --sh-btn:0 1px 2px rgba(16,24,40,.05);
  --ring:0 0 0 3px rgba(0,96,156,.16);

  /* ONE control height, for every button, input, select, textarea and toolbar
     control in the product. It was two: inputs 38px, buttons and filter selects
     36px, so on every form and every toolbar the button sat 1px proud at the
     top and 1px shy at the bottom of the field beside it. 38 rather than 36
     because the taller of the two is the better touch target (CLAUDE.md 1.3)
     and because shrinking a 13.5px field to 36px leaves 11px of vertical pad,
     which reads cramped - and because the complaint was that the BUTTONS look
     wrong, so the buttons are what should grow. .btn-sm (30px) is a deliberate
     variant for row actions, not a mismatch, and keeps its own height. */
  --ctl-h:38px;
  --sidebar-w:232px; --topbar-h:60px; --bottomnav-h:58px;
  --dur:140ms; --ease:cubic-bezier(.2,0,.15,1);
}

*{box-sizing:border-box}
/* `body` must NOT be height:100%. Measured: on any page taller than the
   viewport it computed to 729.6px against 1159px of content, which left the
   sidebar's `position:sticky` with no room to travel in - so sticky was inert
   and the entire navigation scrolled away, leaving a half-height white column
   against the grey canvas. `.app` already carries `min-height:100vh`, so a
   short page still fills the screen and page height is unchanged. */
html{height:100%}
/* line-height 1.6 > 1.5 in the density pass. This is the one lever that
   shortens EVERY block of text on every screen without touching a single font
   size - the brief's hard limit is that body text may not get smaller, and it
   has not: --t-md is still 15px. 1.5 is a normal reading measure, not a tight
   one, and .textarea keeps its own 1.6 because that is where people type
   paragraphs. The A4 preview and the real PDF are unaffected: .sheet declares
   its own line-height, and app/templates/documents/document.html carries its
   own inline <style>, so app.css never reaches the renderer. */
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);
  font-size:var(--t-md);line-height:1.5;-webkit-font-smoothing:antialiased;
  font-feature-settings:"kern" 1;letter-spacing:-.005em}
a{color:var(--blue-600);text-decoration:none}
a:hover{color:var(--navy-800)}
:focus-visible{outline:2px solid var(--blue-600);outline-offset:2px;border-radius:5px}
::selection{background:var(--c-blue);color:var(--navy-900)}
/* Default scrollbars are the loudest unstyled thing left in most builds. */
*::-webkit-scrollbar{width:12px;height:12px}
*::-webkit-scrollbar-track{background:transparent}
*::-webkit-scrollbar-thumb{background:#C3CEDA;border-radius:99px;border:3px solid transparent;
  background-clip:content-box}
*::-webkit-scrollbar-thumb:hover{background:#A9B7C6;background-clip:content-box}
*{scrollbar-width:thin;scrollbar-color:#C3CEDA transparent}
.vh{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);
  white-space:nowrap}
.mono{font-family:var(--mono);font-feature-settings:"tnum" 1;font-size:.94em;letter-spacing:0}
.num,.money{font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1}
.money{text-align:right;white-space:nowrap}
.muted{color:var(--ink-3)}
/* Search highlight. Unstyled <mark> falls back to #FFFF00 browser default â€” six blocks of
   raw highlighter on a desaturated navy page. Caught in design review. */
mark{background:var(--blue-100);color:var(--navy-800);padding:0 3px;border-radius:3px;
  font-weight:500}
.r{text-align:right}.c{text-align:center}
.stack{display:flex;flex-direction:column}
.row{display:flex;align-items:center}
.gap2{gap:var(--s2)}.gap3{gap:var(--s3)}.gap4{gap:var(--s4)}.gap6{gap:var(--s6)}
.grow{flex:1}.wrap-w{flex-wrap:wrap}

/* ===================== SHELL ===================== */
.app{display:grid;grid-template-columns:var(--sidebar-w) 1fr;min-height:100vh;
  background:var(--canvas)}
/* GREY sidebar against a WHITE content area - the shell shape all six
   references share (REDESIGN_SPEC Â§1). Hairline on the right edge only. */
.sidebar{background:var(--sunken);border-right:1px solid var(--line);
  display:flex;flex-direction:column;position:sticky;top:0;height:100vh}
.sb-brand{display:flex;align-items:center;gap:var(--s2);padding:var(--s5) var(--s5) var(--s4)}
.sb-brand img{width:118px;height:auto}
.sb-env{font-family:var(--mono);font-size:9.5px;letter-spacing:.04em;background:var(--orange-50);
  color:var(--orange-700);padding:2px 6px;border-radius:5px;text-transform:uppercase;
  font-weight:500}
.sb-nav{flex:1;overflow-y:auto;padding:var(--s2) var(--s3)}
.sb-sec{font-size:var(--t-2xs);color:var(--ink-4);font-weight:500;letter-spacing:.04em;
  padding:var(--s6) var(--s3) 6px}
.sb-item{display:flex;align-items:center;gap:10px;min-height:34px;padding:0 10px;
  border-radius:var(--r-sm);
  color:var(--ink-2);font-size:13.5px;font-weight:500;position:relative;
  transition:background var(--dur) var(--ease),color var(--dur) var(--ease)}
.sb-item:hover{background:var(--hover);color:var(--ink)}
/* SUPERSEDED (2026-09-01, REDESIGN_SPEC Â§3): the filled navy pill is gone. Not
   one of the six references fills its active nav row - they all use a quiet
   --tint ground, the darkest ink and half a weight step, with no bar and no
   colour. A navy slab is the single loudest object in the old shell. */
.sb-item[aria-current="page"]{background:var(--tint);color:var(--ink);font-weight:550;
  box-shadow:none}
.sb-item[aria-current="page"]:hover{background:var(--tint);color:var(--ink)}
.sb-item .ic{width:18px;height:18px;flex:0 0 18px;opacity:.72}
.sb-item[aria-current="page"] .ic{opacity:1}
.sb-item .badge{margin-left:auto;font-family:var(--mono);font-size:11px;color:var(--ink-4);
  font-weight:400}
.sb-item[aria-current="page"] .badge{color:var(--ink-4)}
/* pinned to the bottom and separated by a hairline, per the references */
.sb-foot{padding:var(--s3) var(--s4) var(--s4);display:flex;align-items:center;gap:10px;
  border-top:1px solid var(--line)}
.sb-avatar{width:32px;height:32px;border-radius:var(--r-pill);background:var(--navy-800);
  display:grid;place-items:center;font-size:var(--t-xs);font-weight:550;color:#fff;flex:0 0 32px}
/* People read faster when their initials have a stable colour. Deterministic per person,
   desaturated so a list of them never looks like a toy. */
.av-a{background:#2F6DA8}.av-b{background:#1F7A6B}.av-c{background:#9A5A2B}
.av-d{background:#6A5AA8}.av-e{background:#A8464A}.av-f{background:#4A6E2E}
.sb-who{min-width:0}
.sb-who b{display:block;font-size:var(--t-sm);font-weight:550;color:var(--ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sb-who span{font-size:var(--t-xs);color:var(--ink-3)}
.sb-who a{color:var(--ink-3);font-size:var(--t-xs)}

/* OPAQUE, and this is a rule rather than a taste (CLAUDE.md 1.4 forbids
   glassmorphism by name). It was rgba(255,255,255,.86) + a 10px backdrop blur,
   and the effect was not theoretical: on a scrolled list the row underneath
   bled through the bar as a blurred smear and read as a rendering fault. The
   1px hairline below already does the separating. */
.topbar{height:var(--topbar-h);background:var(--paper);
  border-bottom:1px solid var(--line);
  display:flex;align-items:center;gap:var(--s4);padding:0 var(--s8);position:sticky;top:0;z-index:30}
.tb-search{max-width:420px;width:100%;position:relative}
.tb-user{margin-left:auto;display:flex;align-items:center;border-radius:var(--r-pill)}
.tb-user .sb-avatar{width:29px;height:29px;flex:0 0 29px;font-size:var(--t-2xs)}
.tb-idle + .tb-user{margin-left:var(--s4)}
/* Phone-only sign out. The sidebar (which holds the real one) is display:none
   below 1024px and .tb-user is .d-only, so a phone had NO way to sign out at
   all. Not a nav item: .bottomnav's track count must keep matching len(BOTTOM)
   in app/nav.py (D-089 / L-03). */
.tb-out{margin-left:auto}
.tb-search input{width:100%;height:var(--ctl-h);border:1px solid transparent;border-radius:var(--r-sm);
  background:var(--sunken);padding:0 var(--s3) 0 34px;font:inherit;font-size:var(--t-sm);
  color:var(--ink);transition:background var(--dur),border-color var(--dur)}
.tb-search input::placeholder{color:var(--ink-4)}
.tb-search input:focus{outline:none;border-color:var(--blue-500);box-shadow:var(--ring);
  background:var(--paper)}
.tb-search .mag{position:absolute;left:11px;top:10px;width:15px;height:15px;color:var(--ink-4)}
.tb-search kbd{position:absolute;right:9px;top:9px;font-family:var(--mono);font-size:10.5px;
  color:var(--ink-4)}
.tb-idle{margin-left:auto;font-size:var(--t-xs);color:var(--ink-4)}
.tb-idle b{font-family:var(--mono);color:var(--ink-3);font-weight:400}

.main{min-width:0;display:flex;flex-direction:column}
.content{padding:var(--s8);max-width:1280px;width:100%}
.content.narrow{max-width:940px}
.content.wide{max-width:1480px}
.bottomnav{display:none}

/* ===================== PAGE ===================== */
.page-head{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--s4);
  flex-wrap:wrap;margin-bottom:var(--s6)}
/* 20px, not 27px. Every reference titles its page at 20-22px and lets the
   toolbar under it do the separating (REDESIGN_SPEC Â§3). */
.page-head h1{font-size:var(--t-xl);font-weight:600;margin:0;letter-spacing:-.01em;
  line-height:1.2;color:var(--ink)}
.page-head p{margin:6px 0 0;color:var(--ink-3);font-size:var(--t-sm)}
.page-actions{display:flex;gap:var(--s2);flex-wrap:wrap}
.breadcrumb{font-size:var(--t-xs);color:var(--ink-4);margin-bottom:var(--s3)}
.breadcrumb a{color:var(--ink-3)}
.breadcrumb span{margin:0 7px;color:var(--ink-4)}

/* ===================== BUTTONS ===================== */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:var(--ctl-h);
  padding:0 15px;border-radius:var(--r-sm);border:0;background:var(--paper);
  color:var(--ink-2);font:inherit;font-size:13.5px;font-weight:500;cursor:pointer;
  white-space:nowrap;letter-spacing:-.005em;
  /* SUPERSEDED (2026-09-01, REDESIGN_SPEC Â§7, D-122 part): a real 1px border,
     not a spread ring. box-sizing is border-box globally, so the control height
     does not move. */
  border:1px solid var(--line);box-shadow:var(--sh-btn);
  transition:background var(--dur) var(--ease),border-color var(--dur) var(--ease),
             color var(--dur) var(--ease),transform 60ms var(--ease)}
.btn:hover{background:var(--hover);color:var(--ink);border-color:var(--line-2)}
/* a half-pixel press. Enough to feel tactile, not enough to notice as motion. */
.btn:active{transform:translateY(.5px)}
.btn .ic{width:15px;height:15px}
/* NEAR-BLACK, not navy (REDESIGN_SPEC Â§7). All six references reserve one
   filled, almost-black pill for the single primary action on the screen; a
   saturated blue button is the thing that read as "not premium". */
.btn-primary{background:var(--ink);color:#fff;border-color:var(--ink)}
.btn-primary:hover{background:#2B2B31;color:#fff;border-color:#2B2B31}
.btn-danger{background:var(--paper);color:var(--bad-fg);border-color:var(--line)}
.btn-danger:hover{background:var(--bad-bg);color:var(--bad-fg);border-color:#EAC4BE}
.btn-ghost{background:transparent;color:var(--ink-2);border-color:transparent;box-shadow:none}
.btn-ghost:hover{background:var(--hover);color:var(--ink);border-color:transparent}
.btn-sm{height:30px;padding:0 11px;font-size:var(--t-xs)}
.btn:disabled{opacity:.45;cursor:not-allowed}
.btn-block{width:100%}
/* Quick create on a record-list row. A soft tinted chip, not an outlined one -
   measured on the real screen: two outlined boxes on every row of a list read as
   twice the furniture, while a tint has no high-contrast edge and recedes. Same
   vocabulary as the tinted squares behind the series icons, and it still reads as
   a button rather than a link. NO ring at rest: the ring is the accent and it
   ARRIVES ON HOVER, exactly like the row's own 2px rail. The half-pixel press
   comes free from .btn:active. Archive beside these stays a plain .btn-ghost, so
   "make a document" and "put this away" never look like the same weight.

   SUPERSEDED (2026-09-01, REDESIGN_SPEC Â§7, D-120): the tint is gone. Every
   reference uses OUTLINED neutral controls and spends fill on the one primary
   action, so a blue lozenge repeated down a list is both the loudest thing on
   the screen and not the most important one. .btn-quick now inherits .btn
   whole; only its leading `+` stays quiet. */
/* `> .ic`, a DIRECT child, on purpose. Â§3a puts a pastel document-type tile on
   the create chip - `<span class="chip chip-xs chip-green"><svg class="ic">`.
   A descendant selector here would be (0,2,0) and would grey that icon back
   out, beating the (0,1,0) .chip-* that gives it its meaning. */
.btn-quick > .ic{color:var(--ink-3)}
.btn-quick:hover > .ic{color:var(--ink-2)}
/* The SAME chip, in a PAGE HEADER, where the argument above inverts. On a row
   the tint is right BECAUSE it recedes: 79 rows x 2 outlined boxes is furniture.
   In a header there are four of them, once, and they are the reason the screen
   is open - so receding is the defect. Measured as shipped: 30px tall,
   box-shadow:none, four flat lozenges at 100/108/126/141px that sit level with
   nothing on the page and carry neither ring nor shadow, i.e. no edge at all.
   Three changes, no new colour and no new element:
     - --ctl-h, not .btn-sm's 30px, so they sit exactly level with the .btn-ghost
       beside them and with every select and input on the screen. 30px was also
       under the 40px floor the owner has twice rejected;
     - the 0 0 0 1px ring D-122 made this system's edge idiom, at .22 alpha of
       --blue-600 - the same "tint an existing token" move --ring and .btn-primary
       already make, so no sixth colour and no new hex enters the palette;
     - --t-sm, the size every other labelled control uses.
   HIERARCHY IS BETWEEN GROUPS, NOT WITHIN. The four types are peers - the type
   filter below says so - and promoting one would be a claim about frequency
   nobody has measured. What was missing was the create family reading as one
   thing against "Outstanding", which stays a .btn-ghost and stays first.
   `.page-actions` is the whole scope on purpose: reports/index.html gets this
   for free, and the master-list rows keep D-120 EXACTLY - measured unchanged
   at 100.1/108.3px, 30px tall, box-shadow:none.

   2026-09-01: the three changes below survive, but the blue that carried them
   does not - .btn now has a real --line border at rest, so the create chips get
   their edge from the same place every other control does and this rule is down
   to geometry. */
.page-actions .btn-quick{height:var(--ctl-h);padding:0 13px;font-size:13.5px}
/* A SQUARE, ICON-ONLY .btn - for a row's repeated actions (View / Download /
   Delete), where the same three words on every one of 79 rows are furniture
   rather than information.
   Geometry only. It adds no colour, no fill and no shadow of its own, so it
   composes with every variant already above: .btn-icon.btn-ghost is the quiet
   one, .btn-icon.btn-danger the destructive one.

   SIZE, and it is a stated number rather than a shrug: 38px square on a
   pointer, which is --ctl-h - the one control height in the product - so an
   icon button in a toolbar or an action cell sits exactly level with the
   buttons, inputs and selects beside it. 30px (.btn-sm) was rejected: it is
   under the 40px the owner has twice said a phone target must clear, and a
   square 30px box is a smaller target than the 30px-tall text button it would
   replace, not a bigger one. On touch it goes to 44px - see the max-width:1023px
   block at the foot of this file, which also raises it back over .btn-sm should
   the two ever be combined.

   THE ICON MUST CARRY AN ACCESSIBLE NAME. There is no label inside this button,
   so the markup owes it an aria-label (or a .sr-only span). CSS cannot supply
   one and this comment is the only place to say so. */
.btn-icon{width:var(--ctl-h);padding:0;flex:0 0 var(--ctl-h)}
.btn-icon .ic{width:16px;height:16px}

/* ===================== SURFACES ===================== */
/* âš  THE PAIRED CHANGE. --canvas is #FFFFFF now, so this border is the ONLY
   thing separating a card from the page it sits on. It and the white canvas
   must never move apart. (REDESIGN_SPEC Â§2; supersedes the D-122 ring.) */
.card{background:var(--paper);border:1px solid var(--line);border-radius:var(--r-md);
  box-shadow:var(--sh);overflow:hidden}
.card+.card{margin-top:var(--s5)}
/* ...but that rule was written for cards STACKED down a column, and it also
   fired on the second child of a two-column grid: measured on Overview, the
   two .grid2 cards sat at y=320 and y=340, misaligned by exactly this --s5.
   The grid's own `gap` already does the spacing there, so the margin is pure
   error. Scoped by the grid containers only - .split is NOT listed, because
   .split is already correctly aligned (both cards y=223 on the document page)
   and a rule it does not need is a rule that can break it later. */
.grid2 > .card + .card,.grid3 > .card + .card{margin-top:0}
.card-head{display:flex;align-items:center;justify-content:space-between;gap:var(--s3);
  padding:var(--s5) var(--s6);border-bottom:1px solid var(--line)}
.card-head h2{font-size:var(--t-md);font-weight:600;margin:0;letter-spacing:-.01em}
.card-head .sub{font-size:var(--t-xs);color:var(--ink-3);margin-top:2px}
.card-body{padding:var(--s6)}
.card-body.tight{padding:0}
.card-foot{padding:var(--s4) var(--s6);border-top:1px solid var(--line);
  display:flex;justify-content:space-between;align-items:center;gap:var(--s3);
  font-size:var(--t-xs);color:var(--ink-3)}
/* A .pager nested in a .card-foot doubled the padding â€” 32px vertical, 48px horizontal. */
.card-foot > .pager{padding:0;flex:1}
/* A .card-head used as a mid-card divider only had a bottom border, so it glued to the group
   ABOVE it and every date grouping read one group off. */
.card-body + .card-head,.card-body.tight + .card-head{border-top:1px solid var(--line-2)}
/* space-between pins a lone child to the LEFT â€” the opposite of every .modal-foot, which is
   flex-end. Deleting an action pair left several footers with one stranded button. */
.card-foot > :only-child{margin-left:auto}
/* Two consecutive .card-body siblings opened 48px of unruled white between a grand total and
   the buttons that act on it. */
.card-body + .card-body{border-top:1px solid var(--line);padding-top:var(--s5)}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:var(--s5)}
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s5)}
.split{display:grid;grid-template-columns:1fr 336px;gap:var(--s5);align-items:start}

/* ===================== KPI ===================== */
/* NOW VARIABLE, same mechanism and same reason as `.rlist-head`/`.rlist-row`
   below and `.bottomnav` above: a LITERAL track count in a stylesheet and an
   item count in a template are two numbers in two files that must be remembered
   together, and on this project that pair has silently disagreed twice (D-089,
   lesson L-03) - a row occupying 4/5 of the width with one dead column. The
   default is byte-for-byte the old value, so every existing caller - Overview's
   four tiles, and every other `.kpi-row` in the app - resolves to exactly the
   grid it had before and no call site changes.
   A row that is NOT four tiles now says so itself:
     <div class="kpi-row" style="--cols:1.6fr 1fr 1fr">  (hero + two)
   Track count is then declared next to the tiles it counts, which is the only
   place it can be kept honest. */
.kpi-row{display:grid;grid-template-columns:var(--cols,repeat(4,1fr));
  gap:var(--s4);margin-bottom:var(--s6)}
/* Border, for the same reason .card has one: the page is white. */
.kpi{background:var(--paper);border:1px solid var(--line);border-radius:var(--r-md);
  padding:var(--s5);box-shadow:var(--sh);position:relative}
.kpi .k{font-size:var(--t-sm);color:var(--ink-3);font-weight:450}
.kpi .v{font-size:var(--t-2xl);font-weight:600;letter-spacing:-.022em;margin-top:7px;
  font-variant-numeric:tabular-nums;line-height:1.1;color:var(--ink)}
.kpi .d{font-size:var(--t-xs);color:var(--ink-4);margin-top:5px}
.kpi .d.up{color:var(--ok-fg)}.kpi .d.down{color:var(--bad-fg)}
.kpi-flat{border:0;background:transparent;padding:0;box-shadow:none}
.kpi .v.mono{font-size:var(--t-lg);letter-spacing:-.01em;overflow-wrap:anywhere;font-weight:550}
/* DELETED (2026-09-01), verified dead by TEST and not by inspection:
     .kpi > .chip{position:absolute;top:var(--s4);right:var(--s4)}
     .kpi .k{padding-right:calc(36px + var(--s3));
       min-height:calc(var(--s4) + 36px - var(--s5))}
   and with them the long measurement note that justified the gutter. Every KPI
   chip is now a GRANDCHILD, inside `.k`, so `.kpi > .chip` matched nothing -
   while the gutter it compensated for went on spending 48px of every label's
   width and ~13px of dead height above every figure.
   tests/test_kpi_chip_clearance.py passes with these rules present AND absent,
   which is what makes the deletion safe in either order. Removed rather than
   left behind: this file's own history says a dead rule with a long rationale
   is how the next reader re-creates the defect, because they assume it is live
   and design around it. The measurement it records is preserved in the git
   history of this line, which is the right place for a superseded measurement.
   REPLACED BY the flex rule below: the label and its mark are one line now, so
   a wrapping label ("Waiting for approval") no longer wraps back UNDER the
   mark at 390px, which is what an inline chip did. */
.kpi .k{display:flex;align-items:center;gap:var(--s2)}

/* ---------- .kpi-hero - the headline figure in a row of figures ----------
   Used WITH .kpi (`class="kpi kpi-hero"`), never instead of it: it is the same
   tile - same paper, same --sh elevation, same chip vocabulary - with one thing
   different, the size of the number. A report answers one question first and
   the rest in support of it; a row of four identical tiles makes the reader
   find the headline instead of being handed it.

   IT DELIBERATELY DOES NOT CHANGE THE TILE'S PADDING, and that is the whole
   care taken here. `.kpi .k` above reserves the chip's gutter with a min-height
   DERIVED from the chip's geometry and the tile's own padding
   (`--s4 + 36px - --s5`). Repadding the hero to --s6 would silently falsify
   that derivation by 4px and put a hero figure back under its own chip - the
   exact defect the block above was written to fix. Bigger type, same box.

   Money gets one rung less than a count, and for a measured reason: `.kpi .v.mono`
   sets `overflow-wrap:anywhere`, so a rupee figure that outgrows its tile does not
   overflow - it BREAKS MID-NUMBER, and "5,14,134.9 / 6" reads as a different
   amount. --t-2xl for money leaves slack where --t-3xl would spend it. That rule
   is `.kpi .v.mono` (0,3,0), so overriding it needs the same weight - a bare
   `.kpi-hero .v` (0,2,0) would lose to it and a hero total would render at 17px. */
.kpi-hero .v{font-size:var(--t-3xl);margin-top:9px}
.kpi-hero .v.mono{font-size:var(--t-2xl)}
/* Money gets one rung less than a count - the doctrine already stated above
   `.kpi-hero .v.mono`.

   âš  `.kpi.kpi-hero`, TWO classes on one element, and that is the whole point of
   this rule. The previous attempt was written `.kpi-hero .v.kpi-money`, which
   is (0,3,0) - and `.kpi .v.kpi-money` in the WAVE 5A block near the END of
   this file is ALSO (0,3,0). Equal specificity is decided by source order, the
   reports block is later, so the later rule won and the hero rendered at 21px:
   exactly the same size as the five tiles it exists to outrank. That shipped,
   and the owner looked at it and said he saw no improvement.

   MEASURED IN CHROME, on the real /reports bytes, at 1536px: before this rule
   hero 21px / secondary 21px; after it hero 27px / secondary 21px. Doubling the
   class to (0,4,0) makes it win on WEIGHT rather than on position, so moving
   either block cannot silently revert it again. Its phone twin in the
   `max-width:760px` band is doubled for the same reason and must stay so - a
   (0,3,0) phone rule would now lose to THIS one and put a 27px crore figure on
   a 390px screen. */
.kpi.kpi-hero .v.kpi-money{font-size:var(--t-2xl)}
.kpi-hero .k{font-size:var(--t-sm)}

/* ---------- icon chip ----------
   A soft tinted rounded square behind an icon. This is what stops a page of white
   rectangles reading as a spreadsheet: <span class="chip chip-amber"><svg class="ic">â€¦</svg></span> */
.chip{display:inline-grid;place-items:center;width:36px;height:36px;border-radius:11px;
  background:var(--c-navy);color:var(--navy-800);flex:0 0 36px;
  box-shadow:inset 0 0 0 1px rgba(13,21,32,.045)}
.chip .ic{width:18px;height:18px}
.chip-blue{background:var(--c-blue);color:var(--blue-600)}
.chip-teal{background:var(--c-teal);color:#0E6E66}
.chip-amber{background:var(--c-amber);color:var(--warn-fg)}
.chip-rose{background:var(--c-rose);color:var(--bad-fg)}
.chip-green{background:var(--c-green);color:var(--ok-fg)}
.chip-violet{background:var(--c-violet);color:#4B44A8}
/* .chip-navy names the ground `.chip` already has. It exists so a MAP can hold
   a class for every branch (core/_hues.html) instead of one branch holding ""
   and relying on the base rule - a blank string in a hue table reads as a bug
   and gets "fixed" into a colour nobody chose. It adds no eighth colour. */
.chip-navy{background:var(--c-navy);color:var(--navy-800)}
.chip-lg{width:52px;height:52px;border-radius:16px}
.chip-lg .ic{width:24px;height:24px}
.chip-sm{width:28px;height:28px;border-radius:9px;flex:0 0 28px}
.chip-sm .ic{width:15px;height:15px}
/* .chip-xs - the tile that sits INSIDE a line of text: beside a KPI label, at
   the head of a list row, in front of a create button's words. Added for Â§3a,
   which is where the pastel actually goes - image 1's four create actions, the
   "what kind of figure" mark beside a KPI label, and image 7's per-group dot
   are all this one object at this one size. `vertical-align:-4px` because it is
   set in running text, not in a flex row. */
.chip-xs{width:20px;height:20px;border-radius:6px;flex:0 0 20px;box-shadow:none;
  vertical-align:-4px}
.chip-xs .ic{width:12px;height:12px}
/* ---------- the two glyphs that ride INSIDE a chip ----------
   `.fmark` is the document-type file mark (core/_rowmark.html), `.sc-folder`
   the site identity folder (core/_sitemark.html). Both are VENDORED artwork on
   their own viewBox, not sprite icons, so they are sized here rather than
   carrying `.ic` - `.ic` is the sprite's contract and these are not in it.
   `> ` and not a descendant combinator: (0,2,0) either way, so the only thing
   that decides between `.chip-xs .fmark` and a later rule is source order, and
   a child selector at least says out loud that the chip is the parent.
   The sizes are the same fraction of the tile `.chip .ic` uses (18/36), which
   is what keeps a file mark and a KPI icon looking like the same object.
   REDESIGN_SPEC 3a, "tint the ground, never the text": nothing here touches a
   foreground, so no contrast floor and no D-123 gap moves. */
.chip > .fmark,.chip > .sc-folder{display:block;margin:0;width:18px;height:18px}
/* SOURCE ORDER IS THE ONLY TIE-BREAK HERE. `.chip-xs > .fmark` and
   `.chip > .fmark` are both (0,2,0), so the size variants MUST follow the base
   rule above; swapping these three lines silently puts an 18px glyph in a 20px
   tile. Checked, not assumed - this file has 1,400 rules. */
.chip-sm > .fmark,.chip-sm > .sc-folder{width:16px;height:16px}
.chip-xs > .fmark,.chip-xs > .sc-folder{width:13px;height:13px}
/* The folder file carries a hardcoded fill (#5b8def) - that is the ONLY thing
   separating the four supplied folders, and it is exactly what had to go: the
   fourth file is folder-dark, a near-black card among pastel siblings. A CSS
   declaration beats a presentation attribute, so this repaints the owner's
   shape in the chip's own darker same-hue ink and lets one drawing serve all
   seven identities. The file marks are NOT repainted - their brand fills are
   the "darker same-hue glyph" 3a asks for, and flattening a mark whose letter
   is a second path would turn a "W" into a solid square. */
.chip > .sc-folder path,.chip > .sc-folder circle,.chip > .sc-folder rect{fill:currentColor}
/* the mark sits in running text in `.rl-id`; the gap in a `.btn` is the button's
   own flex gap, so this must not be a blanket margin on `.chip-xs`. */
.rl-id > .chip{margin-right:8px}

/* ============ DOCUMENT MARK + SITE CARDS  (added 2026-08-30) ============
   The SECOND intentional departure of this file from the owner-approved mockup;
   the first was D-089. Requested by the owner after a knowledge-base UI:
   coloured "folder" cards across the top and a per-file-type icon on every row.
   Built as STRUCTURE, not as that styling. CLAUDE.md 1.4 rules the reference's
   3D illustrations, gradients and purple out ("no purple gradients, no
   glassmorphism, no decorative motion"), and D-038 rules out fetching
   illustration assets from a CDN, so raster art was never available anyway.
   Both blocks below therefore reuse the tint, ring and elevation vocabulary
   already defined above and introduce NO new colour value. */

/* ---------- the per-row document mark ----------
   The .dmark tile that lived here is GONE (D-114). It was a saturated coloured
   box carrying the GST series letters, and it was replaced by the owner's own
   vscode-icons file marks - one per document type - after he asked for them
   three times. The markup now lives in ONE partial, app/templates/core/
   _rowmark.html, included by both the documents list and search: the tile was
   pure CSS, where a single rule kept two screens honest, but an inline SVG is
   markup and two copies of markup drift (L-20).
   Removed rather than left behind because this change orphaned it and nothing in
   any template referenced it any more - verified by grep across app/ before
   deleting. Dead CSS carrying a long rationale is worse than none: the next
   reader assumes it is live and designs around it. The rationale that is still
   true - that a per-row type mark is INFORMATION rather than the decorative
   "one chip per row" the owner rejected in session 1 - now sits in the partial,
   next to the thing it explains. */
/* ---------- .sitecards - the card row at the top of Documents ----------
   The reference's "folders" are containers. Ours is the SITE: D-071 records
   that the site is "the axis the company intends to measure itself on", and
   every document already carries site_id. Flat tinted cards on the existing
   --sh elevation, the same one .kpi uses. No illustration, no gradient.
   WRAPPING FLEX, NOT A GRID, and deliberately: a grid needs a declared track
   count, and a declared track count that disagrees with the number of cards is
   exactly D-089 / lesson L-03 - a row occupying 4/5 of the width with a dead
   column. Flex has no track count to drift, so the view's bound can change
   with no CSS edit at all. (auto-fit + minmax would also work in a browser but
   renders as a single column under WeasyPrint, so the offline capture could
   never show the owner what the row looks like.) */
.sitecards{display:flex;flex-wrap:wrap;gap:var(--s3);margin-bottom:var(--s5)}
.sitecard{display:flex;flex-direction:column;gap:2px;background:var(--paper);
  border:1px solid var(--line);
  border-radius:var(--r-md);padding:var(--s4);box-shadow:var(--sh);color:var(--ink);
  flex:1 1 168px;min-width:0;max-width:100%;
  transition:background var(--dur) var(--ease),box-shadow var(--dur) var(--ease)}
.sitecard:hover{background:var(--hover);color:var(--ink);border-color:var(--line-2)}
/* sc- prefixes, not .n/.c/.k: a bare .c would collide with the global
   .c{text-align:center} utility defined at the top of this file. */
.sc-name{font-size:var(--t-sm);font-weight:550;letter-spacing:-.01em;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sc-client{font-size:var(--t-xs);color:var(--ink-3);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sc-count{font-size:var(--t-xs);color:var(--ink-3);margin-top:var(--s3);
  font-variant-numeric:tabular-nums}
.sc-count b{font-size:var(--t-lg);font-weight:600;color:var(--ink);
  letter-spacing:-.02em;margin-right:5px}
/* the card whose filter is currently applied, so a filtered list can never look
   like an unfiltered one - the same reasoning as .fsel.on above */
.sitecard[aria-current="page"]{background:var(--c-blue);
  box-shadow:0 0 0 1px rgba(0,96,156,.28),var(--sh-btn)}
/* the overflow card: the row is bounded, and this is where the rest live */
.sitecard-more{justify-content:center;background:var(--sunken);box-shadow:none;
  color:var(--ink-2);font-size:var(--t-sm);font-weight:500}
.sitecard-more:hover{background:var(--hover);color:var(--ink);box-shadow:none}
/* The folder, fourth attempt, and this one is the owner's own artwork rather
   than another guess at an icon set - four supplied files, vendored unaltered
   (app/static/icons/README.md). Flat 2D, no gradient, no glass (CLAUDE.md 1.4).
   34px: a filled shape carries at a smaller size than an outline, and this is
   the size the previous two-layer folder occupied, so the card's height and the
   rest of the row do not move with this change. */
/* SUPERSEDED (2026-09-01, REDESIGN_SPEC 3a) - `.sc-folder{width:34px;
   margin-bottom:--s2}`, and the note that said the four folder colours "are NOT
   here, and cannot be", because each was a hardcoded fill inside its own
   supplied file. That is precisely what made every fourth site card draw
   folder-dark, a near-black folder among teal, blue and violet siblings, which
   is the rendered defect this wave was called to fix. The colour is now a
   `.chip-<hue>` GROUND chosen by site id over the seven `--c-*` pastels, the
   fill is overridden to the chip's ink (see `.chip > .sc-folder path` above),
   and the geometry belongs to the chip. The mark keeps the owner's drawing.
   The margin moves to the wrapper, because it is now the chip that occupies
   the card's first line and the folder is 18px inside a 28px tile. */
.sitecard > .chip{margin-bottom:var(--s2)}

/* The PDF mark - vscode-icons, MIT, vendored. Used ONLY where a real PDF file
   exists: the Download-PDF control and the retained-PDF list. It is NOT the
   document-row mark; that tile carries the GST series (SER / TRG / VA / PI),
   and an identical red mark on every row would replace information with none.
   Sized to the cap height of the button label beside it, and nudged to the text
   baseline - an icon centred on the line box rides visibly high next to caps. */
.pdf-mark{display:inline-block;width:16px;height:16px;vertical-align:-3px;
  margin-right:7px}

/* ---------- .backlink - "go to the previous or required screen" ----------
   Rendered by base.html on every screen whose route HAS a parent, and by
   nothing on the seven top-level screens (see app/nav.py BACK).
   A NAMED destination, not a bare arrow: "Documents" tells you where it goes.
   Ghost-button geometry, not a .btn: this is navigation the eye should find
   when it looks for it and ignore otherwise, so it gets no fill and no ring
   until hover. The negative margin pulls the hover pad's left edge back to the
   text's optical edge, so the label lines up with the h1 below it. */
.backlink{display:inline-flex;align-items:center;gap:6px;margin:0 0 var(--s3) -8px;
  padding:5px 11px 5px 8px;border-radius:var(--r-pill);
  font-size:var(--t-sm);font-weight:500;color:var(--ink-3);
  transition:background var(--dur) var(--ease),color var(--dur) var(--ease)}
.backlink:hover{background:var(--hover);color:var(--navy-800)}
.backlink svg{display:block;width:15px;height:15px;flex:0 0 15px}
@media(max-width:760px){
  /* On a phone this IS the way back - the sidebar is gone and the bottom bar
     only reaches the four top-level screens. So it stops being a quiet ghost
     and becomes a real, thumb-sized target on its own ground. */
  .backlink{font-size:var(--t-md);padding:9px 14px 9px 11px;margin-bottom:var(--s4);
    background:var(--sunken);color:var(--ink-2)}
  .backlink svg{width:17px;height:17px;flex:0 0 17px}
}

/* ===================== PILLS ===================== */
/* A rounded RECTANGLE, not a lozenge (REDESIGN_SPEC Â§3). The references keep
   the pill shape for buttons and reserve a small soft-cornered tag for status,
   so the two never read as the same control. Tinted ground, darker text, no
   border. The five colour pairs are untouched - this spec adds no sixth. */
.pill{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;font-weight:500;
  padding:2px 8px;border-radius:var(--r-sm);white-space:nowrap;
  background:var(--neutral-bg);color:var(--neutral-fg);letter-spacing:0;line-height:1.5}
.pill::before{content:"";width:5px;height:5px;border-radius:var(--r-pill);background:currentColor;
  flex:0 0 5px;opacity:.8}
.pill-ok{background:var(--ok-bg);color:var(--ok-fg)}
.pill-warn{background:var(--warn-bg);color:var(--warn-fg)}
.pill-bad{background:var(--bad-bg);color:var(--bad-fg)}
.pill-info{background:var(--info-bg);color:var(--info-fg)}
/* .pill with no variant is already neutral. This names it explicitly so a status
   map can carry all five states uniformly instead of one entry being an empty
   string - the expense workflow has five and a blank looked like an omission. */
.pill-neutral{background:var(--neutral-bg);color:var(--neutral-fg)}
.pill-plain::before{display:none}

/* ===================== WORKFLOW STRIP =====================
   .wf / .wf-step - where a claim has got to AND what happened at each point,
   in one control rather than two. This card used to draw the strip and then
   repeat the whole trail as a flat feed underneath it, so every event was on
   the screen twice; the feed is gone and each step now opens onto its own
   events. That is why the strip is a COLUMN and no longer a row of four bands:
   a step has to be able to open onto something, and a disclosure panel inside
   a flex row would stretch one column and shear the bar it belonged to.

   NO SIXTH COLOUR FAMILY, and this is still the constraint the shape is chosen
   around. The five status pills ARE the app's status language, so three
   progress states borrow three of them:
     .is-done    -> --ok      (cleared)
     .is-current -> --info    (here now)
     .is-todo    -> --neutral (not reached)
   .is-todo is named explicitly even though it is nearly the bare .wf-step, for
   the reason .pill-neutral is: a status map carrying five steps uniformly must
   not have one entry that is an empty string.

   ONLY THE CURRENT STEP IS FILLED. Four full-width tinted bands stacked on one
   another is a heavier object than the information in it, and the owner has
   called this control ugly once already. So the row itself is quiet - label in
   --ink, a date in mono on the right - and the STATE is carried by a 7px dot in
   the step's own colour: filled for cleared, filled and ringed for here-now,
   HOLLOW for not-reached. The current step alone takes a tint, a 1px inset ring
   and 600 weight, so it is the one thing on the card that the eye lands on, and
   it is not distinguishable by colour alone.

   The caret is an AFFORDANCE, not an ornament: it appears only on steps that
   are `<details>` - a step with no events is a plain row and gets none - and it
   rotates to say open/closed. No transition on it: CLAUDE.md 1.4 rules out
   decorative motion, and a disclosure arrow that eases is exactly that.

   44px on pointer, 48px on touch. A step is now a tap target, and the owner has
   raised sub-40px targets twice.

   .wf-log's left hairline ties an open panel to the step it belongs to; without
   it the events float in the card as a second, unattached list - which is the
   thing this change existed to remove. It is offset to sit under the step's own
   dot, so the rail, the dot and the label all share one axis.

   .wf-why is the reason a claim was sent back. width:fit-content is a RENDERED
   finding: at full width a 46-character sentence became a 1228px amber stripe
   across the card, which reads as an alert bar rather than as somebody's note.
   --warn, not a new colour: a reason only ever exists on a Sent-back event, and
   --warn is the pair .pill-warn already uses for exactly that state. */
.wf{list-style:none;margin:0;padding:0}
.wf-row+.wf-row{margin-top:2px}
.wf-step{display:flex;align-items:center;gap:9px;min-height:44px;
  padding:8px var(--s4);border-radius:var(--r-sm);
  font-size:var(--t-sm);font-weight:500;line-height:1.35;color:var(--ink);
  --wf-dot:var(--neutral-fg)}
.wf-step::before{content:"";width:7px;height:7px;border-radius:var(--r-pill);
  background:var(--wf-dot);flex:0 0 7px}
.wf-meta{margin-left:auto;display:flex;align-items:center;gap:10px;
  font-size:var(--t-xs);color:var(--ink-4);white-space:nowrap}
.wf-when{font-family:var(--mono);font-variant-numeric:tabular-nums}
summary.wf-step{cursor:pointer;list-style:none;
  -webkit-tap-highlight-color:transparent}
summary.wf-step::-webkit-details-marker{display:none}
.wf-d>.wf-step:not(.is-current):hover{background:var(--hover)}
.wf-d>.wf-step::after{content:"";width:6px;height:6px;flex:0 0 6px;margin-right:3px;
  border-right:1.6px solid currentColor;border-bottom:1.6px solid currentColor;
  transform:rotate(-45deg);opacity:.45}
.wf-d[open]>.wf-step::after{transform:rotate(45deg) translateY(-1px);margin-right:5px}
.wf-step.is-done{--wf-dot:var(--ok-fg)}
.wf-step.is-current{--wf-dot:var(--info-fg);background:var(--info-bg);
  color:var(--info-fg);font-weight:600;box-shadow:inset 0 0 0 1px var(--info-fg)}
.wf-step.is-current .wf-meta{color:var(--info-fg);opacity:.8}
.wf-step.is-current.is-returned{--wf-dot:var(--warn-fg);background:var(--warn-bg);
  color:var(--warn-fg);box-shadow:inset 0 0 0 1px var(--warn-fg)}
.wf-step.is-current.is-returned .wf-meta{color:var(--warn-fg)}
.wf-step.is-todo{color:var(--ink-4)}
.wf-step.is-todo::before{background:transparent;
  box-shadow:inset 0 0 0 1.5px var(--ink-4)}
.wf-log{margin:0 0 0 17px;padding:2px 0 var(--s3) 12px;border-left:1px solid var(--line)}
.wf-ev{margin:0;padding:8px 0 0;font-size:var(--t-sm);color:var(--ink-2)}
.wf-ev b{font-weight:550;color:var(--ink)}
.wf-stamp{font-family:var(--mono);font-size:var(--t-xs);color:var(--ink-4);
  font-variant-numeric:tabular-nums;margin-left:5px}
.wf-why{display:block;width:fit-content;max-width:100%;margin-top:5px;
  padding:7px 11px;border-radius:var(--r-sm);
  background:var(--warn-bg);color:var(--warn-fg);font-size:var(--t-xs);
  line-height:1.5}
@media(max-width:760px){
  .wf-step{min-height:48px;padding-inline:var(--s3)}
  .wf-log{margin-left:15px;padding-left:11px}
}

/* ===================== TABS / TOOLBAR ===================== */
/* UNDERLINE tabs (REDESIGN_SPEC Â§3). The reference shows both a pill form and
   an underline form; this app already spends the pill idiom on its year and
   month pickers, so a second pill row beside them would be two controls that
   look identical and mean different things. The strip's own hairline is what
   the 2px marker sits on, which is why .tab carries a -1px bottom margin.
   Gap is 20px, not the spec's 10px: with the tab's horizontal padding removed
   (an underline must be the width of its label, not of a box) 10px puts two
   labels 10px apart, which reads as one wrapped phrase. */
.tabs{display:flex;gap:var(--s6);margin-bottom:var(--s5);
  border-bottom:1px solid var(--line)}
.tab{padding:0 0 10px;margin-bottom:-1px;font-size:13.5px;font-weight:500;color:var(--ink-3);
  border-radius:0;border-bottom:2px solid transparent;
  transition:color var(--dur) var(--ease),border-color var(--dur) var(--ease)}
.tab:hover{background:transparent;color:var(--ink);box-shadow:none}
.tab[aria-current="page"],.tab[aria-selected="true"]{background:transparent;color:var(--ink);
  font-weight:550;border-bottom-color:var(--ink);box-shadow:none}
.tab .cnt{font-family:var(--mono);font-size:11px;color:var(--ink-4);margin-left:6px;
  font-weight:400}
.toolbar{display:flex;gap:var(--s2);align-items:center;flex-wrap:wrap;margin-bottom:var(--s4)}
.toolbar .grow{min-width:180px}
.toolbar .spacer{flex:1}
/* a hairline between the search side and the filter side */
.toolbar .div{width:1px;height:20px;background:var(--line-2);margin:0 var(--s1)}
.filter-note{font-size:var(--t-xs);color:var(--ink-4);margin:0 0 var(--s3)}
.filter-note b{color:var(--ink-2);font-weight:500}

/* ---------- search field with a leading icon ---------- */
.search-field{position:relative;flex:0 1 300px;min-width:200px;display:flex}
.search-field > .ic{position:absolute;left:12px;top:50%;transform:translateY(-50%);
  width:15px;height:15px;color:var(--ink-4);pointer-events:none}
.search-field .input{padding-left:35px}

/* ---------- filter select ----------
   A native <select> renders the OS chevron and sits like a form field. These are toolbar
   CONTROLS, so they get a button's shape, our own chevron, and a visible active state. */
.fsel{position:relative;display:inline-flex}
.fsel > select{appearance:none;-webkit-appearance:none;height:var(--ctl-h);padding:0 32px 0 12px;
  border:1px solid var(--line);border-radius:var(--r-sm);background:var(--paper);
  color:var(--ink-2);
  font:inherit;font-size:13.5px;font-weight:450;cursor:pointer;width:auto;
  box-shadow:none;
  transition:background var(--dur) var(--ease),border-color var(--dur) var(--ease)}
.fsel > select:hover{background:var(--hover);border-color:var(--line-2)}
.fsel > select:focus{outline:none;border-color:var(--blue-500);box-shadow:var(--ring)}
/* top:15px, not 14px: the control is --ctl-h (38px), not the spec's 36px. */
.fsel::after{content:"";position:absolute;right:13px;top:15px;width:6px;height:6px;
  border-right:1.6px solid var(--ink-3);border-bottom:1.6px solid var(--ink-3);
  transform:rotate(45deg);pointer-events:none}
/* .fsel.on = a filter is actually applied. Without this there is no way to tell a filtered
   list from an unfiltered one except by reading every dropdown.
   2026-09-01: --tint and --ink, not a blue wash. The references mark an active
   filter by GROUND and WEIGHT, and keep the whole toolbar colourless. */
.fsel.on > select{background:var(--tint);color:var(--ink);font-weight:500;
  border-color:var(--line-2)}
.fsel.on::after{border-color:var(--ink)}
.fsel .fkey{position:absolute;left:12px;top:50%;transform:translateY(-50%);font-size:var(--t-sm);
  color:var(--ink-4);pointer-events:none}
/* `.fkey` shipped with no padding compensation, so the leading icon OVERLAPPED
   the first letter of its own value: the key sits at left:12px and is 14px
   wide, so its right edge lands 26px in, while `.fsel > select` starts its text
   at 12px. Measured at 1536 and 390. Pre-existing - the control could never
   have been used as intended by anyone. The pad is on `.fsel-ico`, the class
   the four reports selects actually carry, so a plain `.fsel` keeps its 12px. */
.fsel-ico > select{padding-left:33px}
/* ---------- .fsel-cap - a filter whose OPTIONS ARE USER DATA ----------
   `.fsel > select` is `width:auto`, so a native select sizes itself to its
   WIDEST OPTION. That is right for every filter whose options this codebase
   writes - "All types", "Issued", "FY 2026-27" are all short and known. It is
   wrong the moment the options are a list of site names, because a site name is
   typed by a user and has no bounded width.
   MEASURED at 390px on /reports: the site filter rendered 449px wide inside a
   380px page and pushed the WHOLE DOCUMENT into a horizontal scroll - 380px of
   client width against 463px of scroll width. No other element on the page was
   over-wide; it was this select alone.
   One new token rather than a change to `.fsel`, deliberately: capping every
   filter in the app would reach the documents, employees and payments toolbars,
   whose options are short strings that are supposed to size their control.
   Applied only where the option text is somebody's data. */
.fsel-cap{max-width:100%;min-width:0}
.fsel-cap > select{max-width:100%;text-overflow:ellipsis}

/* ===================== FORM ===================== */
.field{display:flex;flex-direction:column;gap:6px}
.field > label,.lbl{font-size:var(--t-xs);font-weight:500;color:var(--ink-2)}
.hint{font-size:var(--t-xs);color:var(--ink-4)}
.req{color:var(--orange-700)}
.input,.select,.textarea{height:var(--ctl-h);border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:0 12px;font:inherit;font-size:13.5px;background:var(--paper);color:var(--ink);
  width:100%;box-shadow:none;
  transition:border-color var(--dur) var(--ease),box-shadow var(--dur) var(--ease)}
.textarea{height:auto;padding:10px 12px;line-height:1.6;resize:vertical}
/* A <select> was rendering TWO different ways in one product: the toolbar
   .fsel got the app's own chevron, while a plain .select on every form kept
   appearance:auto and drew the native OS triangle. Same control, two marks.
   A <select> cannot carry ::after, so the chevron is a background image rather
   than the border trick .fsel uses - it is the same 45deg-rotated corner, drawn
   at the same 6px, so the two read as one shape. The stroke colour is written
   out because a CSS variable cannot be interpolated into a data URI; it is
   --ink-3, and it moves when --ink-3 does. background-image, not background,
   so `background:var(--paper)` above still supplies the fill. */
.select{appearance:none;-webkit-appearance:none;padding-right:32px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7' viewBox='0 0 10 7'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23687078' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 12px center}
.input:hover,.select:hover,.textarea:hover{border-color:var(--line-2)}
.input:focus,.select:focus,.textarea:focus{outline:none;
  border-color:var(--blue-500);box-shadow:var(--ring)}
.input:disabled,.input[readonly]{background:var(--sunken);color:var(--ink-3);
  border-color:var(--line)}
.input.money-in{text-align:right;font-variant-numeric:tabular-nums}
.input-file{border:1px dashed var(--line-2);border-radius:var(--r-sm);padding:10px 12px;
  font:inherit;font-size:var(--t-sm);background:var(--sunken);width:100%;color:var(--ink-2)}
.input-file:focus{outline:none;border-color:var(--blue-500);box-shadow:var(--ring)}
.form-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--s5)}
.form-grid.g3{grid-template-columns:repeat(3,1fr)}
/* The document band: ONE horizontal strip of compact labelled cells - number,
   type, currency, line of business, dates, terms, place of supply, reference -
   which is what the owner asked for and what the three separate stacked grids
   it replaced were not. auto-fit rather than a fixed column count so the strip
   fills whatever width it is given and wraps by itself instead of needing a
   media query per breakpoint. 128px is not a taste: it is the largest minimum
   that still lets all EIGHT compact cells share one line at the width this
   card actually has (1168px at a 1536px window, measured in Chrome), and it is
   above the ~120px a Chrome `type=date` needs for its dd-mm-yyyy mask and its
   picker glyph. Subject is the ninth and takes the row below it whole - a
   200-character sentence in a 143px box is not a band, it is a joke. */
.form-band{display:grid;grid-template-columns:repeat(auto-fit,minmax(128px,1fr));
  gap:var(--s4)}
.form-band .band-wide{grid-column:1/-1}
/* Two tracks for the number, because it is the longest fixed string on the
   band and the only one that may not be truncated. `span 2` and not `1/3`: it
   must degrade to a single column when the band has folded to one, which it
   does - a span wider than the grid is clamped, an explicit line range is not. */
.form-band .band-num{grid-column:span 2}
.derived-line{margin:var(--s4) 0 0}
.disc > summary{cursor:pointer}

/* ---- inline line items on the create form (S3-T4) ----
   One label row, then bare rows. A labelled .field per cell would make a
   three-line document taller than the screen, which is the complaint this
   screen exists to answer. On a phone the label row goes and each cell
   carries its own label instead - the same markup, read two ways. */
.lines{display:flex;flex-direction:column;gap:var(--s3)}
/* NINE columns now. Eight of them were already one control each; the ninth is
   the sub-caption, which used to be a second input STACKED under the
   description. Measured in Chrome at 1536px before the change: the row was
   120px tall against a 38px control - three control-heights - which is why the
   owner kept reporting that a line item does not read as one line. Giving the
   sub-caption its own track is what makes the row one line; nothing else on it
   was ever stacked.
   The computed Amount sits between Rate and the row's own Remove, which is
   where a spreadsheet puts it and where the owner's reference puts it. The two
   number columns are the same width so Rate and Amount line up as one
   right-aligned pair of figures rather than two ragged ones. The two text
   columns are the only flexible ones and the description keeps the larger
   share of what is left. */
.lrow{display:grid;gap:var(--s3);align-items:start;
  grid-template-columns:116px minmax(0,1.2fr) minmax(0,.8fr) 78px 76px 88px 106px 106px 74px}
/* The expense line editor reuses .lrow but not its columns: a spend line is a
   date, a category, a description, an amount, a bill and a Remove - not a SAC,
   a quantity and a rate. SIX cells against .lrow's eight tracks left the file
   control in a 96px track and two tracks empty at the right. Only the track
   list and the file control differ. Placed after .lrow and before the
   max-width:1023px rule below, so `.lbody{grid-template-columns:1fr}` still
   wins on a phone. */
.lrow-exp{grid-template-columns:148px 160px minmax(0,1fr) 110px 190px 88px}
.lrow-exp .input-file{height:var(--ctl-h);padding:9px 10px;font-size:var(--t-xs)}

/* The attendance day sheet reuses .lrow but not its columns: a roster row is a
   person, a present/absent, a kind of work, two clock times and a note - not a
   SAC, a quantity and a rate. SIX cells against .lrow's NINE tracks left three
   empty. The person's name takes the larger flexible track because it is the
   row's identity; the two time inputs are the SAME width for the reason Rate
   and Amount are, so From and To read as one pair. Placed after .lrow-exp and
   before the max-width:1023px rule below, so `.lbody{grid-template-columns:1fr}`
   still wins on a phone - MEASURED: appended after that rule instead, the row
   stays six tracks at 390px and two of them compute to 0px. */
.lrow-att{grid-template-columns:minmax(0,1.3fr) 132px 132px 108px 108px minmax(0,1.6fr)}
.lcell{display:flex;flex-direction:column;gap:6px;min-width:0}
.lbody .lbl{display:none}
/* `.lsub` and `.sublbl` lived here to label a sub-caption input stacked inside
   the description cell. The sub-caption is a column of its own now, named by
   the header row like every other column, so both rules were orphaned by that
   change and are removed with it. */
/* Derived, so it is not a control: it takes the control height so it sits on
   the same baseline as the inputs beside it, and the sunken fill is the same
   one .input[readonly] already uses, so "you do not type here" is said the way
   the rest of the app says it. */
.lamt{display:flex;align-items:center;justify-content:flex-end;
  height:var(--ctl-h);padding:0 12px;border-radius:var(--r-sm);
  background:var(--sunken);color:var(--ink-2);font-size:var(--t-sm);
  box-shadow:0 0 0 1px var(--line);min-width:0;overflow:hidden}
.lerr{grid-column:1/-1;margin-top:2px}
.lines-foot{display:flex;align-items:center;gap:var(--s3);margin-top:var(--s4);flex-wrap:wrap}
/* "Add another line" was a .btn, which gave a routine, repeated action the
   same weight as "Save quotation" two inches below it. The owner's reference
   has a quiet text link with a small ringed plus, and that is the right
   weight. Still a real submit carrying action=add_row, so the no-JavaScript
   fallback (D-081) is untouched - only its clothes changed. */
.line-add{display:inline-flex;align-items:center;gap:8px;background:none;border:0;
  padding:0;font:inherit;font-size:var(--t-sm);font-weight:500;color:var(--blue-600);
  cursor:pointer;transition:color var(--dur) var(--ease)}
/* --blue-600, not --blue-500: 500 is the focus-ring blue and does not clear AA
   as text on paper. 600 is the colour every other link in the app uses. */
.line-add-mark{display:inline-flex;align-items:center;justify-content:center;
  width:17px;height:17px;flex:0 0 17px;border-radius:50%;
  box-shadow:0 0 0 1px currentColor;font-size:12px;line-height:1}
.line-add:hover{color:var(--navy-800)}
.line-add:focus-visible{outline:none;border-radius:var(--r-sm);box-shadow:var(--ring)}
@media(max-width:1023px){
  .lhead{display:none}
  .lbody{grid-template-columns:1fr;gap:var(--s4);padding:var(--s4);
    border:1px solid var(--line);border-radius:var(--r-sm);background:var(--sunken)}
  .lbody .lbl{display:block}
  /* Inside the sunken card the sunken amount cell would be invisible, so on a
     phone it reads on paper instead. */
  .lamt{background:var(--paper)}
}
/* The party card. The document's counterparty is the first thing anybody looks
   for on an invoice and it should not be one dropdown among six - the owner's
   reference gives it a card of its own under the page title. */
.pcard{border:1px solid var(--line);border-radius:var(--r-md);padding:var(--s5);
  background:var(--sunken)}
.pcard-lbl{display:block;font-size:var(--t-2xs);font-weight:600;letter-spacing:.06em;
  text-transform:uppercase;color:var(--ink-3);margin-bottom:var(--s4)}
/* The client and site pickers, INSIDE the To card. They used to sit in a grid
   of their own below the band, so the card announced "choose one below" and the
   control you had to touch was somewhere else on the page. Choosing now happens
   where the customer is shown. A hairline above them separates the resolved
   party from the editor that produces it without adding a second card. */
.pcard-pick{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s4);
  margin-top:var(--s5);padding-top:var(--s5);border-top:1px solid var(--line)}
/* The client select and the control that clears it, as ONE control. `.field` is
   a column flex, so the button needs its own row wrapper or it drops beneath the
   select and reads as a separate action. `min-width:0` on the select is what
   stops a long client name from pushing the button out of the card - a grid/flex
   child's default `min-width:auto` refuses to shrink below its content.
   The button is `hidden` until a client is chosen, so this row is a bare select
   for as long as there is nothing to clear. */
.pick-ctl{display:flex;align-items:center;gap:var(--s2)}
.pick-ctl > .select{flex:1 1 auto;min-width:0}
/* "Edit customer info" - a disclosure in place, not navigation and not a
   modal. It reveals the per-document address override right under the card it
   rewrites. */
.pcard-ov{margin-top:var(--s4)}
.pcard-ov > summary{color:var(--blue-600);font-weight:500}
.pcard-ov-note{margin:var(--s3) 0 0}
.pcard-ovgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  gap:var(--s4);margin-top:var(--s4)}
/* The To card grows when the pickers and the override open; a From card
   stretched to match would be a tall grey box holding four lines of address. */
.form-grid > .pcard{align-self:start}
@media(max-width:719px){
  .pcard-pick,.pcard-ovgrid{grid-template-columns:1fr}
}
/* The card-foot is space-between; a note between the left link and the right
   buttons would otherwise be pushed into the middle of the row. */
.card-foot > .foot-note{margin-right:auto;margin-left:var(--s4)}
.form-grid .span2{grid-column:span 2}
.form-sec{padding:var(--s6);border-bottom:1px solid var(--line)}
.form-sec:last-child{border-bottom:0}
.form-sec > h3{font-size:var(--t-sm);font-weight:550;color:var(--ink);margin:0 0 var(--s4);
  letter-spacing:-.01em}
.check{display:flex;align-items:center;gap:9px;font-size:var(--t-sm);min-height:24px}
.check input{width:18px;height:18px;accent-color:var(--ink);flex:0 0 18px}

/* ===================== RECORD LIST ===================== */
.rlist{list-style:none;margin:0;padding:0}
/* padding-block 14px > 10px in the density pass, and this one rule is the
   single largest lever in the file: the documents list is the tallest screen in
   the product and it is N copies of this row. The row's content is 24px, so the
   row goes 52px > 44px - a 15% cut on the whole list, repeated per row.
   Not taken lower: 44px is still above the 40px a whole-row tap target needs,
   and on a phone this row is three lines, so it is far above it there. */
.rlist-head,.rlist-row{display:grid;align-items:center;gap:var(--s4);
  grid-template-columns:var(--cols,180px 1fr 96px 90px 108px 120px);padding:8px var(--s6)}
/* Needs a real edge. Without it the column labels and the first data row sit 10px apart with
   no rule and no tint, which reads as a broken first row rather than a header. */
/* sticky so the column labels survive a long list â€” it sits just under the top bar */
/* NOT position:sticky, and this is a bug fix rather than a preference.
   `.card` is `overflow:hidden` (see .card above), which makes every card its own
   scroll container. A sticky child then resolves `top` against THE CARD, not the
   viewport - so `top:var(--topbar-h)` shoved this header 60px down from the card's
   top edge at zero scroll, leaving an empty white band above it and HIDING THE
   FIRST ROW OF EVERY LIST behind it. Worst case seen: /clients?archived=1 read
   "Showing 1 of 1" with nothing on screen at all.
   And the stickiness bought nothing even when it worked, because a card with
   overflow:hidden can never scroll. Found by an agent measuring a DIFFERENT
   complaint (L-26). */
.rlist-head{font-size:var(--t-xs);color:var(--ink-3);font-weight:500;padding-block:9px;
  background:var(--sunken);border-bottom:1px solid var(--line)}
.card-body.tight > .rlist-head:first-child{border-radius:var(--r-md) var(--r-md) 0 0}
.rlist-row{border-top:1px solid var(--line);color:var(--ink);position:relative;
  transition:background var(--dur) var(--ease)}
/* a 2px accent that arrives on hover â€” tells you which row you are on without a border
   that is always there taking up attention */
.rlist-row::before{content:"";position:absolute;left:0;top:0;bottom:0;width:2px;
  background:transparent;transition:background var(--dur) var(--ease)}
/* 2026-09-01: --ink-4, not --blue-500. Not one of the six references marks a
   hovered row with a colour; the hover ground already says which row you are
   on, and a blue rail arriving on every row of a 79-row list is the kind of
   accent this retheme is spending less of. Kept rather than deleted because the
   rail is the only hover cue a colour-blind reader gets on a --hover ground
   that is 1.5% off white. */
.rlist-row:hover::before{background:var(--ink-4)}
/* .rlist-head is a sibling of the <ul>, not a child, so `.rlist-head + li` could never match.
   The head now carries its own bottom border and the first row drops its top border. */
.rlist > li:first-child .rlist-row{border-top:0}
.rlist-row:hover{background:var(--hover);color:var(--ink)}
.rl-id{font-family:var(--mono);font-size:var(--t-sm);font-weight:450}
.rl-main{font-weight:500;overflow:hidden;text-overflow:ellipsis;letter-spacing:-.01em}
.rl-meta{font-size:var(--t-sm);color:var(--ink-3)}
/* .rl-main has overflow:hidden + text-overflow:ellipsis but no white-space:
   nowrap, so the ellipsis is inert and the cell WRAPS. Chrome takes a break
   opportunity after a hyphen, which split an ISO date down the middle:
   "Proforma Invoice - 2026-08-" / "14" on the documents list. Measured, not
   inferred - that row's .rl-main is 48px tall where every other row is 24px.
   Fixed by making the meta one unbreakable unit, so the wrap falls at the
   space between the client name and it. NOT by adding nowrap to .rl-main: that
   would activate the ellipsis and eat the end of the date instead, which is
   the same information loss in a tidier shape. The meta ellipsises on its own
   only if it is genuinely wider than the column, so nothing is ever clipped
   silently. */
.rl-main > .rl-meta{display:inline-block;max-width:100%;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis;vertical-align:bottom}
.rl-amt{font-variant-numeric:tabular-nums;text-align:right;font-weight:550}
.rl-metawrap{display:flex;gap:8px;align-items:center;font-size:var(--t-sm);color:var(--ink-3);
  flex-wrap:wrap}
.rl-metawrap .sep{color:var(--ink-4)}
@media(max-width:760px){
  .rlist-head{display:none}
  .rlist-row{grid-template-columns:1fr auto;
    grid-template-areas:"id amt" "main main" "meta status";row-gap:4px;padding:10px var(--s4)}
  .rl-id{grid-area:id}.rl-amt{grid-area:amt}.rl-main{grid-area:main}
  .rl-metawrap{grid-area:meta}.rl-status{grid-area:status}
  .rl-hideph{display:none}
}
/* The action cell on the master lists. Two create buttons plus Archive cannot
   share a phone line with the meta column, and letting meta wrap instead reads as
   a broken row. So on narrow it takes a full-width line of its own.
   Done by auto-placing THIS cell, not by editing the shared grid-template-areas
   above - .rl-status is also used by the documents, search, users and outstanding
   lists, where the cell is one small pill that must stay on the meta line. */
.rl-acts{display:flex;align-items:center;gap:var(--s2)}
/* An action cell that holds FORMS, not links - one control plus its own submit,
   twice (attendance/oncall.html). A <form> is a block, so each button fell
   below its control: measured at 1536px the select at y=340 and its button at
   y=378, orphaned. Each form becomes a flex row and control + button read as
   one unit. The controls shrink, the buttons never do - flex:1 1 auto with
   min-width:0 on the field only.
   ON A PHONE THE FORMS STACK, and that is a measurement too: side by side at
   390px the select collapsed to 74px and clipped "Which invoice...", the exact
   failure the note at oncall.html:49 already records at 300px. Stacked it is
   255px + Link then 237px + Record, no clipping, no horizontal scroll. */
.rl-answer form{display:flex;align-items:center;gap:var(--s2);min-width:0;flex:1 1 auto}
.rl-answer .select,.rl-answer .input{min-width:0;flex:1 1 auto}
@media(max-width:760px){
  .rl-answer{flex-wrap:wrap;row-gap:var(--s2)}
  .rl-answer form{flex:1 1 100%}
}
@media(max-width:760px){
  .rl-acts{grid-area:auto;grid-column:1/-1;margin-top:var(--s2)}
  /* A row whose actions are all conditional (an ended posting has none) would
     otherwise spend a full-width line plus its margin on nothing. The cell is
     still RENDERED so head-track count still equals row-cell count - that
     invariant is what L-03 costs us twice when it is broken. */
  .rl-acts:empty{display:none}
}
/* ---------- the laptop band, where six columns do not fit ----------
   A RECORDED DEFECT, not a new idea: the comment at the top of
   app/templates/core/documents.html measured the Action column being cut off by
   198px at 1024px, and could not fix it there because the fix belongs in this
   file. `.card` is `overflow:hidden`, so a row wider than its card is not
   scrolled - it is CLIPPED, silently, with no scrollbar to tell the reader that
   a column exists at all.

   THE ARITHMETIC, from this file's own tokens rather than from a guess. At
   >=1024px the row box is:
       viewport - 232 (--sidebar-w) - 52 (.content --s8 both sides)
                - 40 (.rlist-row --s6 both sides)  =  viewport - 324
   capped at 1188px once .content hits its 1280px max-width. The documents
   list's six floors sum to 850px and its five --s4 gaps to 70px, so it needs
   920px and therefore a 1244px viewport. Between 1024px and 1243px it is short,
   worst case by 220px at 1024. Above 1244 there is nothing to fix; below 1024
   the phone rule replaces the grid entirely. Hence the band, closed at both
   ends: a media query wider than its defect is a rule that will one day fire
   where nobody is watching.

   TWO PARTS, and only the first is automatic. The gap and the padding tighten
   for every list in the band - 18px back, free, and consistent with the density
   pass. That alone does NOT close a 220px deficit and is not claimed to: the
   second part is --cols-md, an OPTIONAL band-only track set that falls back to
   --cols and then to the same default, so every list that does not set it is
   byte-identically unaffected. The list that overflows declares its own tighter
   tracks next to the tracks it already declares, which is the same lever as
   --cols and the same reasoning - one number, in the template, beside the cells
   it counts.
   A blanket narrowing of every .rlist here was rejected: five of the six lists
   in the app fit at 1024 already, and narrowing a list that fits to fix one
   that does not is a regression paid by five screens. */
@media(min-width:1024px) and (max-width:1243px){
  .rlist-head,.rlist-row{gap:var(--s3);padding-inline:var(--s5);
    grid-template-columns:var(--cols-md,var(--cols,180px 1fr 96px 90px 108px 120px))}
}
/* The documents list is the only six-column .rlist, and below 1244 the sixth
   column has no room (the arithmetic is in the band above). One column is
   dropped rather than every column squeezed: "Raised by" is the only cell on
   that row that is neither the document's identity, its money, its state, nor
   a thing you can do to it. The class rides on BOTH the head span and the row
   cell, so five tracks still meet five cells (L-03).
   LOWER EDGE 761, NOT 1024, AND THAT IS MEASURED: with this hide firing at
   <=1243 but the --cols-md override only at >=1024, an innerWidth of 1023
   rendered five cells against six tracks - .rl-acts overflowed by 77.2px and
   the money column clipped by 23px. The two halves must share one boundary. */
@media(min-width:761px) and (max-width:1243px){
  .rl-hidemd{display:none}
  .rlist-head,.rlist-row{grid-template-columns:var(--cols-md,var(--cols,180px 1fr 96px 90px 108px 120px))}
}

/* ===================== TABLE ===================== */
.table{width:100%;border-collapse:collapse;font-size:var(--t-sm)}
/* Density pass: th 11>9, td 13>10. Same reasoning as .rlist-row - a table's
   height is its row padding times its row count, and the line-item table on a
   document is the second-tallest repeated block in the product. */
.table th{font-size:var(--t-xs);color:var(--ink-3);font-weight:500;text-align:left;
  background:var(--sunken);
  padding:9px var(--s4);background:var(--sunken);border-bottom:1px solid var(--line);
  white-space:nowrap}
.table td{padding:10px var(--s4);border-bottom:1px solid var(--line);vertical-align:top}
.table tbody tr:last-child td{border-bottom:0}
.table tbody tr:hover{background:var(--hover)}
.table .r,.table th.r{text-align:right}
.table .c,.table th.c{text-align:center}
.table-wrap{overflow-x:auto}

.li-group td{background:var(--sunken);font-weight:550;color:var(--ink);font-size:var(--t-sm);
  padding-top:12px;padding-bottom:10px}
.li-group .g-sub{font-weight:400;color:var(--ink-3);font-size:var(--t-xs);margin-top:2px}
.li-no{color:var(--ink-4);font-variant-numeric:tabular-nums;font-size:var(--t-xs)}
.li-desc .d-sub{font-size:var(--t-xs);color:var(--ink-3);margin-top:3px}
.li-out{font-variant-numeric:tabular-nums;text-align:right;font-weight:500}
.li-tot{display:flex;justify-content:space-between;gap:var(--s5);padding:7px 0;
  font-size:var(--t-sm);color:var(--ink-2)}
.li-tot.grand{font-size:var(--t-lg);font-weight:600;border-top:1px solid var(--line-2);
  margin-top:9px;padding-top:13px;color:var(--ink);letter-spacing:-.02em}
@media(max-width:760px){
  .li-table thead{display:none}
  .li-table,.li-table tbody,.li-table tr,.li-table td{display:block;width:100%}
  .li-table tr{border:1px solid var(--line);border-radius:var(--r-sm);margin-bottom:var(--s3);
    padding:var(--s4)}
  .li-table td{border:0;padding:5px 0}
  .li-table td::before{content:attr(data-l);display:block;font-size:var(--t-xs);
    color:var(--ink-4);margin-bottom:3px}
  .li-table .li-group td::before{display:none}
}

/* ===================== FEED ===================== */
.feed{list-style:none;margin:0;padding:0}
.feed li{display:grid;grid-template-columns:52px 1fr;gap:var(--s4);padding:10px var(--s6);
  border-top:1px solid var(--line)}
.feed li:first-child{border-top:0}
.feed .t{font-family:var(--mono);font-size:var(--t-xs);color:var(--ink-4);padding-top:2px}
.feed .b{font-size:var(--t-sm);color:var(--ink-2)}
.feed .b b{font-weight:500;color:var(--ink)}
.feed .b .who{color:var(--ink);font-weight:550}
.feed .b .obj{font-family:var(--mono);font-size:var(--t-xs)}
.feed .b .note{font-size:var(--t-xs);color:var(--ink-4);margin-top:2px}
/* Avatar variant: <li class="has-av"><span class="sb-avatar av-b">PS</span><span class="b">â€¦
   A face beside an entry is read faster than a timestamp, and it warms the feed up. */
.feed li.has-av{grid-template-columns:32px 1fr;align-items:start}
.feed li.has-av .sb-avatar{width:30px;height:30px;flex:0 0 30px;font-size:11px}
.feed li.has-av .b .when{font-family:var(--mono);font-size:var(--t-xs);color:var(--ink-4);
  margin-left:6px}

/* ---------- tinted panel ----------
   For summary blocks that should not read as another white card. */
.panel{background:var(--tint);border-radius:var(--r-md);padding:var(--s5);border:0;
  box-shadow:inset 0 0 0 1px rgba(0,96,156,.09)}
.panel-plain{background:var(--sunken);box-shadow:inset 0 0 0 1px rgba(13,21,32,.05)}
.panel h3{margin:0 0 var(--s3);font-size:var(--t-sm);font-weight:550;color:var(--ink)}

/* ---------- little stat row ---------- */
.srow{display:flex;align-items:center;justify-content:space-between;gap:var(--s3);
  padding:9px 0;font-size:var(--t-sm)}
.srow + .srow{border-top:1px solid var(--line)}
.srow .l{color:var(--ink-3)}
.srow .v{font-weight:500;font-variant-numeric:tabular-nums}

/* ===================== MISC ===================== */
.banner{display:flex;gap:11px;align-items:flex-start;padding:var(--s3) var(--s4);
  border-radius:var(--r-sm);font-size:var(--t-sm);margin-bottom:var(--s4)}
.banner-info{background:var(--info-bg);color:var(--info-fg)}
.banner-warn{background:var(--warn-bg);color:var(--warn-fg)}
.banner-ok{background:var(--ok-bg);color:var(--ok-fg)}
/* The fourth variant. .pill has all four states, .banner had three, so the
   sign-in page had no way to say "that was wrong" in the system's own voice
   and invented its own red instead. Same pair of tokens as .pill-bad. */
.banner-bad{background:var(--bad-bg);color:var(--bad-fg)}
.banner b{font-weight:550}
.banner .ic{width:16px;height:16px;flex:0 0 16px;margin-top:2px}
/* .banner-act - a banner that carries THE ONE ACTION THAT CLEARS IT.
   `.banner` is a nowrap flex row, so a button added beside the sentence gets
   squeezed to nothing on a phone. This lets the row wrap and gives the sentence
   a flex basis, so the action drops onto its own line instead of shrinking.
   One token rather than a change to `.banner`, deliberately: every other banner
   in the app is text only and none of their geometry moves. */
.banner-act{flex-wrap:wrap;align-items:center}
.banner-act > span{flex:1 1 240px}

.empty{text-align:center;padding:var(--s12) var(--s5);color:var(--ink-3)}
.empty .ic{width:30px;height:30px;color:var(--ink-4);margin-bottom:var(--s3)}
/* a .chip in an empty state centres itself and gets breathing room.
   2026-09-01: 48px and --r-md (REDESIGN_SPEC Â§3), and GEOMETRY ONLY. Â§3a puts
   the subject's own pastel on this tile, so this rule deliberately declares no
   background and no colour: a bare .chip keeps --c-navy, and .empty .chip
   .chip-green keeps green. Setting a ground here would be (0,2,0) and would
   silently beat every (0,1,0) .chip-* variant the next wave applies. */
.empty .chip{margin:0 auto var(--s4);width:48px;height:48px;flex:0 0 48px;
  border-radius:var(--r-md)}
.empty .chip .ic{margin-bottom:0;color:inherit;width:22px;height:22px}
.empty h3{font-size:15px;font-weight:600;color:var(--ink);margin:0 0 6px}
.empty p{margin:0 0 var(--s5);font-size:var(--t-sm);max-width:40ch;margin-inline:auto}
/* the spacing lived on .empty p â€” an empty state with no paragraph cramped its button */
.empty h3 + .btn,.empty h3 + a{margin-top:var(--s5)}
.pager{display:flex;align-items:center;justify-content:space-between;gap:var(--s3);
  padding:var(--s4) var(--s6);font-size:var(--t-xs);color:var(--ink-3)}
/* .sk (a shimmer skeleton) removed: no template used it - grepped app/ for the
   class before deleting - and it ran a 1.4s gradient animation forever, which
   is the decorative motion CLAUDE.md 1.4 rules out. Nothing in this app loads
   asynchronously, so there is no state for a skeleton to stand in for. */

/* ===================== MODAL ===================== */
/* .scrim removed with .sk: no template renders one - grepped app/templates -
   and it carried a backdrop-filter blur, which is glassmorphism by name
   (CLAUDE.md 1.4). shell.js setOpen() still looks for an `<id>-scrim` element
   and simply finds none; whoever adds the first real modal writes the scrim it
   needs, opaque, rather than inheriting a blurred one nobody chose. */
.modal{position:fixed;z-index:61;left:50%;top:50%;transform:translate(-50%,-50%);
  width:min(540px,calc(100vw - 32px));max-height:calc(100vh - 32px);background:var(--paper);
  border-radius:var(--r-lg);box-shadow:var(--sh-lg);display:none;flex-direction:column}
.modal[data-open]{display:flex}
.modal-head{padding:var(--s6) var(--s6) var(--s3);flex:0 0 auto}
.modal-head h2{font-size:var(--t-xl);font-weight:600;margin:0;letter-spacing:-.02em}
.modal-body{padding:0 var(--s6) var(--s6);font-size:var(--t-sm);color:var(--ink-2);
  overflow-y:auto;flex:1 1 auto;min-height:0}
.modal-foot{padding:var(--s4) var(--s6);border-top:1px solid var(--line);display:flex;
  justify-content:flex-end;gap:var(--s2);flex:0 0 auto}

/* ===================== AUTH ===================== */
/* --sunken, not --canvas: --canvas is white now, and a white card on a white
   page with nothing else on it is the one place the hairline alone is not
   enough. The sign-in page is not in the reference set; this follows the
   sidebar's grey-ground-behind-white-surface shape. */
.auth{min-height:100vh;display:grid;place-items:center;padding:var(--s5);background:var(--sunken)}
.auth-card{width:min(400px,100%);background:var(--paper);border:1px solid var(--line);
  border-radius:var(--r-lg);box-shadow:var(--sh);overflow:hidden}
.auth-band{padding:var(--s8) var(--s8) 0;text-align:center}
.auth-band img{width:146px}
.auth-band .hair{display:none}
.auth-body{padding:var(--s6) var(--s8) var(--s8)}
.auth-body h1{font-size:var(--t-lg);font-weight:550;margin:0 0 4px;text-align:center;
  letter-spacing:-.015em}
.auth-body .sub{text-align:center;color:var(--ink-3);font-size:var(--t-sm);margin:0 0 var(--s6)}

/* ===================== PDF VIEWER =====================
   The preview screen shows the real A4 document in the approved Masthead design,
   not a simplified table. .sheet is a true 794x1123 page scaled to fit. */
.viewer{background:var(--sunken);border-radius:var(--r-md);border:1px solid var(--line);
  padding:var(--s6);display:flex;flex-direction:column;align-items:center;gap:var(--s4);
  overflow:hidden}
.viewer-bar{display:flex;align-items:center;justify-content:space-between;gap:var(--s3);
  width:100%;font-size:var(--t-xs);color:var(--ink-3)}
.sheet-stage{width:100%;overflow:hidden;display:flex;justify-content:center}
.sheet-scale{transform:scale(var(--s,1));transform-origin:top center}
.sheet{width:794px;min-height:1123px;background:#fff;color:#0F1E2B;position:relative;
  display:flex;flex-direction:column;font-size:10.5px;line-height:1.5;
  box-shadow:0 1px 3px rgba(13,21,32,.12),0 18px 44px rgba(13,21,32,.16)}
.sheet *{box-sizing:border-box}
.sheet .doc-body{flex:1;display:flex;flex-direction:column}
.sheet .band{background:var(--navy-900);padding:20px 46px;display:flex;
  justify-content:space-between;align-items:center;gap:26px}
.sheet .plate{background:#fff;padding:10px 15px;border-radius:3px}
.sheet .plate img{width:158px;display:block}
.sheet .bco{text-align:right;color:#fff}
.sheet .bco .n{font-family:var(--serif);font-size:13px;font-weight:600}
.sheet .bco .a{font-size:8.4px;line-height:1.65;color:#AFCDE2;margin-top:5px}
.sheet .bco .g{font-family:var(--mono);font-size:8.4px;margin-top:6px}
.sheet .hair{height:2.5px;background:var(--orange-500)}
.sheet .tbar{display:flex;justify-content:space-between;align-items:flex-end;padding:24px 46px 18px}
.sheet .tbar h2{font-family:var(--serif);margin:0;font-size:27px;font-weight:600;
  letter-spacing:-.015em;color:var(--navy-800);line-height:1}
.sheet .tmeta{display:grid;grid-template-columns:auto auto;gap:3px 16px;font-size:9.4px;
  text-align:right}
.sheet .tmeta span{color:#5A6B7B}
.sheet .tmeta b{font-family:var(--mono);font-size:10.4px;font-weight:500}
.sheet .parties{display:grid;grid-template-columns:1fr 1fr 1fr;gap:30px;padding:0 46px 20px}
.sheet .plbl{font-size:7.6px;letter-spacing:.16em;text-transform:uppercase;font-weight:600;
  color:var(--blue-600);display:block;margin-bottom:4px}
.sheet .pname{font-size:11.5px;font-weight:600;margin-bottom:3px}
.sheet .paddr{font-size:9px;line-height:1.6;color:#4A5765}
.sheet .dsec{padding:0 46px}
.sheet table.dt{width:100%;border-collapse:collapse}
.sheet table.dt thead th{background:var(--navy-800);color:#fff;padding:8px 11px;text-align:left;
  font-size:7.6px;letter-spacing:.14em;text-transform:uppercase;font-weight:600}
.sheet table.dt thead th.r{text-align:right}
.sheet table.dt thead th.c{text-align:center}
.sheet table.dt td{padding:8px 11px;border-bottom:1px solid #E9ECF0;font-size:10px;
  vertical-align:top}
.sheet .dgrp td{background:#F3F8FC;font-weight:600;color:var(--navy-800)}
.sheet .dgrp .gs{font-weight:400;color:#7A8794;font-size:9px}
.sheet .dtot{width:292px;margin-left:auto;margin-top:16px}
.sheet .dtot .tr{display:flex;justify-content:space-between;gap:18px;font-size:10px;
  padding:4.5px 12px}
.sheet .dtot .tr+.tr{border-top:1px solid #E9ECF0}
.sheet .dtot .tr.g{background:var(--navy-800);color:#fff;padding:8px 12px;margin-top:2px;
  font-size:12.5px;font-weight:600}
.sheet .dwords{margin-top:14px}
.sheet .dwords div{font-size:9.5px}
.sheet .dpay{margin-top:22px;display:grid;grid-template-columns:1fr 190px;gap:34px}
.sheet .dsign{font-size:9.5px}
.sheet .dsign b{display:block;margin-bottom:30px}
.sheet .dsign .line{border-top:1px solid #0F1E2B;padding-top:5px;color:#7A8794}
.sheet .dfoot{margin-top:auto;background:var(--navy-900);color:#AFCDE2;padding:13px 46px;
  display:flex;justify-content:space-between;font-size:8.2px}
.sheet .dfoot b{color:#fff;font-weight:500}
.sheet .m{font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap}

/* ===================== MOBILE ===================== */
@media(max-width:1023px){
  .app{grid-template-columns:1fr}
  .sidebar{display:none}
  .content{padding:var(--s5) var(--s4);padding-bottom:calc(var(--bottomnav-h) + var(--s8))}
  .topbar{padding:0 var(--s4)}
  .tb-search kbd,.tb-idle{display:none}
  /* Two-up, WHATEVER --cols said. This sets the longhand rather than --cols on
     purpose: --cols is written as an inline style by the view, inline beats a
     stylesheet, and a rule the caller can silently defeat is not a phone rule.
     A three-track hero row must not survive to 390px - it would be a headline
     figure in a 110px column. */
  .kpi-row{grid-template-columns:1fr 1fr;gap:var(--s3)}
  /* ...and the hero takes the whole line instead of half of it. 1/-1, never
     `span 2`: `span 2` in a caller's single-track row would open an IMPLICIT
     column and put the tiles back out of step with the tracks (L-03), while
     1/-1 spans however many explicit tracks there actually are. Only correct
     for a hero placed FIRST in its row; a hero in the middle would leave the
     cell before it empty, and there is no CSS that can guess otherwise. */
  .kpi-hero{grid-column:1/-1}
  /* 44px on touch. The owner has twice rejected a target under 40px, and this
     button has no text to enlarge its hit area the way a labelled .btn does.
     Placed here, AFTER .btn-sm, so a .btn-sm.btn-icon is raised back over the
     floor rather than staying at 30px - source order is what decides between
     two single-class selectors. */
  .btn-icon{width:44px;height:44px;flex:0 0 44px}
  /* 44px on touch, for the same reason as .btn-icon above: these are the four
     controls a phone user comes to this screen to press. */
  .page-actions .btn-quick{height:44px}
  .grid2,.grid3,.split{grid-template-columns:1fr}
  .form-grid,.form-grid.g3{grid-template-columns:1fr}
  .form-grid .span2{grid-column:auto}
  .card-body,.form-sec{padding:var(--s5)}
  .card-head,.pager,.feed li{padding-inline:var(--s5)}
  .rlist-head,.rlist-row{padding-inline:var(--s4)}
  .page-head h1{font-size:var(--t-xl)}
  .viewer{padding:var(--s3)}
  .tabs{overflow-x:auto;padding-bottom:2px}
  /* The filter bar is the densest thing on a phone. Search takes the full first line, the
     selects scroll horizontally under it rather than wrapping into a four-line block. */
  .search-field{flex:1 1 100%;min-width:0}
  .toolbar{gap:var(--s2);row-gap:var(--s3)}
  .toolbar .div,.toolbar .spacer{display:none}
  /* height:38px dropped - --ctl-h is 38px everywhere now, so this override had
     nothing left to raise. An orphan created by that change, removed with it. */
  .fsel > select{font-size:16px}
  /* .rlist-head{position:static} removed: the base rule is no longer sticky, so
     this override had nothing left to undo. An orphan created by that fix and
     cleaned up with it, per the surgical-changes rule. */
  /* Track count MUST equal len(BOTTOM) in app/nav.py. The approved mockup's bar
     had FIVE items, the middle one a prominent "New document" button; D-083
     removed it, because phones are for looking things up and sharing a PDF, not
     for creating documents at a line-item editor. Four items left in five tracks
     is lesson L-03 - the bar would sit in 4/5 of the width with a dead column.
     NOW VARIABLE: the track count reads --bn-count, which base.html sets from
     the length of the rendered item list, so a role-filtered bar cannot leave a
     dead column. The 4 is only a fallback for a caller that forgets to set it.
     First and only intentional departure of this file from the mockup. */
  .bottomnav{display:grid;grid-template-columns:repeat(var(--bn-count,4),1fr);
    position:fixed;left:0;right:0;bottom:0;
    height:calc(var(--bottomnav-h) + env(safe-area-inset-bottom));
    padding-bottom:env(safe-area-inset-bottom);background:var(--paper);
    border-top:1px solid var(--line);z-index:40}
  .bn-item{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;
    font-size:10.5px;color:var(--ink-3);font-weight:450}
  .bn-item[aria-current="page"]{color:var(--ink);font-weight:550}
  .bn-item .ic{width:19px;height:19px}
  .bn-new .ic{width:25px;height:25px}
  .input,.select,.textarea{font-size:16px}
}
@media(min-width:1024px){ .m-only{display:none!important} }
@media(max-width:1023px){ .d-only{display:none!important} }
@media(prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;transition-duration:.01ms!important}}

/* ==========================================================================
   WAVE 4 â€” TRAINING CALENDAR. Start of the one block this wave adds.

   L-07 keeps this file single-authored by the orchestrator, and that is why
   14+ pages carry no undefined class. This block is a NARROW, granted
   exception: calendar-only classes, all prefixed `.cal-`, in one contiguous
   block at the end of the file, opened and closed by these comments. Nothing
   outside the calendar uses any of them, and nothing here restyles anything
   that already existed.

   Everything the calendar could borrow, it borrowed: the month cards are
   `.card`, the offering state is `.pill`, the year picker is `.fsel`, the add
   panel is `.form-sec`/`.form-grid`/`.field`, the tab strip is `.tabs`/`.tab`.
   These eleven classes are what was left over: a twelve-cell year grid, and the
   month card's own inside.

   The shape follows the owner's OWN printed calendar (2026 TRAINING CALENDAR.pdf):
   three columns of four months, an uppercase month name over a rule, and each
   offering a day-range above an UNDERLINED course name. Written phone-first â€”
   twelve stacked cards is one of the few data-dense layouts that genuinely
   stacks â€” and widened at 640px and 1024px, so the phone case is the base rule
   rather than an override that might not fire.
   ========================================================================== */
.cal-year{display:grid;grid-template-columns:1fr;gap:var(--s4);align-items:start}
/* MEASURED, not inherited: `.card+.card{margin-top:var(--s5)}` (line 253) gives
   every month card but the first a 20px top margin, so JANUARY sat 21px above
   FEBRUARY and MARCH in a real Chrome at 1526px. That is the identical defect
   the `.grid2 > .card + .card` rule above was written for, and the same fix.
   `align-items:start` is the other half: without it every card in a row stretches
   to the tallest, and a month with one course draws a card three times its own
   height with nothing in the bottom two thirds. */
.cal-year > .card + .card{margin-top:0}
@media(min-width:640px){ .cal-year{grid-template-columns:repeat(2,1fr)} }
@media(min-width:1024px){ .cal-year{grid-template-columns:repeat(3,1fr);gap:var(--s5)} }

/* Carried on an element that is also `.card`, so background, radius and
   elevation come from the design system and only the inside is new. */
.cal-month{padding:var(--s4) var(--s5) var(--s5);display:flex;flex-direction:column}
.cal-mhead{display:flex;align-items:center;justify-content:space-between;gap:var(--s2);
  padding-bottom:var(--s2);margin-bottom:var(--s3);border-bottom:1px solid var(--line)}
/* 2026-09-01: --ink, not --navy-800. A retheme leftover measured on the
   rendered calendar - "JANUARY" came out navy on a shell that no longer has a
   navy anywhere else, so twelve headings were the most saturated thing on the
   screen. REDESIGN_SPEC Â§1: colour appears only in small tinted chips, and Â§3:
   headings are --ink. The current month keeps its navy BECAUSE it sits on the
   --c-blue ground (.cal-now below) - that is 3a's "darker same-hue foreground
   on a pastel ground", not a leftover. */
.cal-mname{font-size:var(--t-xs);font-weight:600;letter-spacing:.09em;text-transform:uppercase;
  color:var(--ink);margin:0}
/* The month's "+". A 28px target at --ctl-h would tower over a 12.5px heading,
   so it is deliberately smaller than the standard control - it is a mark on a
   card, not a form control. */
.cal-add{display:inline-grid;place-items:center;width:26px;height:26px;flex:0 0 26px;
  border-radius:var(--r-sm);color:var(--ink-3)}
.cal-add:hover{background:var(--hover);color:var(--navy-800)}
.cal-add .ic{width:15px;height:15px}

.cal-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}
.cal-off{display:block;padding:7px 8px;margin:0 -8px;border-radius:var(--r-sm);color:var(--ink)}
.cal-off:hover{background:var(--hover);color:var(--ink)}
.cal-dates{display:block;font-family:var(--mono);font-size:var(--t-2xs);
  font-variant-numeric:tabular-nums;color:var(--ink-3)}
/* SUPERSEDED (2026-09-01) - `color:--navy-800; border-bottom:1.5px solid
   --orange-500`, on the reasoning that "orange in this system rules and marks
   and never carries text". The reasoning held against the old blue/navy shell.
   MEASURED on the rendered calendar against the retheme: a thin saturated
   orange rule under a course name reads as a SPELLCHECK SQUIGGLE, not as a
   mark, once the surrounding shell is grey and white - it is the only saturated
   line on the screen and it sits exactly where a browser draws an error.
   The affordance it was carrying is not lost: the whole `.cal-off` row is the
   link and already takes a --hover ground, which is the same "row 40px, hover
   --hover" pattern every list in the product uses (REDESIGN_SPEC Â§3).
   NO PASTEL IS SET HERE, deliberately: 3a gives the calendar entry the COURSE's
   identity hue, and that map is the training agent's to define - a colour put
   here now would be a second course palette, which is the collision 3a rule 2
   forbids. This rule leaves the slot open rather than filling it. */
.cal-course{display:inline-block;margin-top:1px;font-size:var(--t-sm);font-weight:500;
  color:var(--ink);padding-bottom:1px}
.cal-meta{display:block;margin-top:4px;font-size:var(--t-2xs);color:var(--ink-3)}
/* A month with nothing in it still draws its card - all twelve are the year.
   It says so in words rather than leaving a hole the reader has to interpret. */
.cal-none{margin:0;padding:7px 0;font-size:var(--t-xs);color:var(--ink-4)}

/* ---------- wave 9: a course that crosses a month, and the month you are in.
   Three rules, and each of them exists because the markup above now says
   something it could not say before. No new colour and no new box: both borrow
   tokens that are already in this file.

   `.cal-carry` is a course this card is NOT the start of - it arrived already
   running from the month before. The left rule is the whole treatment: it is
   the same 1.5px orange the course name is underlined with, so the mark reads
   as part of the calendar's own artwork rather than as a second system, and it
   sits in the negative margin `.cal-off` already opens (`margin:0 -8px`), so it
   costs the row no width and cannot push a long course name into a second line.
   NOT a background tint, which was tried first and rejected: a tinted row on a
   card whose current-month state is ALSO a tint gives two tints that combine,
   and September would then have three different row backgrounds. */
.cal-carry{border-left:1.5px solid var(--orange-500);padding-left:6px;margin-left:-8px}
/* The word beside the dates - "runs on", "ends here", "runs all month". It is
   information the dates cannot carry (see the template's own note), so it is
   set as text at the dates' own size and one ink lighter, NOT as a `.pill`:
   the five pills are this app's STATUS language, and where a course sits in its
   own run is not a status. Nothing new is introduced for it. */
.cal-carry-t{margin-left:6px;color:var(--ink-4);font-family:var(--sans);
  font-variant-numeric:normal;letter-spacing:.01em}
/* The month you are actually in. A tint on the card, not a border or a shadow:
   a border changes the card's outer size and the twelve cards stop lining up in
   the grid, which is the identical defect the `.rep-now` rule above was written
   around. Faint on purpose - it answers "where am I in the year" at a glance
   and must not read as an alert. */
.cal-now{background:var(--c-blue)}
.cal-now .cal-mname{color:var(--navy-900,var(--navy-800))}
/* ================= WAVE 4 â€” TRAINING CALENDAR. End of block. ============= */

/* ==========================================================================
   WAVE 5A â€” REPORTS. Start of the one block this wave adds.

   L-07 keeps this file single-authored, which is why 14+ pages carry no
   undefined class. This block is a NARROW, granted exception of the same shape
   wave 4 was given for `.cal-`: report-only classes, every one prefixed `.kpi-`
   or `.rep-`, in one contiguous block at the end of the file, opened and closed
   by these comments. Nothing outside /reports uses any of them, and nothing
   here restyles anything that already existed.

   Everything the screen could borrow, it borrowed: the tiles are `.kpi` in
   `.kpi-row`, the year picker is `.tabs`/`.tab`, the states are `.pill`, the
   containers are `.card`/`.card-head`/`.card-foot`, the empties are `.empty`.
   Three things had no existing home and are the whole of this block:
     1. a money figure inside a KPI tile (`.kpi .v` is 27px and a lakh-plus
        rupee amount does not fit that with slack);
     2. a numeric TABLE â€” `.rlist` is a record list, and its phone rules name
        exactly five grid areas, so a sixth column auto-places outside the
        layout (L-03 / D-089 / D-121, which has fired twice here);
     3. a proportional bar. The bar never carries a value on its own â€” the
        number it draws is printed in full in the same row.
   ====================================================================== */

/* ---------- a money figure on a KPI tile ----------
   `.kpi .v` is --t-2xl (27px). MEASURED in Chrome at a 1280px viewport: the
   tile's content box is 191.4px, and "INR 19,62,788.74" renders 151.8px at
   --t-xl (21px) - 39.6px of slack, one line - against roughly 195px at 27px,
   which wraps a rupee figure mid-number. The currency is set smaller and
   lighter so the digits keep the size.
   Where it runs out: a 15-character figure (a 12-crore year) is a 1px fit and a
   17-character one takes two lines. It WRAPS rather than clipping or
   overflowing - `overflow-wrap:anywhere` - and the four tiles grow together, so
   the failure mode is a taller row, never a lost digit. Recorded because the
   company's largest year to date is 19.6 lakh, so that is 60x away.

   âš  Written as `.kpi .v.kpi-money`, not `.kpi-money`. MEASURED, not assumed:
   a bare `.kpi-money` is specificity (0,1,0) and loses to the existing
   `.kpi .v` (0,2,0), so the declaration below was ignored and the figure
   rendered at 27px anyway - a rule that looks applied in the file and is not
   applied in the browser. Caught only by reading the COMPUTED style in Chrome
   (L-30). The negative-value colour below needs the same treatment for the
   same reason: `.kpi .v` also sets `color`. */
.kpi .v.kpi-money{font-size:var(--t-xl);letter-spacing:-.022em;overflow-wrap:anywhere}
.kpi .v.rep-neg{color:var(--bad-fg)}
.kpi-cur{font-size:var(--t-sm);font-weight:500;color:var(--ink-3);letter-spacing:0;
  margin-right:1px;font-variant-numeric:normal}
/* Keeps the tile's caption clear of the chip, which is absolutely positioned at
   `top/right: var(--s4)` and therefore takes no space in the flow. MEASURED, not
   assumed: at 1280 the longest caption here ("Waiting for approval", 113.8px)
   cleared the chip by 45.6px, but at 390 the tile is 170px wide and the caption
   ran UNDER the chip and was cut to "Waiting for approv" - visible only in a
   real phone-width render, which is why this file's rule is to render and look.
   36px chip + 8px gap. Scoped to `.kpi-k` rather than added to `.kpi .k`, so no
   tile outside /reports changes. */
.kpi-k{padding-right:44px}

/* ---------- the report table ----------
   Same mechanism as `.rlist-head`/`.rlist-row` â€” one `--cols` track string used
   by both the head and every row â€” but with NO grid-template-areas. That is the
   point: `.rlist`'s phone layout names five areas, so a sixth cell places
   itself outside them and silently breaks every row. This one restacks to two
   plain columns, so a column added later cannot land outside the layout. */
.rep-head,.rep-row{display:grid;align-items:center;gap:var(--s4);
  grid-template-columns:var(--cols);padding:9px var(--s6)}
.rep-head{font-size:var(--t-xs);color:var(--ink-3);font-weight:500;padding-block:9px;
  background:var(--sunken);border-bottom:1px solid var(--line)}
.rep-row{border-top:1px solid var(--line);color:var(--ink)}
/* The head already carries a bottom edge; a second line under it would read as
   an empty first row. Same fix `.rlist > li:first-child` uses. */
.rep-head + .rep-row{border-top:0}
a.rep-row{color:var(--ink);transition:background var(--dur) var(--ease)}
a.rep-row:hover{background:var(--hover);color:var(--ink)}
/* The month you are in. A tint, not a border: a border would change the row's
   height and make the twelve rows unequal. */
.rep-now{background:var(--c-blue)}
.rep-name{font-weight:500;min-width:0;overflow:hidden;text-overflow:ellipsis}
.rep-n{font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1;
  text-align:right;white-space:nowrap}
/* A real zero, printed, in a lighter ink. NOT hidden: a month that earned
   nothing and a month that is missing must not look the same. */
.rep-zero{color:var(--ink-4)}
.rep-neg{color:var(--bad-fg)}
/* Per-cell labels. Hidden on desktop, where the head row carries them; shown on
   a phone, where the head row is gone. Same two-readings-of-one-markup shape as
   `.lhead`/`.lbl` on the line editor. */
.rep-l{display:none}

/* ---------- the proportional bar ----------
   Two stacked tracks, in and out, scaled to ONE ceiling shared by all twelve
   rows so they are comparable with each other. Width is an integer percentage
   computed in Python; there is no chart library and no script. */
.rep-bar{display:flex;flex-direction:column;gap:3px;min-width:0}
.rep-track{height:6px;border-radius:var(--r-pill);background:var(--sunken);
  box-shadow:inset 0 0 0 1px rgba(13,21,32,.04)}
.rep-fill{display:block;height:100%;border-radius:var(--r-pill)}
.rep-fill-in{background:var(--blue-500)}
.rep-fill-out{background:var(--orange-700)}
.rep-legend{display:flex;gap:var(--s3);flex-wrap:wrap;font-size:var(--t-xs);
  color:var(--ink-3)}
.rep-key{display:inline-flex;align-items:center;gap:6px}
.rep-swatch{width:10px;height:10px;border-radius:3px;flex:0 0 10px}

/* ---------- .rep-chart - the year's shape, in one picture ----------
   The owner's words were "it is not visual" and "make it look like a real
   dashboard". This is the answer, and it is INFORMATION rather than decoration
   by a test it has to keep passing: every column stands directly above the row
   that prints its two figures in full, to the paisa, and the chart REPLACED
   that row's per-month bar rather than being added beside it. Twelve two-line
   bars stacked down a table compare a month with itself; one strip of twelve
   compares the year with itself, which is the question a year screen is open to
   answer. It also took a column OUT of the table - six tracks to five.

   NO NEW COLOUR. The columns are `.rep-fill-in` / `.rep-fill-out`, the two
   fills the legend already names and the bars already used - so the legend
   above the chart is still the legend for it. No sixth family, no gradient, no
   transition: a bar that grows on load is decorative motion.

   NOT AN SVG and not a library. Two divs and a percentage height, drawn from
   the SAME integer `in_pct`/`out_pct` the bars used, against the SAME single
   ceiling for all twelve months - so the columns are comparable with each other
   and no CDN, no script and no font is involved in reading the year.

   L-03: `--cols` is written from `months|length`, so the track count IS the
   item count and cannot drift from it. */
.rep-chart{display:grid;grid-template-columns:var(--cols,repeat(12,1fr));
  gap:var(--s2);align-items:end;padding:var(--s5) var(--s6) 0}
.rep-col{display:flex;flex-direction:column;align-items:stretch;gap:6px;min-width:0}
/* The plot area. A fixed height, because a percentage of an auto-height parent
   resolves to auto and every column would collapse to its min-height - the
   classic silent-zero chart. */
.rep-cols{display:flex;align-items:flex-end;gap:2px;height:104px}
/* min-height, so a month that billed something REAL but tiny is a visible mark
   rather than nothing: `in_pct` is integer-floored, so anything under 1% of the
   year's biggest month arrives here as 0% and would otherwise vanish.
   `.rep-c0` is the opposite case and is keyed off the AMOUNT, never the
   percentage - a month that billed nothing draws nothing, and "nothing" and
   "a rounding-down of something" must not look the same. Both bars are always
   RENDERED either way, so column width is constant across the strip and a
   month with one kind of money does not draw a bar twice as wide as its
   neighbour's - which would be a signal that means nothing. */
.rep-cbar{flex:1 1 0;min-width:0;min-height:3px;border-radius:3px 3px 0 0}
.rep-cbar.rep-c0{min-height:0;background:none}
.rep-cm{font-size:var(--t-2xs);color:var(--ink-4);text-align:center;
  white-space:nowrap;overflow:hidden}
.rep-cnow .rep-cm{color:var(--ink-2);font-weight:600}

@media(max-width:760px){
  /* Wave 6A, and it is a MEASUREMENT, not a preference. At 390px the four
     tiles are two-up and each is 168px wide. At --t-xl the year's own figure,
     "19,62,788.74", renders 166.7px inside that 168px box - a 1.3px fit, which
     is not slack, it is luck: one more digit (the first crore year) and
     `overflow-wrap:anywhere` breaks the NUMBER across two lines, where
     "19,62,788." reads as a complete figure to anyone scanning.
     At --t-lg the same string is 134.9px - 33.1px of slack - and a 14-character
     crore figure still clears the box by ~10px. The desktop size is untouched:
     there the box is 252px and the figure has 95.7px of room.
     The "INR" prefix already sits on its own line here at both sizes; that is
     the tile wrapping between two words, not a broken number. */
  .kpi .v.kpi-money{font-size:var(--t-lg)}
  /* Without this the desktop hero rule wins THROUGH the media query and a 27px
     crore figure lands on a 390px screen. One rung above the secondary tiles,
     not three.
     âš  Doubled to `.kpi.kpi-hero` (0,4,0) to match the desktop rule it has to
     beat. A media query adds NO specificity, so at (0,3,0) this line would lose
     to `.kpi.kpi-hero .v.kpi-money` above and the phone would silently take the
     desktop size. MEASURED at 390px, not inferred. */
  .kpi.kpi-hero .v.kpi-money{font-size:var(--t-xl)}
  .rep-head{display:none}
  /* Two columns of figures with their own labels, the name and the bar full
     width. No named areas, so this holds however many columns a row has. */
  .rep-row{grid-template-columns:1fr 1fr;row-gap:var(--s3);padding:var(--s4) var(--s4)}
  .rep-name,.rep-bar{grid-column:1/-1}
  .rep-n{text-align:left}
  .rep-l{display:block;font-size:var(--t-2xs);font-weight:500;color:var(--ink-3)}
  .rep-legend{display:none}
}
/* ==================== WAVE 5A â€” REPORTS. End of block. ================== */

/* â”€â”€ Sortable money columns, and the document's own terms â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Two additions, appended at the END of the file on purpose. `a{color:var(--blue-600)}`
   (line 139) would otherwise paint the header link blue against grey siblings;
   `.rlist-head .sortlink` outranks it on specificity, and being last also settles
   source order against the four `max-width` blocks above. Checked first: nothing
   else in this file targets `.sortlink`, `.sortbar`, `.sortcaret` or `.terms-text`. */

/* A sort control is a COLUMN LABEL that happens to be pressable, so it wears the
   header's own type and colour rather than link blue. `is-sorted` is the only
   state that changes ink and weight, so which column is sorted is legible without
   reading the caret. Focus is the global :focus-visible ring (line 141) - not
   suppressed here, and the caret cannot be the only signal for a keyboard user
   because aria-label carries the column AND the direction in words. */
.rlist-head .sortlink{color:inherit;font:inherit;display:flex;align-items:center;gap:6px;
  white-space:nowrap;border-radius:var(--r-sm);transition:color var(--dur) var(--ease)}
/* The link IS the header cell (a direct child of the grid), so `.r` cannot lean
   on text-align here - a flex container aligns with justify-content. */
.rlist-head .sortlink.r{justify-content:flex-end}
.rlist-head .sortlink:hover{color:var(--ink)}
.rlist-head .sortlink.is-sorted{color:var(--ink);font-weight:650}
/* The caret is DRAWN, not typed: IBM Plex Sans is self-hosted and subset, and a
   triangle character it does not carry renders as tofu on the one screen nobody
   re-checks. Borders always work. Down = highest first. Dimmed until sorted, so
   the affordance is visible without the unsorted state claiming an order. */
.sortlink::after{content:"";width:0;height:0;border-left:4px solid transparent;
  border-right:4px solid transparent;border-top:5px solid var(--ink-4);opacity:.55}
.sortlink[data-dir="desc"]::after{border-top-color:var(--ink-2);opacity:1}
.sortlink[data-dir="asc"]::after{border-top:0;border-bottom:5px solid var(--ink-2);
  opacity:1}
/* Phone only. `.rlist-head{display:none}` fires at exactly this width (line 1092),
   taking the header sort control with it, so this bar takes over below 760px and
   the two are never on screen together. `display:none` first, the media override
   after: a media query adds NO specificity, so the later rule is what wins. */
.sortbar{display:none}
.sortbar-l{color:var(--ink-3);font-size:var(--t-xs)}
@media(max-width:760px){
  .sortbar{display:flex;align-items:center;gap:var(--s3);padding:10px var(--s4);
    background:var(--sunken);border-bottom:1px solid var(--line);
    border-radius:var(--r-md) var(--r-md) 0 0;font-size:var(--t-xs)}
  .sortbar .sortlink{color:var(--ink-2);display:inline-flex;align-items:center;gap:6px;
    padding:4px 10px;border:1px solid var(--line);border-radius:var(--r-pill);
    background:var(--surface,#fff)}
  .sortbar .sortlink.is-sorted{color:var(--ink);font-weight:600}
}

/* Terms are typed as LINES and print as lines; collapsing them into a paragraph
   would show the admin something other than what the client receives. */
.terms-text{white-space:pre-wrap;color:var(--ink-2);font-size:var(--t-sm);
  margin:0 0 var(--s3);line-height:1.55}

/* ---------- WAVE 6 â€” tiles that drill, and dollars that do not shout --------
   HANDED BACK, NOT APPLIED: app.css was not this agent's file this wave
   (binding ruling 9). Append verbatim, at the END of the file â€” three of the
   selectors below depend on losing-or-winning against rules that already sit
   near the end, and moving the block earlier changes what wins.

   Nothing existing is edited. Three additions.

   1. `.kpi-act` â€” a tile that IS a link.
      The tile is already an `<a>` in the template, and `.kpi .k`, `.kpi .v` and
      `.kpi .d` each set their own colour, so the anchor needs NO colour reset:
      the figure does not turn blue. What it needs is the AFFORDANCE, and the
      rule this project already wrote for a clickable row â€” `a.rep-row:hover`
      tints the background â€” is reused rather than invented beside.
      Without this block the tiles still lay out and still navigate; they just
      do not signal that they will. */
.kpi-act{cursor:pointer;
  transition:border-color var(--dur) var(--ease),background var(--dur) var(--ease)}
.kpi-act:hover{background:var(--hover)}
/* Keyboard, not only mouse. A tile that answers a click and not a Tab is a
   control only some people have. */
.kpi-act:focus-visible{outline:2px solid var(--blue-600);outline-offset:2px}
/* The caption's call to action. It is a `<span>`, because the tile around it is
   the anchor and an anchor inside an anchor is not valid HTML â€” so it has to be
   given back the link colour it lost by not being one. */
.kpi-go{color:var(--blue-600);font-weight:500}
.kpi-act:hover .kpi-go{text-decoration:underline}

/* 2. `.kpi-mini` â€” the demoted tile. Owner, 2026-09-01: export amounts are rare
      and get "a small tile like that". One rung down on the figure, one on the
      label, one on the padding, and nothing else: it is still a `.kpi`, still
      bordered, still a tile, still legible. Demotion, not removal â€” the two USD
      figures may never be folded into a rupee total (D-066/D-101).

      âš  Written `.kpi.kpi-mini .v.kpi-money` â€” FOUR classes â€” for the
      specificity reason this file already records twice. `.kpi .v.kpi-money` is
      (0,3,0) and sits near the end; a lighter selector is silently ignored and
      the "small" figure renders at the full size. MEASURE it, do not assume:
      the export figure must come out visibly smaller than "Money in". */
.kpi.kpi-mini{padding:var(--s4)}
.kpi.kpi-mini .v.kpi-money{font-size:var(--t-lg);margin-top:5px}
.kpi.kpi-mini .k{font-size:var(--t-xs)}
.kpi.kpi-mini .kpi-cur{font-size:var(--t-xs)}
/* The demoted row is a footnote to the rupee row above it, not a section of its
   own, so it sits closer to what it qualifies than to the next card. */
.kpi-row-minor{margin-top:calc(-1 * var(--s3))}

/* 3. `.rep-filters` â€” the two report filters are one real GET `<form>` now (so
      that they work with scripts off and so that htmx has something boostable),
      which means the form element itself has to be the flex row that the two
      `.fsel` divs previously sat in directly. Without this they stack. */
/*    REV-16: and it has to be CAPPED, because introducing it moved the goalposts
      for `.fsel-cap` above. That token says `max-width:100%` on the site filter
      and it was doing its job - but "100%" now resolves against THIS form, and a
      flex item's default `min-width:auto` let the form itself size to its widest
      option. MEASURED at 390px: form, `.fsel-cap` and the `<select>` all 471px
      inside a 380px page, `documentElement.scrollWidth` 486. With these two
      declarations the chain resolves against the toolbar instead: 486 -> 380, no
      horizontal scroll, and every measured box at 1280px is unmoved. The fix
      belongs on the new parent, not on `.fsel-cap`, which was never wrong. */
.rep-filters{display:flex;align-items:center;gap:var(--s3);flex-wrap:wrap;
  max-width:100%;min-width:0}
