/* make.css — THE SOURCE. Hand-maintained. Edit this file directly.

   IT WAS GENERATED UNTIL 6 SEPTEMBER 2026, by `python3 design/extract_css.py`
   from the <style> block of design/make-system.dc.html, and that header said
   "do not hand-edit". This one replaces it because the pipeline is retired,
   and the reason is not a change of mind about generated files:

   THE GENERATOR LOST ITS SOURCE. Claude Design produced the .dc.html; the
   design authority is now the external UI/UX reviewer, working from a mockup
   and rendered screenshots. Nobody maintains the .dc.html. An extraction that
   nothing feeds does not sit idle — it OVERWRITES this file with a stale
   snapshot the first time somebody runs it, which is worse than no pipeline
   at all. design/extract_css.py was deleted rather than left as a loaded gun.

   WHAT THE EXTRACTION DID BESIDES COPYING, all four verified present in this
   file before it was deleted, and each now guarded by a test that does not
   depend on it:

     the mockup board was CUT           140 lines and 32 selectors of specimen
                                        chrome sit below a marker in the
                                        .dc.html and never belonged here
                                        -> test_no_mockup_board_selector_
                                           reached_the_stylesheet
     mode and density were RESCOPED     `.app[data-mode]` -> `:root[data-mode]`,
                                        because base.html puts the attribute on
                                        <html>; left on .app it never matches
                                        -> test_the_extraction_scoped_every_
                                           mode_selector
     `.app { }` was LEFT ALONE          it is the shell grid, not a token
                                        block, and moving it to :root put the
                                        grid on <html> and ran the whole app in
                                        a 190px strip for six extractions
                                        -> test_the_shell_keeps_its_grid
     the theme stayed COMPLETE          a page with no data-mode attribute —
                                        which is every page — must get every
                                        token, colours and radii and fonts
                                        -> test_a_page_with_no_attribute_gets_
                                           a_complete_theme

   THOSE FOUR RULES STILL BIND. They are properties of this stylesheet, not of
   the tool that used to enforce them, and the tests above enforce them now.
   The failure mode has changed shape rather than gone: it used to be "an
   extraction reverts a hand edit", and it is now "somebody pastes a block out
   of a mockup". A paste is exactly how board chrome and .app-scoped selectors
   arrive, which is why the first two tests exist.

   THE Source-sha256 STAMP IS GONE. It pinned the .dc.html this file was built
   from, and a checksum of a file nothing reads is a fact that cannot go wrong
   and cannot be useful.

   design/ IS KEPT AS HISTORY and is read by nothing at runtime. One test reads
   the .dc.html — for the list of board selectors that must stay OUT of here,
   not to build anything. See design/README.md. */

/* ===================================================================
   make. operations dashboard — stylesheet, pass 1
   Plain CSS. No build step. Copy this block into static/css/make.css.
   ONE TOKEN SYSTEM, DARK, UNCONDITIONAL. There is no light theme, no
   mode switch, no OS query and no alternate palette — deleted
   4 September 2026, not parked. Tokens live on .app for the mockup; in
   the repo move them to :root. data-mode survives as a SELECTOR ANCHOR
   with no colour meaning; see the note at the base block.
   =================================================================== */

/* --- typeface ----------------------------------------------------- */
/* Self-hosted, per DESIGN.md. Paths are relative to this stylesheet at
   dashboard/static/css/, so ../fonts/ resolves to dashboard/static/fonts/. */
@font-face {
  font-family: "General Sans";
  src: url("../fonts/general-sans-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "General Sans";
  src: url("../fonts/general-sans-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "General Sans";
  src: url("../fonts/general-sans-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "General Sans";
  src: url("../fonts/general-sans-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* --- tokens ------------------------------------------------------- */
/* SCOPED TO .app BY DESIGN, and this is the one thing the repo must change.
   Decided 29 August 2026 after considering :root here: rejected, because it
   would break the board.

   This file is a design document. It renders many frames on one page — light
   and dark side by side, comfortable and compact, the phone, the login frame,
   the retired screens — and each frame sets its own data-mode and data-density
   on its own .app wrapper. A :root[data-mode="dark"] block can only hold ONE
   mode per document, so root scoping here would take every dark frame light and
   there would be nothing left to review dark mode on.

   IN THE REPO THE TOKENS MUST BE :root, and data-mode / data-density go on the
   ROOT element. The app renders one mode at a time, and login.html overrides the
   body block with no .app wrapper at all — under .app scoping it can never go
   dark. An always-light login is defensible; an accidental one is not.

   So the extraction rewrite is not a workaround for a mistake in this file. It
   is the single unavoidable edit between a document that shows every mode at
   once and an app that is in one mode at a time. THREE selector families rescope
   together, and it is all or none — a :root token block left with .app-scoped
   dark overrides fails silently, light winning everywhere:

     .app, .app[data-mode]  → :root, :root[data-mode]
     .app[data-density]     → :root[...]

   TWO LINES NOW, down from four. The light block is deleted and the dark
   MECHANISM rules are gone — every one of them folded into the component it
   modified, because with one mode there is nothing for a mode-specific rule to
   be specific about. A selected nav item is simply raised; it is not raised
   "in dark".

   NO COUNTS HERE, deliberately. This comment carried "18 at last count" while
   the extraction script counted 11 — selectors against rewrites, neither wrong,
   two numbers for one thing with nothing forcing them to agree. That is the
   derived-value problem stated in a comment inside the file that documents the
   derived-value problem. A count belongs where it can be derived, which is the
   extraction script and its assertions, not prose here.

   Anchor the rewrite on the selector text, never on those counts — they move
   whenever a mode-specific rule is added.

   Recorded in Make_System_UI_Design.md and in the handoff README §1.

   LIGHT IS DELETED, 4 September 2026. The token block that stood below this
   comment is gone — not retired, not commented out, not parked. One
   unconditional dark system, and nothing to keep in agreement with it.

   SUPERSEDING THE 29 AUGUST DECISION RECORDED BELOW: the reason this file
   scoped tokens to .app rather than :root was that it rendered light beside
   dark on one page, and a :root block can only hold one mode per document.
   That reason is gone with light. The .app scoping STAYS anyway, because the
   frames still each carry their own data-density, and because the repo
   extraction that rewrites .app to :root is already written and tested against
   this shape. Do not take .app scoping out as a follow-on from this pass; it is
   a separate change with the same audit requirement as the attribute itself. */

/* THE ONLY TOKEN BLOCK. Dark, unconditional, no companion — the light block,
   the prefers-color-scheme mirror and every restoration note are deleted as of
   4 September 2026. Nothing switches this and nothing overrides it.

   [data-mode] IS A SELECTOR ANCHOR AND CARRIES NO COLOUR MEANING. It is not a
   mode, it does not name one, and a value on it changes nothing — the frames
   write data-mode="" and get exactly these tokens. It is in this selector list
   because it is doing a SECOND job nobody designed it for: it puts this rule at
   (0,2,0) on any wrapper that carries the attribute, and the 900px query
   matches that shape to override --page-x. Delete the attribute and every one
   of those pairings shifts at once — a bare .app in the query would sit at
   (0,1,0) against a (0,1,0) base and be decided by source order alone, which
   this file forbids at source. That is the failure that put the dashboard in a
   190px strip.

   ITS REMOVAL IS A SEPARATE PASS WITH A SPECIFICITY AUDIT. The route is to make
   the two declarations MUTUALLY EXCLUSIVE rather than competing — the same
   trick :not([data-mode]) played for the OS mirror — which means a second
   min-width query and therefore a change to the one-query rule. Not a visual
   pass. Measure --page-x at 390 and 1440 afterwards; do not read it.

   Both selectors sit on ONE LINE: the repo extracts this sheet by rewriting
   selectors line by line, and a split list is a shape that rewrite
   half-applies. */
:root, :root[data-mode] {
  /* Added 30 August 2026, the one defect found when the app rendered dark for
     the first time. Without this, Chrome keeps painting native control internals
     light: the date-picker indicator goes black on a dark input and is nearly
     invisible, and spinners, scrollbars and native dropdowns follow it.

     THIS CLASS OF DEFECT IS INVISIBLE TO A STYLESHEET AUDIT. The wrong colour is
     painted by the browser, not by any rule here, so there is nothing to read
     and nothing to grep — only rendering finds it. It sat in three blocks while
     three grounds existed; there is one ground now and this is its only copy.
     ALWAYS dark, unconditionally, with nothing to switch it back. */
  color-scheme: dark;

  /* DARK is designed, not inverted. Three surfaces, and a CARD READS BY BEING
     LIGHTER THAN THE PAGE — that step is where the depth comes from:

       --bg              page    very dark warm grey, never black
       --surface         card    a clear step lighter than the page
       --surface-raised  raised  a step lighter again, for what sits over a card

     --surface-sunken is the one token the inversion got backwards. In light,
     sunken is DARKER than the card it sits in; flipped naively it came out
     LIGHTER than the card, so table heads and quiet panels read as raised. In
     dark it belongs just above the page and below the card. */
  /* Steps roughly doubled, 27 August 2026. They were ~4% apart, which is the
     difference between "is that a card" and "that is obviously a card". Hue
     stays WARM — the reference values are cool-neutral and the brand is not. */
  /* The page drops another step, 4 September 2026, now that this is the only
     ground anyone sees. The card is unchanged and RAISED IS UNCHANGED ON
     PURPOSE: --text-muted is measured against --surface-raised, the lightest
     ground in this mode, and lifting it re-opens that measurement. Widening the
     page-to-card step instead buys separation for free — the card reads by
     being lighter than the page, and the further apart those two are, the less
     any edge has to do. */
  /* VALUES FROM THE APPROVED MOCKUP, 5 September 2026 (globals.css :root). They
     are within three units of what measurement had already produced here, which
     is the useful part of the comparison: two people working from the same brief
     landed on the same warm dark ladder. Where they differed the mockup wins,
     because it is now the visual source of truth.

     RAISED GOT DARKER (#2e2b29 → #2b2826), which raises --text-muted's ratio —
     it is measured against raised, the lightest ground in this mode. The note
     above about not lifting raised still stands; the hazard is lifting it, and
     this went the other way.

     BUT THE MUTED INK DID NOT COME WITH IT. The mockup's #8f8882 measures
     4.19:1 on this raised — it is one of the four failures in its own report,
     and adopting both halves would have kept the failure intact while the
     comment claimed a pass. TWO VARIABLES MOVED AND ONLY ONE WAS CREDITED:
     darker ground helps, darker ink hurts, and the ink change dominates. Same
     inverted-direction error this sheet correctly diagnosed for overdue,
     chasing and held — made in the one case where the mockup's value was
     accepted. Corrected 5 September 2026, before it shipped. */
  --bg: #121110;
  --surface: #211f1e;
  --surface-raised: #2b2826;
  --surface-sunken: #181716;
  --surface-hover: #292624;
  --border: #302d2b;
  --figure-edge: 16px;           /* one right edge for a document card's figures */
  --border-strong: #45403d;

  /* Nothing is pure white, and the headings come down too — #e9e4de was still
     bright enough to vibrate against the page. #ffffff on a near-black ground is
     tiring on a screen opened at 8am and left open all day. */
  --text: #e0dad3;
  --text-secondary: #b3aca6;
  /* Softer means the BRIGHT end comes off pure white. The muted end still has to
     clear AA at 11.5px, and it is measured against --surface-raised, the
     LIGHTEST ground in this mode — light text loses contrast as the ground
     lightens, so the direction is the opposite of light mode's.

     Raised is also where the important copy is: everything lifted to it was
     lifted so it would outrank, so it holds the highest-priority text on Home
     and Jobs, and the issue gate. Hardest ground, most important words.

     THE RULE IS "the ground with the LEAST contrast against the text", which is
     the lightest surface here and the darkest one in light. Not "the card", and
     not "the lightest surface" — that phrasing swept dark and left light
     failing at 4.42:1. Adding a surface at either extreme re-opens both. */
  /* STAYS AT THE MEASURED VALUE. The mockup's #8f8882 is 4.19:1 on raised and
     fails; this is 4.83:1 on raised, 5.42 on the card, 4.96 on hover — so the
     sheet's own prior ink already satisfies the rule against the mockup's darker
     raised, and no new value had to be invented. It lands at 11.5px on
     .floor-unit and .attention-group-count, both of which sit ON raised. */
  --text-muted: #9a938d;

  /*  THE SELECT'S CHEVRON, 22 Sep 2026 — the same mark Home's rows end with.
      `.attention-chevron` is not an SVG: it is the character › in General
      Sans at --text-muted, so "match it" meant measuring it rather than
      drawing something chevron-shaped and calling it matched.

      MEASURED OFF THE CAPTURE'S PIXELS, not estimated from a magnified crop:
      Home's glyph is 3.0 x 5.5 CSS px of ink with a 1.0px stroke, and its
      brightest pixel is 154, which is #9a938d exactly. Rotated to point down
      that is 5.5 wide by 3.0 tall. The first attempt at this swap eyeballed a
      7x crop, built 10.0 x 7.0 at stroke 1.5, and looked obviously heavier
      beside Home — three times the ink.

      The viewBox is stated in FINAL CSS PIXELS so the numbers above are the
      numbers below: a 1-unit stroke inset half a unit from each edge fills
      the box exactly, and background-size copies the viewBox one to one.

      THE COLOUR IS WRITTEN OUT HERE AND ONLY HERE. An SVG in a data URI
      cannot read a custom property, so this hex has to be a literal — a copy
      of --text-muted directly above it, and a copy is a thing that drifts.
      `test_the_chevron_is_the_colour_it_claims_to_be` forces them equal.

      %23 is a url-encoded #. An unencoded one ends the data URI at the hash
      and the chevron silently disappears. */
  --caret-chevron: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5.5 3" fill="none" \
stroke="%239a938d" stroke-width="1" stroke-linecap="round" \
stroke-linejoin="round"><path d="M0.5 0.5 L2.75 2.5 L5 0.5"/></svg>');

  /* THE ACCENT IS #169cf7, AND IT IS ONE TOKEN — the period in Hasan's logo
     artwork, 28 Sep 2026. The single-token decision is 27 Sep's; that day's
     value was typed by hand rather than sampled from the brand file, and was
     corrected to the artwork the next day.
     It replaced a red accent that had three values (a fill, a darker text
     value for type under 18px on a light ground, and a dark-ground variant)
     and a rule for choosing between them. The rule is retired with the
     colour: one value for the wordmark period, the primary button and prose
     links, on screen and on paper. The budget is unchanged — the wordmark
     period and one primary action per screen.

     THE PRIMARY LABEL IS DARK INK, NOT WHITE. White on #169cf7 is 2.94:1;
     --accent-ink (the page ground) is 6.41:1, and 5.59:1 on the hover fill.
     tests/test_accent.py computes it from these tokens. As TEXT the accent
     reads 6.41 on --bg, 5.57 on --surface, 4.97 on --surface-raised and
     6.08 on --surface-sunken: every ground it sits on clears 4.5:1. */
  --accent: #169cf7;
  --accent-ink: var(--bg);

  /* THE ONE PLACE THE APPROVED MOCKUP IS NOT FOLLOWED, 5 September 2026, and the
     reason is arithmetic. Its semantic set is #d15b61 overdue, #c18a44 chasing,
     #6f89aa held, #8f8882 muted — and four of its seven semantic colours fail AA
     on its own raised surface: muted 4.19, held 4.07, the red accent of the day 3.83, overdue 3.75.

     THE FIX DIRECTION IN THE REPORT WAS INVERTED. It offered "either raised
     lightens or those four darken". Both of those LOWER the ratio: this is light
     ink on a dark ground, so contrast rises when the ink gets LIGHTER or the
     ground gets DARKER. Darkening #d15b61 takes 3.75 down, not up. The axis is
     lighter ink, or darker ground — and since the surfaces are now the mockup's,
     it is lighter ink.

     So these keep the values measured here, which are the same hues lifted.
     RE-MEASURED against #2b2826 on 5 September 2026 — see the note below:
       overdue #ff7b74 on raised  5.81:1   (mockup #d15b61: 3.75)
       chasing #f2ae35 on raised  7.59:1   (mockup #c18a44: 4.87)
       held    #8fa7c4 on raised  5.92:1   (mockup #6f89aa: 4.07)
       muted   #9a938d on raised  4.83:1   (mockup #8f8882: 4.19 — see the
                                            muted token's own note)

     WHEN A SURFACE VALUE CHANGES, EVERY RATIO MEASURED AGAINST IT IS STALE.
     Re-measure the table; do not carry the numbers over. This list was first
     written against the old #2e2b29 and shipped unchanged when raised became
     #2b2826 — overdue's 5.58 was exactly its ratio against the OLD ground, left
     where it stood while the ground moved under it. Third instance of the same
     shape in two days: a number that reads as measured evidence, computed once
     and then inherited. Two of these were caught in review, not by reading.

     OVERDUE IS WHY THIS MATTERS AND NOT A ROUNDING ARGUMENT. Red is the only
     colour carrying "act now", it was the LEAST readable thing in the mockup's
     set, and the studio reads it on the raised tier — the lightest ground, the
     hardest one. The most urgent word on the screen cannot be the dimmest.

     The accent is measured with its own token above (27 Sep 2026). Until then
     the red accent read 3.83 as text on raised and its white button label
     3.82:1 — below the 4.5:1 a body-size label needs; the new accent and its
     dark label clear it. */
  --state-overdue: #ff7b74;
  --state-overdue-surface: #2e1718;
  --state-chasing: #f2ae35;
  --state-chasing-surface: #2c2113;
  /*  BLUE, AND IT WAS ALREADY HERE. --state-held survived the lanes being
      retired with no rule reading it. It is what INFORMATIONAL means from
      22 Sep 2026 — scheduled, sent, awaiting an answer, in for maintenance:
      real positions that are neither good nor a problem. The surface and ink
      are added beside it so the variant has the same three parts as the
      others. Measured on the pill: #cfdcea on #1a2430 is 11.26:1, the marker
      #8fa7c4 on that ground 6.34:1. */
  --state-held: #8fa7c4;
  --state-held-surface: #1a2430;
  --state-held-text: #cfdcea;

  /* LANES RETIRED 27 August 2026 — kept because automatic pricing may return.
     GREEN IS NOT KEPT. It meant "sent without a person", nothing sends without
     a person, and there is no successor: --lane-green and --lane-green-surface
     were deleted on 4 September 2026 along with the .lane-green rules and every
     green example on the board. Green is not in this sheet at any value. If
     automatic pricing returns, the lane that comes back with it needs a colour
     decision, not this one restored. */
  /*  GREEN, ADDED 21 Sep 2026, REVERSING "Green is dead" in HANDOVER §8.
      A SEMANTIC STATE COLOUR AND NEVER AN ACTION COLOUR — the accent remains the
      only primary action. The old rule was right about what it governed:
      green as decoration, on surfaces with nothing confirmed to say. What
      earns it back is that the system now HAS positively confirmed states —
      operational, in stock, approved, won, settled — and had no way to
      distinguish them from "not yet".

      Muted deliberately. A bright success green on a warm dark ground reads
      as a notification rather than as a property of a record, and this
      palette has no other saturated colour to balance it.

      MEASURED, against the ground each one actually sits on:
        text  #b9e6c8 on the pill    10.13:1   (AA needs 4.5)
        dot   #5fd08b on the pill    7.25:1   (non-text needs 3)
        border #3b7656 on the card   3.06:1   (non-text needs 3)
      The border is the boundary between the pill and the CARD, so that is
      what it is measured against — not against its own fill, which is the
      comparison that looks rigorous and answers nothing. */
  --state-good: #5fd08b;
  --state-good-surface: #163126;
  --state-good-border: #3b7656;
  --state-good-text: #b9e6c8;
  --lane-amber: #f2ae35;
  --lane-amber-surface: #2c2113;
  --lane-red: #ff7b74;
  --lane-red-surface: #2e1718;

  --focus: #e9e4de;
  --shadow-card: 0 1px 0 rgba(255, 255, 255, .03), 0 10px 26px rgba(0, 0, 0, .45);
  --shadow-pop: 0 12px 38px rgba(0, 0, 0, .66);

  /* Elevation replaces hairlines — and the card border must be DARKER than the
     card, never lighter. A line lighter than the fill it surrounds reads as an
     outline around a hole, which is exactly how these cards read at #262322 on
     a #1e1c1b card. Below the card, it reads as the shadowed underside of
     something raised. The surface step and the shadow do the separating. */
  --card-border: #1c1a19;
  /* The two borders the lead tier uses on the raised surface. They were literal
     hexes inside the deleted mode block — the one place literals were tolerated
     — and folding those rules into components brought them into the sheet
     proper, where the no-literal-hex rule applies. */
  --border-raised: #3a3431;
  --border-lead: #3e2b29;
  --card-shadow: 0 1px 2px rgba(0, 0, 0, .55), 0 10px 26px rgba(0, 0, 0, .34);
  /* A SECOND, QUIETER ELEVATION for chrome planes and raised rows that should
     read as lifted without a card's weight. Dark separates by surface step
     first and shadow second; this is the second half. */
  --shadow-plane: 0 1px 0 rgba(0, 0, 0, .5), 0 8px 22px rgba(0, 0, 0, .3);
  /* Flagged as the one base-only token carrying colour with no light value stated.
     MOOT as of 4 September 2026: there is no light, so a single value is not half a
     pair — it is the value. */
  /* Chrome planes — sidebar, topbar, strip — are separated by the surface step
     and the space around them, so their border effectively vanishes. A visible
     line down the sidebar draws a box nobody needs drawn: the sidebar is
     already darker than the page beside it. */
  --chrome-border: #141312;

  /* ── geometry, type and the content axis — NOT mode-specific ──────────
     Declared here because this is the base block, and these reach every mode
     through the cascade. Do not copy them into a mode block.

     RADIUS IS A THREE-STEP HIERARCHY and each step has a job:
       --radius-lg  14px  panels, cards, tables — things with content inside
       --radius      9px  buttons, inputs, selects — things you press or type in
       --radius-sm   6px  small tags and marks
     Pills (999px) are for filter chips and tags, where the shape carries the
     semantics. Not everything is a pill. */
  --radius-lg: 14px;
  --radius: 9px;
  --radius-sm: 6px;
  --row-h: 46px;
  /* A LIST row, not a grid row. 46px is the editable-grid density; an
     operational list opened at 08:00 reads at Home's rhythm, and Home's
     .attention-row is 70px with 12px padding. 64 is the floor the row grows
     from when an address needs a second line. */
  --row-list-h: 64px;

  /* TYPE SCALE — five steps, and the DISTANCE between them is the point.
     Added 4 September 2026: page titles were 25px, section titles 14px and
     body 14px, so a section title and the sentence under it were the same
     size and the hierarchy rested on weight alone. An operational tool is not
     an excuse to make everything small.

       --fs-title      the screen's name, once per page
       --fs-section    a group's name inside the page
       --fs-body       the default, set on .app
       --fs-secondary  supporting prose and detail lines
       --fs-meta       timestamps, units, counts beside a label

     DISPLAY FIGURES ARE NOT ON THIS SCALE. A figure that carries the meaning
     takes its own size and dominates its label — see .metric-figure and
     .stat-count, which are display type in the text face, not mono.

     BUT A DISPLAY FIGURE IS STILL A TOKEN, added 22 Sep 2026. Not being on
     the text scale is not a licence to be a literal: the sizes on this screen
     drifted to 34px on .signal-figure, 38px on .floor-figure, 40px and 30px
     on two .metric-figure variants and 26px on its base — five numbers, none
     of them related to any other, each typed where it was needed. The table
     fiasco this file already records was two literals off the scale; this was
     five.

     --fs-display is the one size a promoted metric's number takes, and every
     metric card reads it. A page that wants a different size is a page that
     has decided it is not a metric card. */
  --fs-title: 29px;
  --fs-section: 15px;
  --fs-body: 14px;
  --fs-secondary: 13px;
  --fs-meta: 11.5px;
  /*  52px: the middle of the 48-56 the brief asks for, and it has to clear
      --fs-title at 29 by enough that the NUMBER wins the page and not the
      screen's name. 46px on a phone — the 900px query sets it — which is
      still far above badge scale and leaves the widest figure the system can
      produce room at 390. Measured, in General Sans at weight 600 with this
      tracking: "9,684.59" is 193px at 52 and 171px at 46, against a 342px
      content column at 390. The brief's floor of 44 was never the binding
      constraint; the number just has to not wrap, and it does not. */
  --fs-display: 52px;

  /* ONE CONTENT AXIS. Breadcrumb, title, description, metrics, filters and
     content all inset by --page-x, so a screen has ONE left edge and nothing
     invents its own horizontal position. The 900px query sets it to 16px; no
     component redeclares it. --page-gap is the space BETWEEN groups — the
     rule is more air between groups and less dead space inside them, so this
     is generous and component padding is not. */
  --page-x: 34px;
  --page-gap: 26px;

  /* THE STRIP'S HEIGHT, DECLARED ONCE AND CONSUMED TWICE — .mobile-nav's own
     min-height and the shell's bottom reserve. It was a 65px literal typed into
     the 900px query beside a strip that derived its height from item padding,
     an icon, a label and a count; this pass moved the item metrics and the two
     numbers silently disagreed by 15px, so the last row of every phone screen
     sat under the strip. The same hazard the file records for counts, applied
     to a height: the number goes where it can be consumed, not where it has to
     be maintained. Change this and both ends move. */
  --strip-h: 80px;

  /* General Sans is self-hosted in dashboard/static/fonts — no external request. */
  --font: "General Sans", system-ui, -apple-system, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
  /* MONO IS SET ONE STEP DOWN, AND THE STEP IS MEASURED, NOT EYEBALLED.
     A monospace face at the same nominal size reads larger. At 100px, against
     the vendored General Sans:

         cap height (H)     71.80  vs  72.90   ratio 1.015
         x-height   (x)     52.70  vs  54.69   ratio 1.038
         FIGURE HT  (0)     68.60  vs  73.97   ratio 1.078   <- the one that matters

     Figures are the only thing mono still carries, so the figure ratio is the
     correction: 1 / 1.0783 = 0.927. At 13px sans that is 12.06px mono, at 14px
     it is 12.98 — a monospace digit then stands exactly as tall as the General
     Sans text beside it. Applied as calc(1em * …) so it follows the context
     rather than being re-typed per rule. */
  --mono-scale: .927;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}

/* NO MODE-SPECIFIC RULES REMAIN. There were eleven, all
   .app:not([data-mode="light"]) …, and on 4 September 2026 each one was folded
   into the component it modified: the nav item, the metric card and its hover,
   the lead band and the cards and rows inside it, the act band, the money net
   card, the issue gate, the chip hover, the work-row hover and the strip's
   current item. With one mode there is nothing for a mode rule to be specific
   about — a selected nav item is raised, not "raised in dark".

   The folding was done AT EACH COMPONENT, never by dropping the prefix. Half of
   those selectors sat ABOVE the component they modified, so a prefix-stripped
   copy would have tied on specificity and lost on order — silently, and only
   for the states nobody screenshots. If you add a rule that needs to outrank a
   component, put it in the component. */

:root[data-density="compact"] { --row-h: 36px; }

/* --- shell -------------------------------------------------------- */
.app {
  display: grid;
  grid-template-columns: 232px minmax(0, 1fr);
  min-height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-body);
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  text-wrap: pretty;
}
.app *, .app *::before, .app *::after { box-sizing: border-box; }
.app :focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 3px; }
/* DISABLED, once, for every native control. On a dark ground a disabled
   control cannot be signalled by going pale — it has to go QUIET: muted ink on
   the sunken surface, the border down to --border, and no hover response.
   Never opacity: it dims border and text at the same rate and takes the label
   below AA. Zero specificity on the element list, so a component with a
   deliberate disabled treatment still wins — .cell-input:disabled stays
   transparent, because a machine-state cell is context, not a dead field. */
.app :where(button, input, select, textarea):disabled {
  background: var(--surface-sunken);
  border-color: var(--border);
  color: var(--text-muted);
  cursor: default;
}
.app :where(button, input, select, textarea):disabled:hover { background: var(--surface-sunken); }

/* --- anchors ------------------------------------------------------- */
/* Recurring drift, three times now (.chip, .tag, .wordmark): this file styles a
   component as <p> or <span>, the template renders it as an <a> to the record,
   and the anchor arrives underlined and browser-blue.

   The general rule: an anchor CARRYING A CLASS is a component first and a link
   second, so it keeps its own colour and takes no underline. Only a bare <a> in
   prose looks like a link. Both selectors are wrapped in :where() so they hold
   no specificity — every component rule below still wins, including .nav-item's
   own colour — which is what lets one rule cover components not written yet.
   Do not add per-component text-decoration overrides. */
/* A prose link is the accent, --accent. Until 27 Sep 2026 this read a separate
   "accent text" token, because the red accent needed a darker value for text
   under 18px; the new accent is one value at every size (5.41:1 on the
   lightest ground it sits on). */
:where(.app a) { color: var(--accent); text-underline-offset: 2px; }
:where(.app a[class]) { color: inherit; text-decoration: none; }
/* A bare anchor INSIDE a component is a record link, not prose. The repo renders
   card and row refs as <a href> with no class, and the rule above would paint
   every one accent — Inquiries showed an accent New inquiry beside accent card
   refs, Jobs showed accent job refs. The budget is the wordmark period and ONE
   primary button; the primary survives, the refs lose the colour here at source.
   :where() keeps this at zero specificity so .doc-link's own rule still wins. */
:where(.inquiry-card a, .work-row a, .quote-card a, .table-data a, .metric-card a, .receipt-row a) { color: inherit; text-decoration: none; }

/* --- money and dates ---------------------------------------------- */
/* Set once here so no template formats either by hand.

   Currency is MVR, thousands separated, TWO DECIMALS, always — including on
   round figures. One format means one format: a unit price is 9.40 and a total
   is 3,693.60, and the columns line up because both carry the same tail.
   The MVR label is its own span so it can be muted without lightening the
   figure, and it is written by the template, never concatenated into the data.

   Dates are 27 Aug 2026. NEVER numeric: the studio's existing records were
   written in two different orders — one record is 19 Aug 2026 entered day
   first, another is 21 Feb 2026 entered month first — so a numeric date cannot
   be read without knowing which system wrote it, and the eighth of December is
   genuinely ambiguous either way. A numeric date here is a bug, not a
   preference. This comment does not write one either. */
.money { font-family: var(--font-mono); font-size: calc(1em * var(--mono-scale)); font-variant-numeric: tabular-nums; white-space: nowrap; }
.money-unit { font-family: var(--font); font-size: .82em; font-weight: 500; color: var(--text-muted); margin-right: .4em; }
.money-lg { font-size: 17px; font-weight: 600; letter-spacing: -.01em; }
/*  THE DOCUMENT'S TOTAL FIGURE. It shared its name with the Money screen's
    tile until 23 Sep 2026, and the tile's flex, padding, border and
    background landed on every Total and Outstanding figure: a stretched,
    filled, left-aligned box on every document. The tile is .money-tile now,
    and tests/test_css_collisions.py fails on the next name used twice. */
.money-total { font-size: 19px; font-weight: 700; }
/* tabular-nums DROPPED here, 6 September 2026. It did nothing: General Sans
   carries no tabular figures, so the declaration described an effect the face
   cannot produce. A rule that reads as doing something and does nothing is the
   same defect as a comment asserting a reason that has expired.
   These are SINGLE figures with nothing to align against, so they keep General
   Sans rather than moving to mono. The ones that do form columns — .work-when,
   and the ones held for review — are handled separately. */
.date { white-space: nowrap; }
.date-muted { color: var(--text-muted); }

/* --- wordmark ----------------------------------------------------- */
/* THE MARK IS ARTWORK, 27 Sep 2026 — templates/_wordmark.svg, Hasan's SVG
   cleaned: letters fill currentColor, the period is .wordmark-dot. The SIZE
   IS STILL EACH SITE'S font-size (27 sidebar and phone header, 22 login, 34
   paper); the mark is .66em tall, which puts it at the width the typed
   wordmark had — 73px at 27px, measured as the link's width at 390. */
.wordmark-mark { display: block; height: .66em; width: auto; aspect-ratio: 315.9 / 76.97; }
.wordmark {
  font-size: 27px;
  font-weight: 600;
  letter-spacing: -.03em;
  text-transform: lowercase;
  color: var(--text);
  margin: 0;
}
/* THE ARTWORK IS UNCHANGED; ONLY THE ANCHOR AREA GROWS. Measured at 390 the
   link was 73x39 — five pixels under the 44px rule, on every screen, because
   the box was the glyphs' own height. The text stays 27px and in the same
   place; `display:inline-flex` with a min-height and centred alignment adds
   the missing pixels above and below without moving the baseline. */
a.wordmark {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  text-decoration: none;
}
.wordmark-dot { fill: var(--accent); }
.legal-line {
  font-size: 11px;
  color: var(--text-muted);
  letter-spacing: .01em;
  margin: 0;
}

/* --- sidebar ------------------------------------------------------ */
/* A PLANE, NOT A PANEL. The sidebar reads as separate because it is a lighter
   surface than the page and because of the space inside it — the border is
   --chrome-border, which is nearly invisible on the dark base by design. A
   visible line here would draw a box around navigation that is already
   obviously navigation. */
.sidebar {
  display: flex;
  flex-direction: column;
  gap: 24px;
  padding: 22px 14px 16px;
  background: var(--surface);
  border-right: 1px solid var(--chrome-border);
  /* FULL VIEWPORT HEIGHT, AND STICKY. The account block sits on
     `margin-top: auto`, which only pushes it down if the sidebar is taller
     than its content — and .app grows to the DOCUMENT height, so on a long
     list the footer was at y=1057 in a 900px window: present, correct, and
     below the fold. It read as missing because on every screen with more than
     a screenful of records it was. Sticky keeps the column the height of the
     window and the account block on the bottom edge of it. */
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}
.sidebar-foot .btn { min-height: 44px; }
.sidebar-nav { display: flex; flex-direction: column; gap: 2px; }
/* A group label is metadata about the list, so it sits at the bottom of the
   scale and stays quiet. It is not a heading competing with the wordmark. */
.nav-group-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: 0 10px 7px;
}
/* THE NAV IS THE QUIETEST THING IN THE SHELL. No icon colour, no accent
   background, no pill at rest, no boxed group. An item at rest is muted ink
   and nothing else; the destination you are on is the only one at full
   strength. */
.nav-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 36px;
  padding: 7px 10px;
  border-radius: var(--radius);
  color: var(--text-muted);
  text-decoration: none;
  font-weight: 500;
}
.nav-item:hover { background: var(--surface-hover); color: var(--text); }
/* ACTIVE IS WEIGHT AND SURFACE. Never the accent — that budget is the wordmark
   period and one primary button per screen, and an active nav item is on every
   screen, which would make it a third permanent use. On the dark base it rises
   to --surface-raised (the mode rule above); recessing it into an already-dark
   slab hides it. */
/* Selected reads UP: it rises to the raised surface. Recessing a selected item
   into an already-dark slab hides it. */
.nav-item[aria-current="page"] { background: var(--surface-raised); color: var(--text); font-weight: 600; }
.nav-count { font-size: var(--fs-meta); color: var(--text-muted); }
.nav-item[aria-current="page"] .nav-count { color: var(--text-secondary); }
.nav-label { min-width: 0; }

/* --- main / page header ------------------------------------------- */
.main { display: flex; flex-direction: column; min-width: 0; }
/* QUIETER THAN THE PAGE BELOW IT. It was a --surface plane with a border,
   which made the chrome the first thing on the screen; it now shares the page
   ground and carries no division. Context on the left, screen-level utilities
   on the right, nothing decorative — the colour-mode switch came out on
   4 September 2026 because the app is dark and there is no mode to choose.
   THE PAGE TITLE IS NOT IN HERE: it belongs to the page body, where it can be
   the largest thing on the screen without competing with chrome. */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-height: 46px;
  padding: 8px var(--page-x);
  background: transparent;
}
.breadcrumb { font-size: var(--fs-meta); letter-spacing: .02em; color: var(--text-muted); }
/* ONE AXIS: the page's inset is --page-x and every section inherits it by
   sitting in this flow. A section that needs its own horizontal position needs
   a functional reason. */
.page { padding: 14px var(--page-x) 44px; display: flex; flex-direction: column; gap: var(--page-gap); }
/* The head is TITLE BLOCK plus ACTIONS, and the title block owns the width.
   The actions are intrinsic and never squeeze the description into a column —
   at 920px the page note was five words wide beside a search field. Below the
   space for both, the actions wrap onto their own line instead. */
.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px 24px; flex-wrap: wrap; }
/*  12px BETWEEN ACTIONS, AND THE SAME 12 IN BOTH WRAPPERS. `.page-actions`
    used 8 and `.head-actions` used 10 for the same job — two spacings for
    "these are the page's actions", neither chosen against the other. */
.page-actions { display: flex; gap: 12px; flex-wrap: wrap; }
.page-head > :first-child { flex: 1 1 340px; min-width: 0; }
/* WRAPS SINCE 12 Oct 2026: an issued invoice gained a fifth action (send from
   the studio's number) and the row ran 254px past the page at 924. */
.head-actions { flex: 0 1 auto; min-width: 0; display: flex; flex-wrap: wrap; justify-content: flex-end;
  align-items: center; gap: 12px; }
/* The screen's name, once, at the top of the scale. Tighter tracking because
   nothing else on the page is this size. */
.page-title { font-size: var(--fs-title); font-weight: 600; letter-spacing: -.025em; line-height: 1.15; margin: 0 0 5px; }

/*  ── A RECORD'S NAME AND ITS STATE ─────────────────────────────────────
    THE PILL WAS INSIDE THE <h1>. Three screens did it — the inquiry, the
    invoice document and the quote — so the badge inherited a 29px line box
    and took whatever horizontal space the reference left it. At 390
    "MK-I-FAKE-0068 [Won]" read as a badge squeezed against an identifier.

    A ROW OF TWO THINGS instead, centred on each other, with a real 16px
    minimum between them. The pill is `flex: none`, so it keeps its own
    padding at every width and can never be compressed to fit; the row wraps
    first, which puts the state on its own line under the reference — which
    is the right answer on a phone and was unreachable while it lived inside
    the heading.

    THE IDENTIFIER IS NOT MADE SMALLER to keep them on one line. */
.record-title { display: flex; align-items: center; flex-wrap: wrap; gap: 11px 16px; margin: 0 0 5px; }
.record-title .page-title { margin: 0; }
.record-title .pill-status { flex: none; }
/*  12px between the state and the metadata under it — the pill is a taller
    object than a line of text and the 5px .page-title carries is not enough
    once it has wrapped onto its own line. */
.record-title + .page-note { margin-block-start: 7px; }
/* A document number is one word. INV-2026-0061 has two legal break points and
   took all three lines at 390px; the number never breaks. */
.page-title-ref { white-space: nowrap; letter-spacing: 0; }
.page-note { font-size: var(--fs-secondary); color: var(--text-secondary); margin: 0; max-width: 62ch; }

/* A NOTE THAT HOLDS INDICATORS RATHER THAN A SENTENCE. Product detail used to
   say "Draft · availability unknown, no material linked" — three states in one
   clause. They are pills now, so the line needs to wrap as a row and lose the
   62ch measure, which is a rule about reading prose. */
/* 8px, as every row of pills in this sheet (.tag-set). It read
   var(--space-2), a token never defined, so the row had no gap at all. */
.page-note--marks { display: flex; flex-wrap: wrap; align-items: center;
  gap: 8px; max-width: none; }
.page-note__plain { color: var(--text-muted); }

/* --- section: a group WITHOUT a box ------------------------------- */
/* THE DEFAULT WAY TO GROUP THINGS, and the answer to six equal containers
   stacked down a page. A heading, the space around it, and — only where a
   division is genuinely needed — one separator. Reach for .panel when the
   group is a discrete object with content inside it: a table, a form, a
   document, a confirmation. Reach for this the rest of the time.

   Grouping by border is the habit this component exists to break. A bordered
   rectangle around a conceptual group says "these things are contained",
   which is rarely the fact worth stating. */
.section { display: flex; flex-direction: column; gap: 12px; }
.section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 18px; flex-wrap: wrap; }
.section-title { font-size: var(--fs-section); font-weight: 600; letter-spacing: -.005em; margin: 0; }
.section-note { font-size: var(--fs-meta); color: var(--text-muted); margin: 0; }
/* A separator is a decision, not a default: one hairline on the axis, between
   groups that would otherwise run together. */
.section-rule { border: 0; border-top: 1px solid var(--border); margin: 0; }

/* --- buttons ------------------------------------------------------ */
/* THREE WEIGHTS, ONE PRIMARY PER SCREEN. .btn is the SECONDARY: a neutral
   surface and a quiet border, deliberately lighter than .btn-primary so the
   one accent action is unmistakable. .btn-quiet is tertiary. A secondary action
   given primary weight is how a screen ends up with three equal calls to
   action and no answer to "what do I do here".

   38px minimum on desktop, 44px on mobile (the 900px query) — the hit box is
   a floor, not the visual size. */
/*  HIDDEN MEANS HIDDEN (5 Oct 2026). `.btn` sets display:inline-flex, which
    beats the browser's own [hidden] rule — so Share PDF, hidden until
    share.js proves the browser can share a file, showed everywhere. */
[hidden] { display: none !important; }

.btn {
  font: inherit;
  font-weight: 500;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 38px;
  padding: 8px 15px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface-sunken);
  color: var(--text);
  cursor: pointer;
}
.btn:hover { background: var(--surface-hover); border-color: var(--border-strong); }
.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
}
.btn-primary:hover { filter: brightness(.93); }
/*  DISABLED GOES QUIET, LIKE EVERY OTHER CONTROL. The shared disabled rule is
    zero-specificity on purpose, so .btn-primary's fill beat it: a disabled
    primary button still looked like the one thing to press. Same treatment
    as the shared rule, at the specificity needed to win. */
.app .btn-primary:disabled, .app .btn-primary:disabled:hover {
  background: var(--surface-sunken); border-color: var(--border);
  color: var(--text-muted); filter: none; cursor: default;
}
/*  A QUIET BUTTON HAS NO BOX, SO ITS TEXT SITS ON THE CONTENT EDGE. It kept
    the boxed button's side padding, which put "Back to Customers" and every
    text-only job control 16–24px in from the edge everything else shares.
    Hover underlines instead of filling a box that is not there. Height and
    the 44px phone minimum are untouched. */
.btn-quiet { border-color: transparent; background: transparent; color: var(--text-secondary); }
/*  (0,2,0), so a later `.btn-sm` — which every card control is — cannot put
    the padding back. It did, on the first pass: the job card's quiet
    controls stayed 24px in from the edge the rest of the card sits on. */
.btn.btn-quiet, .btn-sm.btn-quiet { padding-inline: 0; }
.btn-quiet:hover { background: transparent; color: var(--text);
                   text-decoration: underline; text-underline-offset: 3px; }
.btn-sm { min-height: 32px; padding: 4px 11px; font-size: var(--fs-secondary); white-space: nowrap; }
/* A button label never wraps. Labels here are two or three words by rule; a
   label that needs three lines at 390px is too long and is fixed in the copy,
   not by letting the button grow downward. */
.btn { white-space: nowrap; }
.btn-decide { font-weight: 600; border-color: var(--text); }
.btn-decide:hover { background: var(--text); color: var(--bg); }

/* --- segmented control (mode switch, density) --------------------- */
.segmented {
  display: inline-flex;
  padding: 2px;
  gap: 2px;
  background: var(--surface-sunken);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
}
.segmented-option {
  font: inherit;
  font-size: 12.5px;
  font-weight: 500;
  padding: 4px 11px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}
.segmented-option[aria-pressed="true"] {
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .12);
}

/* --- constant-column statement ------------------------------------ */
/* a column holding one value in every row is a sentence, not 23 cells */
.constant-note {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  padding: 11px 14px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  font-size: 13px;
  color: var(--text-secondary);
}
.constant-note-value { font-weight: 600; color: var(--text); }
.constant-note-edit {
  margin-left: auto;
  font-size: 12.5px;
  /* Was the accent (then --accent-text). The budget shrank to the wordmark period and the primary
     button when the engine went, and a Change link is neither. Underlined
     already, so it loses nothing but the colour. */
  font-weight: 600;
  color: var(--text);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* --- metric strip ------------------------------------------------- */
/* FOUR FIGURES, NOT FOUR CARDS. This was a bordered grid with 1px gutters, so
   a screen opened on five boxes before it said anything. It is a row of
   figures divided by hairlines now, sitting on the page ground and starting ON
   the content axis — the first figure lines up with the title above it. */
/* Flex, not a fixed four columns: the strip carries four figures on one screen
   and five on another, and a fifth item in a four-column grid wrapped into a
   lonely second row. */
.metric-strip { display: flex; flex-wrap: wrap; gap: 0; }
.metric {
  flex: 1 1 150px;
  min-width: 0;
  padding: 2px 20px;
  border-left: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.metric:first-child { border-left: 0; padding-left: 0; }
/* THE FIGURE DOMINATES ITS LABEL — display type in the text face, the label a
   step down in both size and colour. */
/* tabular-nums dropped here as inert, 6 September 2026 — and UNEXERCISED,
   which is a weaker claim than the others on purpose. .metric-value renders
   only on /calculator and only once a result exists; the fixture produces
   none, so this element has never been on screen for anyone to look at. The
   reasoning is the same as the verified drops — General Sans has no tabular
   figures — but nothing was measured here, because there was nothing to
   measure. If the calculator is ever un-parked, look at it. */
.metric-value { font-size: 28px; font-weight: 600; letter-spacing: -.025em; line-height: 1.1; }
.metric-label { font-size: var(--fs-meta); color: var(--text-muted); }
.metric-primary .metric-label { color: var(--text-secondary); font-weight: 500; }

/* --- filter chips ------------------------------------------------- */
.filter-chips { display: flex; flex-wrap: wrap; gap: 9px; align-items: center; }
/* A fuller pill: a sunken rest state so the bar reads as a bar rather than as
   loose outlines, and enough padding to be a comfortable target at arm's length
   from a machine. Selected is still the inverted fill. */
.chip {
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 36px;
  padding: 7px 15px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-sunken);
  color: var(--text-secondary);
  cursor: pointer;
}
.chip:hover { background: var(--surface-raised); border-color: var(--border-strong); color: var(--text); }
/* A filter chip is a link when the filter is a GET, and a button when it
   toggles in place. aria-pressed is for the button; aria-current for the link. */
.chip[aria-pressed="true"],
.chip[aria-current="page"] {
  background: var(--text);
  border-color: var(--text);
  color: var(--bg);
}
/* No text-decoration reset here — :where(.app a[class]) covers it. */
.chip-count { font-size: 12px; opacity: .75; }

/* --- table -------------------------------------------------------- */
.table-card {
  background: var(--surface);
  border: 1px solid var(--card-border);
  box-shadow: var(--card-shadow);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.table-scroll { overflow-x: auto; overflow-y: visible; }
.table-machines {
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
  /* `min-width: 1120px` STOOD HERE and was correct for the nineteen-column
     grid this used to be: the columns genuinely needed that much, so the table
     scrolled inside its card. Machines now has two columns, and the minimum
     kept the table 1120px wide inside a 1069px card at every width — a
     horizontal scrollbar on a desktop around a table with room to spare. A
     minimum that was a measurement of the old content survives as a claim
     about the new. */
  font-size: var(--fs-secondary);
}
.table-machines thead th {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--surface-sunken);
  border-bottom: 1px solid var(--border);
  padding: 9px 12px;
  text-align: right;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .085em;
  text-transform: uppercase;
  color: var(--text-secondary);
  white-space: nowrap;
}
.table-machines thead th.col-machine,
.table-machines thead th.col-lane,
.table-machines thead th.col-status { text-align: left; }
.table-machines tbody td,
.table-machines tbody th { border-bottom: 1px solid var(--border); height: var(--row-list-h); padding: 14px 16px; }
/* The app emits one <tbody> per machine, so tr:last-child matches EVERY row.
   Scope to the last tbody, or the whole table loses its row rules. */
.table-machines tbody:last-child tr:last-child td,
.table-machines tbody:last-child tr:last-child th { border-bottom: 0; }
.table-machines tbody tr:hover td,
.table-machines tbody tr:hover th { background: var(--surface-hover); }

/* sticky machine name — horizontal scroll never hides a header */
.col-machine {
  position: sticky;
  left: 0;
  z-index: 2;
  background: var(--surface);
  text-align: left;
  font-weight: 500;
  min-width: 232px;
  border-right: 1px solid var(--border);
}
.table-machines thead th.col-machine { z-index: 4; background: var(--surface-sunken); }
.machine-name { display: block; color: var(--text); white-space: nowrap; }
.machine-kind { display: block; font-size: 11.5px; color: var(--text-muted); }

.cell-num { text-align: right; font-family: var(--font-mono); font-size: calc(1em * var(--mono-scale)); font-variant-numeric: tabular-nums; }
.cell-empty { color: var(--text-muted); }

/* --- inline editing ----------------------------------------------- */
/* GENERAL SANS, NOT MONO. Every editable cell used to be monospaced, which put
   material and machine NAMES in a code face. After the pricing-engine columns
   came off these lists the only editable cells left are names and a category,
   so the default is the page's typeface; the numeric cells below opt back in,
   because General Sans has no tabular figures and a column of digits in it
   staggers by up to 28.7% of an em. */
.cell-input {
  font: inherit;
  width: 100%;
  min-width: 74px;
  text-align: right;
  padding: 6px 8px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
}
.cell-input:hover { border-color: var(--border-strong); background: var(--surface); }
/* FOCUS IS THE SAME EVERYWHERE: the border comes UP to --text-secondary and the
   ring is the surface ABOVE. --focus is near-white in dark, which is the
   outlined-hole treatment the dark rules forbid, and a --surface-sunken ring is
   DARKER than both the field and the card, so in dark there was no ring at all.
   Corrected 4 September 2026, with .search — .input was moved and these two
   were missed. :focus-visible still draws the keyboard outline over the top. */
.cell-input:focus {
  outline: none;
  border-color: var(--text-secondary);
  background: var(--surface);
  box-shadow: 0 0 0 3px var(--surface-raised);
}

/* The only reason mono survives anywhere: a column of figures has to align,
   and the vendored General Sans carries no tnum. Measured at 200px, the ten
   digits span 57.4px; ui-monospace spans 0. */
.cell-num .cell-input, .num .cell-input, .cell-mono .cell-input {
  font-family: var(--font-mono); font-size: calc(1em * var(--mono-scale)); font-variant-numeric: tabular-nums; text-align: right;
}.cell-input::placeholder { color: var(--text-muted); }

/* BLOCKED BY MISSING DATA — someone types a number and it is solved.
   Loud: dashed field, a written prompt, always reads as an input. */
.cell-input-needed {
  border: 1px dashed var(--border-strong);
  background: var(--surface-sunken);
  text-align: left;
  font-family: var(--font);
  font-weight: 500;
  font-size: 12.5px;
}
.cell-input-needed::placeholder { color: var(--text); opacity: 1; font-weight: 600; }
.cell-input-needed:hover { border-style: solid; border-color: var(--text); }

/* BLOCKED BY MACHINE STATE — nothing to type. Quiet, non-interactive. */
.cell-input:disabled { background: transparent; border-color: transparent; color: var(--text-muted); cursor: default; }

/* --- state flags — overdue and chasing ---------------------------- */
/* The successors to the lanes, and the only coloured components in the sheet.
   Shape stays load-bearing: a DIAMOND is overdue, a TRIANGLE is chasing, so
   neither reading depends on hue. Two states, two shapes, no third.

   These carry no surface fill by default. A row is flagged, not filled — the
   Home bands are separated by spacing and headings, and if a band took a tint
   the red would stop meaning late and start meaning decoration. */
.state-flag {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 12.5px;
  font-weight: 600;
  white-space: nowrap;
}
.state-glyph { width: 12px; height: 12px; flex: none; display: inline-block; }
.state-overdue { color: var(--state-overdue); }
.state-overdue .state-glyph { background: var(--state-overdue); clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
.state-chasing { color: var(--state-chasing); }
.state-chasing .state-glyph { background: var(--state-chasing); clip-path: polygon(50% 0, 100% 100%, 0 100%); }
/* HELD — 3 September 2026. Waiting on somebody outside the studio. Carried on
   the band edge (below); as an in-row flag only where a row needs it. */
.state-held { color: var(--state-held); }
.state-held .state-glyph { background: var(--state-held); border-radius: 50%; }
/* --- lane markers — RETIRED 27 August 2026 ------------------------ */
/* The engine no longer computes a lane, so no shipping screen sets one. Kept
   whole rather than deleted: automatic pricing may return, and with it these
   three classes, their glyphs and the legend. Everything from here to the state
   badges block is reference only. */
.lane {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 12.5px;
  font-weight: 600;
  white-space: nowrap;
}
.lane-glyph { width: 12px; height: 12px; flex: none; display: inline-block; }
.lane-amber { color: var(--lane-amber); }
.lane-amber .lane-glyph { background: var(--lane-amber); clip-path: polygon(50% 0, 100% 100%, 0 100%); }
.lane-red { color: var(--lane-red); }
.lane-red .lane-glyph { background: var(--lane-red); clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
.lane-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 22px;
  font-size: 12.5px;
  color: var(--text-secondary);
  padding: 12px 16px;
  border-top: 1px solid var(--border);
  background: var(--surface-sunken);
}
.lane-legend-item { display: flex; align-items: center; gap: 8px; }

/* --- state badges (quiet context) --------------------------------- */
.badge-state {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
  white-space: nowrap;
}
.badge-state-glyph {
  width: 7px;
  height: 7px;
  flex: none;
  border: 1px solid var(--border-strong);
  background: transparent;
}
.badge-running { color: var(--text-secondary); }
.badge-running .badge-state-glyph { background: var(--border-strong); border-radius: 50%; }

/* --- readiness / stock pill --------------------------------------- */
/* A DIFFERENT fact from .badge-state. That badge says a machine is broken;
   this one says a record is short of numbers, or stock is running out.
   ok = nothing outstanding, warn = short but usable, stop = blocked. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 11.5px;
  font-weight: 600;
  padding: 3px 9px 3px 7px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text-secondary);
  white-space: nowrap;
}
.pill::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--border-strong);
  flex: none;
}
.pill-ok { border-color: var(--text); color: var(--text); }
.pill-ok::before { background: var(--text); }
.pill-warn { border-style: dashed; border-color: var(--text); color: var(--text); }
.pill-warn::before { background: transparent; border: 1px solid var(--text); }
.pill-stop { background: var(--text); border-color: var(--text); color: var(--bg); font-weight: 700; }
.pill-stop::before { background: var(--bg); }

/* --- per-row state control + confirm ------------------------------ */
.status-cell { position: relative; min-width: 220px; }
/* flex-wrap ADDED HERE 6 September 2026, and it was an inline style on
   jobs.html until this file became hand-maintained. Three status buttons need
   about 220px; wherever the control lands in a narrower column they overflowed
   it and printed on top of the money column — "Cancelled" across "2,250.00".
   A scrollWidth check reported no overflow, because flex items that spill out
   visibly do not always extend their container's scroll width.

   IT WAS INLINE BECAUSE THERE WAS NOWHERE ELSE TO PUT IT: make.css was
   generated from the design file, and a hand-written rule here would have been
   silently overwritten by the next extraction. The extraction is retired, so
   the rule comes home and the inline style is deleted. */
.status-control { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* MACHINE STATUS — PERMANENTLY VISIBLE AS A CONTROL, and styled into the
   shared system rather than hidden like the other cells.
   It posts to /status, which states the consequence and waits: a confirmation
   behind a hover state is a confirmation nobody sees. So it does not take the
   .table-list reveal treatment — it is the one cell that must look pressable
   before anyone approaches it.

   IT WAS THE ONLY SELECT IN THE APP THAT LOOKED RIGHT, and on 21 Sep 2026
   that stopped being a virtue. `.status-select` carried the drawn indicator,
   the 30px of right padding it needs and a pointer cursor; the other
   thirty-six selects carried `.input`, which gives a text field's box and
   leaves the NATIVE arrow jammed against the right edge with 11px behind it.
   One control, two appearances, and the good one was the exception.

   So this rule is gone and `select` — the ELEMENT, below — carries all of
   it. A select is now correct with no class at all, which is the only
   version of "one component" that survives somebody adding a field next
   month. `.status-control` stays: it is the wrapper, not the control. */
.confirm-pop {
  position: absolute;
  top: calc(100% - 6px);
  right: 12px;
  z-index: 20;
  width: 306px;
  padding: 14px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-pop);
  white-space: normal;
}
.confirm-title { font-size: 13.5px; font-weight: 600; margin: 0 0 5px; }
.confirm-body { font-size: 12.5px; color: var(--text-secondary); margin: 0 0 12px; }
.confirm-actions { display: flex; gap: 8px; justify-content: flex-end; }

/* --- quote queue, mobile ------------------------------------------ */
/* The .phone frame itself is a board device, not a repo component — it and its
   rules live in the mockup board block at the end of this sheet. */
.quote-list { display: flex; flex-direction: column; gap: 12px; padding: 14px; }
.quote-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 11px;
}
.quote-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.quote-product { font-size: 15px; font-weight: 600; margin: 0; }
.quote-qty { font-size: 12.5px; color: var(--text-muted); margin: 2px 0 0; }
.quote-price { font-size: 17px; font-weight: 600; white-space: nowrap; font-variant-numeric: tabular-nums; }
.quote-price-unit { font-size: 11px; color: var(--text-muted); display: block; text-align: right; font-family: var(--font); }
.quote-reason {
  font-size: 12.5px;
  color: var(--text-secondary);
  padding: 10px 12px;
  border-radius: var(--radius);
  background: var(--surface-sunken);
}
.quote-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.quote-actions .btn { min-height: 46px; }

/* --- generic data table (queue, jobs, materials, people) ---------- */
.table-data { border-collapse: separate; border-spacing: 0; width: 100%; font-size: var(--fs-secondary); }
/* MINIMAL BOXING. The head is restrained — one step below the card, not a band
   of contrast — the rows are separated by hairlines rather than boxed
   individually, and hover and selected do the rest. A cell is never a box. */
.table-data thead th {
  background: var(--surface-sunken);
  border-bottom: 1px solid var(--border);
  padding: 10px 16px;
  text-align: left;
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--text-muted);
  white-space: nowrap;
}
/*  A ROW HEADER IS A CELL, 23 Sep 2026. `<th scope="row">` in the body —
    Suppliers' name, the calculator's labels — matched none of these rules
    and took the browser's own th: centred, unpadded. On Suppliers that read
    as the name sitting indented under a left-aligned heading. */
.table-data tbody td,
.table-data tbody th { border-bottom: 1px solid var(--border); height: var(--row-list-h); padding: 14px 16px; vertical-align: middle; }
.table-data tbody th { text-align: left; }
/*  A NUMERIC COLUMN'S HEADING SITS OVER ITS NUMBERS. `thead th` sets left
    and outranks `.num`, so Suppliers' MATERIALS read left over a count
    aligned right. */
.table-data thead th.num { text-align: right; }
.table-data tbody tr:last-child td,
.table-data tbody tr:last-child th { border-bottom: 0; }
.table-data tbody tr:hover td,
.table-data tbody tr:hover th { background: var(--surface-hover); }
/* SELECTED READS UP, like every other selected state on the dark base. */
.table-data tbody tr[aria-selected="true"] td { background: var(--surface-raised); }
.table-data tbody tr[aria-selected="true"] .cell-strong { font-weight: 600; }
.cell-strong { font-weight: 500; color: var(--text); }
.cell-muted { color: var(--text-muted); }
/*  A SECOND LINE INSIDE A CELL. Product Detail marks a superseded revision
    with one, and the class went in with no rule behind it — so "MK-Q-FAKE-0005
    superseded — not a separate quote" ran on as one sentence and read like
    part of the reference. A span with no rule is not a quiet treatment; it is
    no treatment. */
.cell-sub { display: block; margin-block-start: 2px; font-size: var(--fs-meta);
  color: var(--text-muted); }
.cell-mono { font-family: var(--font-mono); font-size: calc(1em * var(--mono-scale)); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.cell-actions { display: flex; gap: 8px; justify-content: flex-end; }
.cell-reason { color: var(--text-secondary); max-width: 34ch; }

/* --- panels ------------------------------------------------------- */
.panel {
  background: var(--surface);
  border: 1px solid var(--card-border);
  box-shadow: var(--card-shadow);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
/* A PANEL IS FOR A DISCRETE OBJECT — a table, a form, a document, a
   confirmation. It is not how to group two paragraphs; that is .section.
   Padding is tighter than it was: more space BETWEEN groups, less dead space
   INSIDE them, and a panel 70% empty reads as unfinished rather than airy. */
.panel-head {
  display: flex;
  /* WRAP, DO NOT SQUEEZE. At 390 a long title and a long note competed for one
     row and the title broke to one word a line — "What you / have / charged /
     before". A flex item shrinks below its content by default; these two are
     prose, and prose gets its own row rather than a narrower column. */
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
}
.panel-head > * { min-width: 0; }
.panel-title { font-size: var(--fs-section); font-weight: 600; margin: 0; }
.panel-note { font-size: var(--fs-meta); color: var(--text-muted); margin: 0; }
.panel-body { padding: 14px 16px; display: flex; flex-direction: column; gap: 12px; }
/*  PROSE INSIDE A PANEL IS NOT A PANEL BODY. `.panel-body` is a flex column,
    so every inline child of it becomes a flex item on a line of its own. Put
    it on a <p> and one <em> splits the sentence into three lines: Stock's
    empty state read "…the first movement above turns / never counted / into a
    real figure." A container class and a text class are not the same class,
    and nothing in the stylesheet says so — which is why this was invisible
    until a material with no movements was opened. */
.panel-empty { padding: 14px 16px; margin: 0; color: var(--text-secondary); max-width: 72ch; }
/*  THE RAIL WIDENS TO 400 WHERE THERE IS ROOM, 22 Sep 2026. At 330px the
    timeline's descriptions wrapped hard — "Quote MK-Q-FAKE-0016 v1 sent"
    broke across two lines beside a 58px date column — and the panel read as
    one continuous block partly because every line was a fragment.

    minmax(340px, 400px) RATHER THAN 400px, so the track can give some of it
    back before the working column is squeezed. Below 1200 the rail stops
    being a column at all and stacks under the work; that rule and its
    measurements are three sections down and are unchanged. */
.page-columns { display: grid; grid-template-columns: minmax(0, 1fr) minmax(340px, 400px); gap: 20px; align-items: start; }

/* --- product cards ---------------------------------------------- */
.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(272px, 1fr)); gap: 14px; }
.product-card {
  background: var(--surface);
  border: 1px solid var(--card-border);
  box-shadow: var(--card-shadow);
  border-radius: var(--radius-lg);
  padding: 15px 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.product-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.product-name { font-size: 14.5px; font-weight: 600; margin: 0; }
.product-meta { font-size: 12px; color: var(--text-muted); margin: 3px 0 0; }
.product-price { font-size: 15px; font-weight: 600; white-space: nowrap; }
.product-price-unit { display: block; font-family: var(--font); font-size: 11px; color: var(--text-muted); text-align: right; }
.product-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding-top: 10px; border-top: 1px solid var(--border); }

/* --- tags, slots, forms ------------------------------------------- */
.tag {
  display: inline-flex;
  align-items: center;
  font-size: 11.5px;
  font-weight: 500;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  color: var(--text-secondary);
  white-space: nowrap;
}
/* A tag is a link where it points at a record (materials grid) and carries a
   dismiss where the relation is edited (supplier detail). Both are the same
   .tag; only the second one holds a .tag-remove. */
a.tag:hover { background: var(--surface-hover); border-color: var(--border-strong); color: var(--text); }

/* The padding belongs to the control, not to .tag, so a tag without one is
   unchanged. Suppliers are an unordered set — no index, unlike .route-step. */
.tag:has(.tag-remove) { gap: 5px; padding-right: 3px; }
/*  A SET OF TAGS, which had no container. Supplier detail put its tags
    straight into .panel-body — a flex column — so each stretched to the
    panel's full width and inline-flex never got to apply. Seen 23 Sep 2026,
    the first time any supplier existed to draw. */
.tag-set { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.tag-remove {
  font: inherit;
  font-size: 12px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  flex: none;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}
.tag-remove:hover { background: var(--border-strong); color: var(--text); }
.tag-remove:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.slot-row { display: flex; align-items: center; gap: 16px; padding: 13px 18px; border-bottom: 1px solid var(--border); }
.slot-row:last-child { border-bottom: 0; }
.slot-time { font-size: 13px; font-weight: 500; width: 62px; flex: none; }
.slot-main { min-width: 0; flex: 1; }
.slot-customer { font-weight: 500; margin: 0; }
.slot-detail { font-size: 12.5px; color: var(--text-muted); margin: 2px 0 0; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field-label { font-size: 12px; font-weight: 500; color: var(--text-muted); }
.field-hint { font-size: 11.5px; color: var(--text-muted); }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
/* AN INPUT IS EMBEDDED IN THE SURFACE, not a white control repainted dark. It
   sits on --surface-sunken, one step BELOW the card it is in, so the field
   reads as a recess you type into; the border is the quiet --border and only
   focus brightens it. A field LIGHTER than its card reads as a raised slab and
   competes with the content around it — which is what a converted light form
   looks like.

   FOCUS IS THE BRIGHT STATE: the border comes up and a ring sits in the
   surface above. Never the accent, which is spent. :focus-visible still draws
   the keyboard outline over the top. */
.input {
  font: inherit;
  width: 100%;
  min-height: 40px;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  color: var(--text);
}
/* A PLACEHOLDER IS AN EXAMPLE, AND LOOKS LIKE ONE (live Test 1, 28 Sep 2026):
   "250" in the quantity box read as a quantity somebody had entered. Muted
   AND italic, and the words start "e.g.", so no hint passes for a value. */
.input::placeholder, textarea::placeholder { color: var(--text-muted); font-style: italic; }
.input:hover { border-color: var(--border-strong); }
.input:focus { outline: none; border-color: var(--text-secondary); background: var(--surface); box-shadow: 0 0 0 3px var(--surface-raised); }
.setting-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; padding: 13px 0; border-bottom: 1px solid var(--border); }
.setting-row:last-child { border-bottom: 0; padding-bottom: 0; }
.setting-label { font-weight: 500; margin: 0; }
.setting-hint { font-size: 12.5px; color: var(--text-secondary); margin: 3px 0 0; max-width: 52ch; }
.switch {
  flex: none;
  width: 40px;
  height: 23px;
  padding: 2px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: var(--surface-sunken);
  cursor: pointer;
  display: flex;
  justify-content: flex-start;
}
.switch[aria-pressed="true"] { background: var(--text); border-color: var(--text); justify-content: flex-end; }
.switch-knob { width: 17px; height: 17px; border-radius: 50%; background: var(--surface); box-shadow: 0 1px 2px rgba(0,0,0,.3); }
.switch[aria-pressed="true"] .switch-knob { background: var(--bg); }

.input-inline { padding: 5px 9px; font-size: 13px; width: auto; min-width: 132px; }

/* ══════════════════════════════════════════════════════════════════════
   THE SELECT. One rule, one appearance, every <select> in the app.

   WHY THE ELEMENT AND NOT A CLASS. Thirty-six selects carried `.input` and
   one carried `.status-select`, and the difference between them was not a
   decision anybody made — it was that the machine status select got a pass
   of attention in August and nothing else did. A class-based component would
   have left the same hole open: a select added without the class looks
   unstyled, and the way that gets noticed is somebody seeing it on a phone.
   Styled by element, a new <select> is right before anybody names it.

   It sits AFTER `.input` deliberately. Equal specificity, later wins, so a
   select still carrying `class="input"` gets this treatment and not a text
   field's — though none does any more: the class was removed from all
   thirty-six in the same pass, because markup that claims a treatment it
   does not receive is the next person's wasted hour.

   THE INDICATOR IS DRAWN, NOT NATIVE. Two gradients, a caret. The native
   arrow is a different shape, size and colour on macOS, Windows, Android and
   iOS — on iOS it is a double chevron in the system blue — so the one thing
   a native arrow cannot be is the same control on a phone and a laptop.
   Drawing it is what makes the 390 capture and the 1400 capture agree.

   THE RIGHT PADDING IS THE POINT OF THE WHOLE PASS. 30px, so the longest
   option has somewhere to end that is not underneath the caret. `.input`
   gave a select 11px and the arrow sat on the text, which is what Hasan was
   looking at on material detail's Direction and What it was. */
.app select, .login-screen select {
  font: inherit;
  /*  AN EXPLICIT SIZE, BECAUSE `font: inherit` IS NOT ONE. The machine card
      sets a smaller context, so the status select measured 11.5px the moment
      its own `font-size: var(--fs-secondary)` was deleted — a control whose
      type size depends on where it lands is the thing this rule exists to
      stop. 14px matches `.input`, which is what a select sits beside on
      nineteen of the twenty screens that have one. */
  font-size: 14px;
  width: 100%;
  min-height: 40px;
  /*  Asymmetric on purpose: 30px on the right is the caret's room. */
  padding: 9px 30px 9px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background-color: var(--surface-sunken);
  color: var(--text);
  /*  A SELECT IS PRESSED, NOT TYPED IN. `.input`'s default text cursor said
      the opposite on thirty-six controls. */
  cursor: pointer;
  max-width: 100%;
  appearance: none;
  -webkit-appearance: none;
  /*  A STROKED CHEVRON, NOT A SOLID TRIANGLE, since 22 Sep 2026. The triangle
      was two gradients in `currentColor` — the select's own ink — so it was
      both a different SHAPE and a different WEIGHT from the mark Home ends
      its rows with. The app has one "there is more here" glyph and this is
      it, rotated a quarter turn because this one opens downwards.

      5.5 x 3 at 12px from the edge, so it occupies 12-17.5px of the 30px of
      right padding and never touches the longest option. It is a small mark;
      so is the one on Home, and matching it was the instruction. */
  background-image: var(--caret-chevron);
  background-position: right 12px center;
  background-size: 5.5px 3px;
  background-repeat: no-repeat;
}
/*  HOVER AND FOCUS COME WITH IT. Removing `class="input"` from the selects
    took `.input:hover` and `.input:focus` with it, so they are restated here
    rather than left to be discovered as a missing focus ring. Same values:
    this is the same system, said once for selects. */
.app select:hover, .login-screen select:hover { border-color: var(--border-strong); }
.app select:focus, .login-screen select:focus {
  outline: none; border-color: var(--text-secondary);
  background-color: var(--surface);
  box-shadow: 0 0 0 3px var(--surface-raised);
}
.app select:focus-visible, .login-screen select:focus-visible {
  outline: 2px solid var(--focus); outline-offset: 2px;
}

/* --- route builder — RETIRED 27 August 2026 ----------------------- */
/* A route existed to tell the engine which machines a job passes through and
   how narrow the chain is. Nothing reads it now that a person writes the price.
   Kept, not deleted: the width arithmetic is the part worth keeping if
   automatic pricing returns. */
.route-layout { display: grid; grid-template-columns: 250px minmax(0, 1fr); gap: 20px; align-items: start; }
.route-picker { display: flex; flex-direction: column; }
.route-picker-item {
  font: inherit;
  text-align: left;
  padding: 11px 16px;
  border: 0;
  border-bottom: 1px solid var(--border);
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
}
.route-picker-item:last-child { border-bottom: 0; }
.route-picker-item:hover { background: var(--surface-hover); color: var(--text); }
.route-picker-item[aria-pressed="true"] { background: var(--surface-sunken); color: var(--text); font-weight: 600; }
.route-picker-sub { display: block; font-size: 11.5px; font-weight: 400; color: var(--text-muted); margin-top: 2px; }

/* the resolved usable width, or an honest statement that it cannot resolve */
.route-result { padding: 16px 18px; border-bottom: 1px solid var(--border); }
.route-result-label { font-size: 12px; color: var(--text-muted); margin: 0 0 4px; }
.route-result-value { font-size: 28px; font-weight: 600; letter-spacing: -.02em; margin: 0; }
.route-result-why { font-size: 12.5px; color: var(--text-secondary); margin: 5px 0 0; }
.route-result-unresolved { border-left: 3px dashed var(--border-strong); }
.route-result-unresolved .route-result-value { font-family: var(--font); font-size: 17px; }

.route-chain { list-style: none; margin: 0; padding: 0; counter-reset: route; }
.route-step {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 18px;
  border-bottom: 1px solid var(--border);
}
.route-step:last-child { border-bottom: 0; }
.route-step-index {
  font-size:12px ;
  color: var(--text-muted);
  width: 20px;
  flex: none;
}
.route-step-main { flex: 1; min-width: 0; }
.route-step-name { font-weight: 500; margin: 0; }
.route-step-kind { font-size: 11.5px; color: var(--text-muted); margin: 2px 0 0; }
.route-step-width {
  font-variant-numeric: tabular-nums;
  font-size:13.5px ;
  white-space: nowrap;
  min-width: 92px;
  text-align: right;
}
.route-step-missing { font-family: var(--font); font-size: 12.5px; font-weight: 600; }
.route-step-controls { display: flex; gap: 6px; flex: none; }
.route-move {
  font: inherit;
  font-size: 13px;
  line-height: 1;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text-secondary);
  cursor: pointer;
}
.route-move:hover { background: var(--surface-hover); color: var(--text); }
.route-move:disabled { opacity: .35; cursor: default; }
.route-limit {
  display: inline-flex;
  align-items: center;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: 4px;
  border: 1px solid var(--text);
  color: var(--text);
  white-space: nowrap;
}
.route-step-fixed { background: var(--surface-sunken); }
.route-add { display: flex; align-items: center; gap: 10px; padding: 14px 18px; border-top: 1px solid var(--border); }

/* --- inquiry board ------------------------------------------------ */
/* Six columns, one per state. Follow-up is never a seventh column: it is
   quote_sent plus a due date, surfaced as a filter above the board. */
.pipeline {
  display: grid;
  grid-auto-flow: column;
  /*  260, NOT 180. The floor decides how narrow a lane is allowed to get
      before the board starts scrolling, and 180px is under what a board card
      needs: at a 924 viewport it produced four 180px lanes in 608px, each too
      narrow for the controls inside it. The phone case is handled by its own
      rule below — one lane per viewport with a glimpse — and this is the same
      complaint at the width between. Desktop is unchanged: 1400 already
      resolved these to 262px. */
  grid-auto-columns: minmax(260px, 1fr);
  gap: 16px;
  align-items: start;
  overflow-x: auto;
  padding-bottom: 4px;
}
.pipeline-column {
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  min-width: 0;
}
.pipeline-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
  padding: 11px 13px;
  border-bottom: 1px solid var(--border);
}
.pipeline-state { font-size: 12px; font-weight: 600; margin: 0; }
.pipeline-owner { font-size: 10.5px; letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted); margin: 3px 0 0; }
.pipeline-count { font-size: 12px; color: var(--text-muted); }
.pipeline-body { display: flex; flex-direction: column; gap: 10px; padding: 12px; min-width: 0; }
.pipeline-empty { font-size: 12px; color: var(--text-muted); margin: 0; padding: 2px 0 6px; }

.inquiry-card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 9px;
  box-shadow: var(--card-shadow);
}
.inquiry-ref { font-size: 11px; color: var(--text-muted); margin: 0; }

/* --- the card is one tap target ------------------------------------ 6 Sep 2026
   Hand-maintained. The board's cards carried a 12.5px anchor on the reference
   and nothing else clickable, so the tap target on a phone was a line of text
   inside a 229px card. This makes the whole card the target WITHOUT nesting
   anything interactive: one real anchor, stretched over the card by a
   pseudo-element. No JavaScript — the anchor is a real link and works with
   middle-click, cmd-click and "open in new tab" like any other.

   THE ANCHOR IS THE PRODUCT TITLE, not the reference. The reference is now
   plain text (see below for why that matters), and the title is the line a
   person reads to know what the card is.

   THE WORKFLOW BUTTONS SIT ABOVE THE OVERLAY. `.inquiry-actions` is raised, so
   a click on "Ask for artwork" hits the form and never the link beneath it. They
   are SIBLINGS of the anchor, never inside it: a button inside an <a> is
   invalid HTML and browsers disagree about which one fires.

   TEXT SELECTION — THE KNOWN COST, AND WHAT IS DONE ABOUT IT. An overlay that
   covers the card sits above its text, so dragging to select the reference
   selects nothing. That matters here: MK-I-FAKE-0074 is the thing somebody
   copies into WhatsApp. So `.inquiry-ref` is RAISED ABOVE THE OVERLAY and stays
   selectable. The cost is that clicking exactly on the reference does not
   follow the link — every other part of the card does. That is the right way
   round: the reference is the one line you want to copy, and it is the one line
   whose click does nothing. */
.card-link { color: inherit; text-decoration: none; }
.card-link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}
.card-link:hover { text-decoration: underline; text-underline-offset: 2px; }
/* Raised over the stretched overlay. The actions must be clickable; the ref
   must be selectable. Everything else is click-through, which is the point. */
.inquiry-actions, .inquiry-ref { position: relative; z-index: 1; }
/* THE OUTLINE GOES ROUND THE WHOLE CARD, not round the title. A keyboard user
   tabbing the board is moving between CARDS, and an outline hugging one line of
   text does not show which card is focused. :focus-visible only, so a mouse
   click does not draw it. */
.inquiry-card:has(.card-link:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}
.card-link:focus-visible { outline: none; }
.inquiry-product { font-size: 13.5px; font-weight: 600; margin: 0; }
.inquiry-qty { font-size: 11.5px; color: var(--text-muted); margin: 2px 0 0; }
.inquiry-price { font-size:14px ; font-weight: 600; font-variant-numeric: tabular-nums; }
.inquiry-unpriced { font-size: 12px; color: var(--text-muted); }
.inquiry-note { font-size: 12px; color: var(--text-secondary); margin: 0; }
/* Overdue is a date, not a lane. Never coloured. */
.inquiry-due {
  display: inline-flex;
  align-self: flex-start;
  font-size: 11.5px;
  font-weight: 600;
  padding: 3px 8px;
  border: 1px dotted var(--border-strong);
  border-radius: 4px;
  color: var(--text);
}
.inquiry-actions { display: flex; flex-direction: column; gap: 6px; }
/* .btn-sm is nowrap; inside a board column it must wrap or it sets the column floor */
.inquiry-actions .btn { width: 100%; white-space: normal; }
.confirm-pop-card { top: 8px; left: 8px; right: 8px; width: auto; padding: 12px; }
/* Inside a 180px board column the two actions cannot sit on one row:
   .btn-sm is nowrap, so a flex-end row overflows the popover to the left. */
.confirm-pop-card .confirm-actions { flex-direction: column-reverse; align-items: stretch; }
.confirm-pop-card .btn { width: 100%; white-space: normal; }

/* --- applied filter banner ---------------------------------------- */
/* A row on Home links here PRE-FILTERED. This states the filter that arrived
   with the link and offers the only way back to the full list. */
.list-filter {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border: 1px solid var(--text);
  border-radius: var(--radius);
  background: var(--surface);
  font-size: 13px;
}
.list-filter-label { font-weight: 600; }
.list-filter-clear {
  margin-left: auto;
  font: inherit;
  font-size: 12.5px;
  /* Was the accent (then --accent-text), same reason as .constant-note-edit. */
  font-weight: 600;
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 2px;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* --- messages ----------------------------------------------------- */
/* .error is a failure the person must resolve; .notice is an outcome to read;
   .hint is standing guidance. Only .error uses the alarm weight. */
.error {
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text);
  padding: 10px 12px;
  border: 1px solid var(--text);
  border-left-width: 3px;
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  margin: 0;
}
.notice {
  display: flex;
  align-items: baseline;
  gap: 12px;
  font-size: 12.5px;
  color: var(--text-secondary);
  padding: 10px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  margin: 0;
}
.hint { font-size: 11.5px; color: var(--text-muted); margin: 0; }

/*  A ONE-TIME PASSWORD on Settings: large enough to read aloud across a
    counter and copy by hand, in the mono face so l/1 and O/0 would differ
    even if the generator did not already leave them out. */
.staff-password p { margin: 0 0 8px; }
.staff-password p:last-child { margin-bottom: 0; }
.staff-password__value { font-family: var(--font-mono);
  font-size: 22px; font-weight: 700; letter-spacing: .06em; color: var(--text);
  user-select: all; }

/*  HOME'S SCHEMA LINE, the alarm weight with room under it before the
    signals. Its link is text-coloured and underlined: a bare <a> takes
    --accent, and New inquiry is already the screen's one accent thing. */
.home-schema { margin: 0 0 20px; }
.home-schema a { color: var(--text); text-decoration: underline;
  text-underline-offset: 2px; }

/* The field-level twin of .cell-error: the message and the input it belongs
   to are two things, so the wrapper marks the input and .error carries the
   words. Put has-error on the .field, never on the input itself. */
.field.has-error .field-label { font-weight: 600; }
.field.has-error .input,
.field.has-error .search {
  border-color: var(--text);
  border-width: 1px;
  box-shadow: inset 2px 0 0 var(--text);
}
.field.has-error .input:focus { box-shadow: inset 2px 0 0 var(--text), 0 0 0 3px var(--surface-sunken); }
.field-error { font-size: 11.5px; font-weight: 600; color: var(--text); margin: 0; }

/* --- forms -------------------------------------------------------- */
.form-card {
  background: var(--surface);
  border: 1px solid var(--card-border);
  box-shadow: var(--card-shadow);
  border-radius: var(--radius-lg);
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  /*  A form body has a working width — see "a form body has a working width"
      below, where this used to be a second `.form-card` rule. */
  max-inline-size: 760px;
}
.form-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  justify-content: flex-end;
  padding-top: 4px;
  border-top: 1px solid var(--border);
  margin-top: 4px;
}
.form-actions .hint { margin-right: auto; }

/* --- grid cells --------------------------------------------------- */
/* Contract: .cell-error is the CELL, .cell-error-message is the TEXT.
   Two things, two classes — do not collapse them. */
/* The grid validates on blur and returns the row with the message beside the
   cell, so the error state lives on the cell, not on a banner elsewhere. */
.cell-error { position: relative; background: var(--surface-sunken); }
.cell-error .cell-input,
.cell-error .input {
  border: 1px solid var(--text);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
.cell-error .cell-input:focus { box-shadow: 0 0 0 3px var(--surface-raised); }
.cell-error-message {
  display: block;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--text);
  padding: 3px 2px 0;
  text-align: left;
  max-width: 148px;
}
/* A cell that is stopping the business, distinct from one that is merely empty. */
.cell-blocking { box-shadow: inset 2px 0 0 var(--text); }
/* Hatched, with the reason written in the cell — the wording is the template
   author's to change, so it is never set from a content property here. */
.cell-void { background: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--surface-sunken) 5px, var(--surface-sunken) 6px); }
.cell-void span { font-size: 11px; color: var(--text-muted); }
.cell-ink { text-align: right; font-variant-numeric: tabular-nums; }
.cell-mid { vertical-align: middle; }
.cell-note { font-size: 12px; color: var(--text-secondary); max-width: 32ch; }
.cell-routes { font-size: 12.5px; color: var(--text-secondary); white-space: nowrap; }
.cell-wide { min-width: 232px; }
.num { text-align: right; font-family: var(--font-mono); font-size: calc(1em * var(--mono-scale)); }
/* MONO: a column. Measured on /machines — 8 of these sharing an edge, the
   route counts "20", "5", "9" down the list. The class is NAMED for the effect
   it was not producing: on General Sans, which has no tabular figures,
   `font-variant-numeric: tabular-nums` is inert, so a utility called .tabular
   made nothing tabular. The mono stack has them — every digit 361.23px. */
.tabular { font-size:1em ; font-variant-numeric: tabular-nums; }
.sticky-col {
  position: sticky;
  left: 0;
  z-index: 2;
  background: var(--surface);
  border-right: 1px solid var(--border);
}
thead .sticky-col { z-index: 4; background: var(--surface-sunken); }

/* --- list filters ------------------------------------------------- */
/* A SEARCH FIELD IS AN INPUT, and it is the one field a person touches on Jobs,
   Inquiries and Materials — so it takes .input's treatment and not its own.
   It was left on --surface / --border-strong / --radius-sm when .input moved:
   lighter than the card it sits in, which is the raised-slab reading the input
   rules exist to prevent, on the wrong radius step. If a search field ever
   needs to differ from .input, the difference goes in this comment first. */
.search {
  font: inherit;
  min-height: 40px;
  font-size: var(--fs-secondary);
  min-width: 220px;
  padding: 7px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  color: var(--text);
}
.search::placeholder { color: var(--text-muted); }
.search:hover { border-color: var(--border-strong); }
.search:focus { outline: none; border-color: var(--text-secondary); background: var(--surface); box-shadow: 0 0 0 3px var(--surface-raised); }
.archived { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--text-secondary); }
/* An archived record is still readable, just not part of the working set. */
.is-archived { opacity: .55; }
.is-archived .cell-strong { font-weight: 400; }
/* Scheduled for later, so not part of today's work. */
.is-later { color: var(--text-muted); }
.is-later .cell-strong { color: var(--text-muted); font-weight: 400; }

/* --- mobile chrome ------------------------------------------------ */
/* Daily use is phone-first, per DESIGN.md §8.

   .mobile-bar is a ROW OF .app, not a sibling of it. Chrome that replaces the
   sidebar belongs to the shell that owns the sidebar: same grid, same token
   scope, and it cannot stick to the viewport independently of the app it
   navigates. Sticky top:0 is correct for THIS element because everything it
   holds is a page title and the wordmark — a header. The bottom nav strip
   DESIGN.md §8 calls for is a different component and needs its own class;
   this one cannot be both.

   Hidden by default so the desktop shell never renders two navigations. */
/* DESIGNED FOR THE DARK BASE, not the desktop chrome shrunk. It is a plane a
   step above the page with no border and a quiet elevation — the surface step
   separates it while it is at rest, and the shadow keeps the boundary readable
   once content scrolls under it. It inherits --page-x, so the title sits on
   the SAME axis as everything in .page below it. */
.mobile-bar {
  display: none;
  grid-column: 1 / -1;
  position: sticky;
  top: 0;
  z-index: 10;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px var(--page-x);
  background: var(--surface);
  border-bottom: 0;
  box-shadow: var(--shadow-plane);
}
.mobile-head { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.mobile-head h1 { font-size: var(--fs-section); font-weight: 600; letter-spacing: -.01em; margin: 0; }
.mobile-head p { font-size: var(--fs-meta); color: var(--text-muted); margin: 0; }

/* Sign out belongs to the HEADER, not the strip. .mobile-nav is nav.py's list of
   destinations; signing out is an account action, and in that strip it would
   both read as a section and scroll out of reach. On desktop Sign out sits in
   .sidebar-foot beside the identity block, and .mobile-bar is that block's
   mobile counterpart — it already carries who and where you are.

   It takes the slot the wordmark held. At 380px the only way to leave the app
   outranks a decorative mark, and the wordmark's one function — the link to
   Home — is already the first item in .mobile-nav. */
.mobile-signout {
  flex: none;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 13px;
  font: inherit;
  font-size: var(--fs-secondary);
  font-weight: 500;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
}
.mobile-signout:hover, .mobile-signout:active {
  background: var(--surface-hover);
  border-color: var(--border-strong);
  color: var(--text);
}

/* --- mobile nav (the bottom strip) -------------------------------- */
/* DESIGN.md §8's bottom bar, and a SEPARATE component from .mobile-bar. That
   one is the header — title plus wordmark, sticky at top. This one is nav.py's
   items, fixed at the bottom. Two components, two positions, one breakpoint.

   Eight items, two groups, no room for group headings: the SETUP / DAILY split
   is carried by a rule drawn between Finishes and Stock, not by a label. Order
   is nav.py's order, unchanged.

   Eight text labels do not fit across 380px. The strip scrolls horizontally
   rather than shrinking labels below legibility or hiding half the IA behind a
   More menu — a partially cut item at the right edge is the affordance. There
   are no icons in this system, so labels cannot be traded for glyphs.

   One-handed on a shop floor: fixed inside thumb reach, 52px targets, snap
   points so a rough swipe still lands on an item, and the home-indicator inset
   kept clear. */
.mobile-nav {
  display: none;
  min-height: var(--strip-h);
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  gap: 2px;
  padding: 6px 10px calc(6px + env(safe-area-inset-bottom, 0px));
  background: var(--surface);
  border-top: 1px solid var(--chrome-border);
  box-shadow: 0 -8px 22px rgba(0, 0, 0, .3);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
}
.mobile-nav::-webkit-scrollbar { display: none; }
/* 52px tall and 72 wide before padding — comfortably past the 44px floor, and
   wide enough that "Customers" and "Machines" are not clipped. Inactive items
   are muted; only the current one comes up to --text. */
.mobile-nav-item {
  flex: none;
  min-width: 72px;
  min-height: 52px;
  scroll-snap-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 6px 12px;
  border-radius: var(--radius);
  font-size: 11.5px;
  font-weight: 500;
  color: var(--text-muted);
  white-space: nowrap;
}
/* The second group starts here. A rule, because a heading has nowhere to go. */
.mobile-nav-item[data-group-start] {
  margin-left: 10px;
  border-left: 1px solid var(--border);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding-left: 18px;
}
.mobile-nav-item:active { background: var(--surface-hover); color: var(--text); }
/* Current item: WEIGHT AND SURFACE. The 2px accent rule along the top edge came
   off 4 September 2026 — it was a third accent element on screens that already
   carry the wordmark period and one primary button, and the budget is two. An
   item is current because it is heavier and sits on its own surface; it does not
   need a colour to say so. The marker is never the label.
   Reads UP on the dark base, like every other selected state here. */
.mobile-nav-item[aria-current="page"] {
  background: var(--surface-raised);
  color: var(--text);
  font-weight: 600;
}
.mobile-nav-count { font-size: 10.5px; line-height: 1; color: var(--text-muted); }
.mobile-nav-item[aria-current="page"] .mobile-nav-count { color: var(--text-secondary); }

/* The one 900px query lives at the END of the app stylesheet, directly above the
   board block — see there. It must come after every component rule it
   overrides: written here, beside the mobile chrome, it sat above .work-row,
   .lines-table and .quote-layout in the cascade and lost to them at equal
   specificity, which is how the phone got one word per line. */


/* --- sidebar footer ----------------------------------------------- */
.sidebar-foot {
  margin-top: auto;
  padding-top: 14px;
  border-top: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.who { min-width: 0; }
.who strong { display: block; font-size: 13px; font-weight: 600; }
/* The role is a bare text node in _sidebar.html, so `.who span` never
   matched it. Style the element, not the wrapper it does not have. */
.who { font-size: 11.5px; color: var(--text-muted); }
.who span { display: block; font-size: 11.5px; color: var(--text-muted); }

/* --- login -------------------------------------------------------- */
/* Email + password, per auth.py. There is no reset flow, so this component
   carries no reset link — offering one is worse than omitting it. */
/*  LOGIN IS OUTSIDE THE SHELL, SO IT INHERITS NOTHING FROM IT.

    `.app` is where this stylesheet puts the typographic base — font-family,
    colour, size, line-height, antialiasing — and `.app *` is where
    box-sizing: border-box is set. login.html overrides the `body` block, so
    it has no `.app` anywhere on the page and was getting NONE of them:

      · the whole screen rendered in TIMES, the serif fallback, because
        --font was defined and nothing applied it
      · text was pure #ffffff rather than --text #e0dad3
      · without border-box, .login-card's `width: 100%` plus its border
        overflowed the 20px gutter and the page scrolled sideways by 3px at
        390

    Measured, not assumed: the brief for this batch said to check what login
    inherits rather than assume the shell's tokens reach it, and they did not.
    A stranger signing in is the only person who sees this screen, and it was
    the one screen in the system not using the typeface. */
.login-screen {
  min-height: 100%;
  display: grid;
  place-items: center;
  padding: 40px 20px;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-body);
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
}
.login-screen, .login-screen *, .login-screen *::before, .login-screen *::after {
  box-sizing: border-box;
}
.login-screen :focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 3px; }
/*  44px, LIKE EVERY OTHER FIELD. They measured 322x40 — the shell's
    `.make-record-page ... { min-height: 46px }` never reaches here either,
    for the same reason as the typeface. Two fields and a button is the whole
    screen; there is no excuse for one of them being the wrong size. */
.login-screen .input { min-block-size: 44px; }
.login-card {
  width: 100%;
  max-width: 384px;
  background: var(--surface);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--card-shadow);
  padding: 32px 30px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.login-head { display: flex; flex-direction: column; gap: 4px; }
.login-title { font-size: 18px; font-weight: 600; margin: 14px 0 0; }
.login-note { font-size: 13px; color: var(--text-secondary); margin: 0; }
.login-form { display: flex; flex-direction: column; gap: 14px; }
.login-form .btn { min-height: 44px; }
.login-foot { font-size: 11.5px; color: var(--text-muted); margin: 0; }

/* --- home: readiness to-do — SCREEN retired 27 August 2026 -------- */
/* CORRECTED 29 August 2026. What retired was this SCREEN and the LANES, not the
   readiness vocabulary. "Needs a roll width", "needs a printable width",
   "needs setup minutes" state something a person can act on whether or not an
   engine exists; they are live in the database and they are the only real data
   the dashboard currently holds. Home's Setup band IS this vocabulary under a
   new heading, and it reads the same computation. Do not delete the source. */
/* Home was a WHOLE SCREEN of what blocked the engine — "11 machines need a
   printable width" and eighteen more like it. What changed is that the screen
   became a work queue: a studio in motion opens Home to see what needs
   attention today, not a configuration checklist. So the readiness rows moved
   DOWN into the sixth band, at the most receded tier, where they shrink as they
   clear. The vocabulary did not go with the engine — it is live, it is the only
   real data here, and the Setup band reads it. Kept, not deleted. The retired
   frame at the bottom of the board still renders the old screen. */
/* The headline is the most important sentence on the screen. */
.readiness-headline { font-size: 20px; font-weight: 600; letter-spacing: -.01em; margin: 0; }
.readiness-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.todo-list { list-style: none; margin: 0; padding: 0; }
/* A dozen-plus rows is the normal case, so each row stays one line tall. */
.todo-item {
  display: flex;
  align-items: center;
  /*  THE WAY OUT WRAPS BELOW THE REASON, rather than squeezing it. The link
      is `white-space: nowrap`, so at 390 it kept its whole width and left
      the blocker's sentence ~130px — one word a line on the issue screen. */
  flex-wrap: wrap;
  gap: 8px 16px;
  padding: 11px 18px;
  border-bottom: 1px solid var(--border);
}
.todo-item:last-child { border-bottom: 0; }
.todo-item:hover { background: var(--surface-hover); }
/* Counts run to three digits — 105 products not switched on. Sized for it. */
.todo-count {
  font-size:19px ;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  min-width: 52px;
  flex: none;
  text-align: right;
}
.todo-main { flex: 1 1 16rem; min-width: 0; }
.todo-label { font-weight: 500; margin: 0; }
.todo-blocks { font-size: 12.5px; color: var(--text-muted); margin: 2px 0 0; }
/* Demoted 27 August 2026 with .constant-note-edit and .list-filter-clear. The
   budget shrank to two while this component was already retired, and an accent
   site sitting in retired CSS is one somebody resurrects by accident. */
.todo-link { font-size: 13px; font-weight: 500; color: var(--text); text-decoration: underline; text-underline-offset: 2px; white-space: nowrap; }
/* The row is real; the screen it would open is not built yet. Say so rather
   than offering a link that goes nowhere. */
.todo-pending { font-size: 12px; color: var(--text-muted); white-space: nowrap; }
.todo-link:hover { text-decoration: underline; text-underline-offset: 2px; }
/* The launch gate, made visible. Only reachable when every row above clears. */
.readiness-clear {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 22px 20px;
  background: var(--surface);
  border: 1px solid var(--text);
  border-radius: var(--radius-lg);
}
.readiness-clear-mark {
  width: 34px;
  height: 34px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--text);
  color: var(--bg);
  font-size: 17px;
  font-weight: 700;
}
.readiness-clear-title { font-size: 16px; font-weight: 600; margin: 0; }
.readiness-clear-counts { font-size: 13px; color: var(--text-secondary); margin: 3px 0 0; }
.todo-done { display: flex; align-items: center; gap: 12px; padding: 16px 18px; font-size: 13px; color: var(--text-secondary); }

/* --- home: the morning briefing ----------------------------------- */
/* Five bands: late, overnight, waiting on us, waiting on them, in production.
   STRUCTURE carries the grouping — a heading, a rule and spacing per band, and
   no fills anywhere. If five bands took five tints the red on Late would stop
   meaning late and start meaning decoration, and the one thing that has to be
   unmissable at 8am is what is late.

   Late is first because that is why the screen was opened. Its primacy is
   POSITION, not colour; the flags in its rows are the only overdue marks on
   the page. Every count is a link that opens exactly those records. */
/* Tighter than .page, because five bands have to fit 950px with nothing cut
   off. The band rules already separate the groups; the gap only has to keep
   them from touching. */
.page-brief { gap: 12px; }
.brief-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; padding: 0 16px; }
.brief-head .scope-marker { margin-bottom: 4px; }
.brief-greeting { font-size: 22px; font-weight: 600; letter-spacing: -.02em; line-height: 1.2; margin: 0 0 2px; }
/* ONE headline, not two. The greeting and the count were two statements of one
   fact 4px apart in size, and neither read first. The count is the headline;
   the name and date are one meta line above it. */
.brief-meta { font-size: 12.5px; line-height: 1.3; color: var(--text-muted); margin: 0 0 3px; }
.brief-date { font-size: 13px; line-height: 1.3; color: var(--text-secondary); margin: 0; }
/* THE ANSWER, ABOVE THE BANDS — 3 September 2026. At 08:00 the screen has to
   answer "is anything on fire" before it is read at all. One sentence, the
   largest type on the page, counting only the items that need a person in this
   studio: late jobs, overnight replies, and the work waiting on us. Nothing
   waiting on a customer is counted, because that is not the studio's morning.
   Summed from the same filters the bands render, never typed. */
.brief-lede { font-size: 26px; font-weight: 600; letter-spacing: -.025em; line-height: 1.25; color: var(--text); margin: 0; text-wrap: pretty; }
.brief-lede-sub { font-size: 13px; color: var(--text-muted); margin: 3px 0 0; }
/* The prose line under the greeting is gone. It stated in a whisper what the
   Late band now states at 46px, and two statements of one fact make neither
   of them the first thing read. (.head-actions is defined once, above; its
   second definition here was the one that won, `flex: none`.) */

/* Three tiers, because the bands are not equally urgent and that has to be
   visible without reading a heading. LEAD is late — the reason the screen is
   open. Default is work the studio can act on right now. QUIET is waiting on
   somebody else, or already running.

   The ranking is weight, size and space. No band has a fill.

   ONE BOX PER BAND, 3 September 2026. Each band is a card — surface, hairline,
   radius — so the six segments are separable at a glance. Nothing inside a band
   carries a box of its own: the rows are hairline-separated lines within it,
   which is what lets every figure in a band sit on one left edge with every
   label starting at the same place. Boxes inside boxes gave the page four left
   edges and made the whole screen read as one slab.

   COLOUR IS THE BAND BOX'S OWN LEFT EDGE, never a bar beside it. Two on the
   page: OVERDUE red on Late, HELD on Waiting on them. Overnight carries none —
   amber means chasing, and a reply clock is not chasing until a row's window is
   running out, when that row takes the triangle. Waiting on us, In production
   and Availability carry none; the absence is the information.

   RHYTHM BY TIER. Late is heaviest: --text border, raised in dark, 40px figure.
   Waiting on us is a hairline box at 30px. Overnight, the quiet pair and
   Availability are hairline boxes with progressively smaller figures and tighter
   rows. Same radius throughout; surface, border, figure size and row height
   vary. */
.band {
  display: flex;
  flex-direction: column;
  gap: 0;
  background: var(--surface);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  overflow: hidden;
}
.band-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; padding: 11px 16px 10px; border-bottom: 1px solid var(--border-strong); }
/* WHO MUST ACT, said in words. The band titles name a category; this names the
   demand, so the reader does not have to learn the ranking to use the screen.
   Type, not colour — the accent budget and the two state colours stand. */
.band-aside { margin-left: auto; display: flex; align-items: baseline; gap: 14px; }
.band-tier { font-size: 10.5px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: var(--text-muted); white-space: nowrap; }
/* A collapsed band's prose note earns nothing — the summary line below states
   the figures — and at 390 it wrapped to two lines, making the heads of the
   three no-action bands the heaviest chrome on the screen. */
.band.is-closed .band-note { display: none; }
.band-tier-act { color: var(--text); }
/* The three bands that need nothing open on request. Collapsed they state their
   figures in one line, so the phone screen answers "what needs me now" without
   scrolling and the context is still one tap away. */
/* 44px in BOTH dimensions. The inline padding is hit box, not visual weight,
   and the negative right margin keeps the label on the 16px line. */
.band-toggle {
  font-family: var(--font);
  font-size: 12px;
  font-weight: 500;
  color: var(--text-secondary);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  padding: 6px 10px;
  margin-right: -10px;
  min-height: 44px;
  min-width: 44px;
  cursor: pointer;
  white-space: nowrap;
}
.band-toggle:hover { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
.band-summary { font-size: 13px; color: var(--text-secondary); margin: 0; padding: 9px 16px 11px; text-wrap: pretty; }
.band-title { font-size: 11px; font-weight: 700; letter-spacing: .13em; text-transform: uppercase; margin: 0; }
.band-note { font-size: 12px; color: var(--text-muted); margin: 0; }
.band-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px 40px; align-items: start; }
.band-list { list-style: none; margin: 0; padding: 0; }
.stat { display: flex; align-items: center; gap: 14px; padding: 6px 0; border-bottom: 1px solid var(--border); }
.stat:last-child { border-bottom: 0; }
/* TWO KINDS OF NUMERAL, split 3 September 2026. The mono face with tabular
   figures is for COLUMNS — money, cell-num, nav counts — where digits in one
   row must sit over digits in the next. A standalone figure in a card or a
   band row is a DISPLAY figure: nothing lines up under it, so it takes the
   proportional text face. Mono here made a 2 look like a table cell that had
   lost its table. .metric-figure and .stat-count are display; .money, .cell-num,
   .nav-count and the rest stay tabular. */
/* Large and light. On a screen whose whole job is "how many of what", the
   figure is what the eye lands on and the label is secondary to it — so the
   count carries the size and the label carries the weight. */
/* ONE ROW, ONE ALIGNMENT, 3 September 2026. The row is a single anchor —
   figure, then label, then any flag — every part on one baseline-led flex line
   and the whole row the tap target at 44px minimum. The earlier shape had three
   alignments at once: a right-set number, a label wrapping under it, and a flag
   positioned at the row's end that landed on the label once it wrapped at 390.
   The flag now follows the label inside .stat-main, so it can never overlap it. */
.stat-row {
  display: flex;
  flex: 1 1 auto;
  width: 100%;
  align-items: flex-start;
  gap: 14px;
  min-height: 44px;
  padding: 7px 16px;
  color: var(--text);
}
a.stat-row:hover .stat-count { text-decoration: underline; text-underline-offset: 3px; }
/* THE FIGURE IS THE ANSWER, so it is the brightest thing in the row: --text at
   weight 600, with the label a step down in colour and weight. The earlier pass
   had the label bold and the number pale, which read the question louder than
   the answer. */
.stat-count {
  font-family: var(--font);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -.02em;
  line-height: 1.1;
  min-width: 34px;
  flex: none;
  text-align: left;
  color: var(--text);
}
.stat-main { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; padding-top: 3px; }
.stat-label { font-size: 13px; font-weight: 400; color: var(--text-secondary); margin: 0; }
.stat-detail { font-size: 12px; color: var(--text-muted); margin: 0; }
.stat-flag { align-self: baseline; }

/* A count is a ROW inside the band box: figure, then the label and its detail
   beside it, then any flag. No box of its own — that is what keeps the figures
   on one left edge and the labels on another. */
.metric-row { display: flex; flex-direction: column; }
.metric-card {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 9px 16px 10px;
  min-width: 0;
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.metric-card:last-child { border-bottom: 0; }
a.metric-card:hover { background: var(--surface-raised); }
.metric-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; padding-top: 3px; }
.metric-cap { display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 12px; }
.metric-name { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin: 0; }
.metric-figure {
  font-family: var(--font);
  font-size: 26px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -.03em;
  color: var(--text);
  min-width: 34px;
  flex: none;
  text-align: left;
}
.metric-sub { font-size: 12px; color: var(--text-muted); margin: 0; }

/* LEAD — late. The strongest edge on the screen and the largest figure. Jobs
   only, since 2 September 2026: no due date exists on an invoice and no terms
   on a customer, so nothing defines an overdue invoice yet. The row layout
   stays two-up so an invoice card can return the day that is decided. */
/* OUTRANKING IS ELEVATION, NOT AN OUTLINE. The lead tier rises to the raised
   surface and keeps its red edge; a near-white border on a dark card is the
   outlined-hole look at its worst. Folded out of the mode block 4 September
   2026 — the ranking and the figure sizes are unchanged. */
.band-lead { background: var(--surface-raised); border-color: var(--card-border); border-left: 3px solid var(--state-overdue); }
.band-lead .metric-card { background: var(--surface-raised); border-color: var(--border-raised); }
.band-lead .work-row { background: var(--surface-raised); border-color: var(--border-lead); }
/* The 3px edge comes out of the padding, so the text stays on the 16px line.
   .work-row was missing from this list, so late rows inside the lead band sat 3px off
   the axis the band's own head and cards were corrected onto — a single alignment axis
   is section 1 of the brief, and a compensation list is only as good as its last
   entry. Anything that gains a left edge inside this band joins it — but the ROW is
   corrected at its own rule further down, not here: it already sets padding as a
   SHORTHAND, and a longhand added to this list sat earlier in the sheet at equal
   specificity and was silently reset to 16px. One rule owns one element's padding. */
.band-lead .band-head, .band-lead .metric-card { padding-left: 13px; }
.band-lead .band-head { padding-bottom: 10px; border-bottom: 1px solid var(--text); }
.band-lead .band-title { font-size: 13px; letter-spacing: .16em; }
.band-lead .band-note { font-size: 12.5px; }
.band-lead .metric-name { font-size: 13.5px; color: var(--text); }
.band-lead .metric-figure { font-size: 40px; min-width: 44px; }
.band-lead .metric-main { padding-top: 6px; }
.band-lead .metric-sub { font-size: 12.5px; }
.band-lead .state-flag { font-size: 13px; }
/* The overdue total, at the size of a figure somebody is owed. Colour and
   size, no tint: a fill around it was decorating a number rather than
   carrying urgency, and it was the only fill on the screen. */
/* No consumer since 2 September 2026 — the overdue-invoices card left Late with
   the due date. Kept for the day an invoice can be overdue. */
.money-overdue { font-size: 21px; font-weight: 500; color: var(--state-overdue); }

/* Waiting on us: the one band the studio can clear. Its figure is a step under
   Late's and its border is a step over the quiet bands'. No colour — ours is
   the default. */
.band-act { border-color: var(--card-border); }
.band-act .metric-figure { font-size: 30px; min-width: 38px; }
/* Overnight: a reply clock. No band colour — amber means chasing, and a row
   takes the triangle only when its own window is running out. */

/* QUIET — waiting on them, and already running. Nothing here is actionable in
   the next ten minutes, so it recedes: smaller figures, a lighter rule, and
   the label at secondary colour. */
/* The quiet pair sits in a half-width column at every viewport, so its note
   always takes its own line under the title and aside \u2014 the same shape the
   narrow breakpoint gives every band. */
.band-quiet .band-aside { order: 2; }
.band-quiet .band-note { order: 3; flex: 1 0 100%; }
.band-quiet .band-head { border-bottom-color: var(--border); }
.band-quiet .band-title, .band-avail .band-title { font-size: 11px; color: var(--text-secondary); }
/* Rows inside the band box, hairline-separated. The figure is 20px and still
   the brightest thing in its row. */
.band-quiet .band-list { padding: 0; }
.band-quiet .stat { padding: 0; border-bottom: 1px solid var(--border); }
.band-quiet .stat:last-child { border-bottom: 0; }
.band-quiet .stat-row { gap: 12px; padding: 7px 16px; }
.band-quiet .stat-main { padding-top: 3px; }
/* HELD is the band box's own left edge; the rule comes out of the padding. */
.band-held { border-left: 3px solid var(--state-held); }
.band-held .band-head, .band-held .stat-row { padding-left: 13px; }
.band-quiet .stat-label { font-size: 12.5px; font-weight: 500; color: var(--text-secondary); }
.band-quiet .stat-detail { font-size: 11.5px; }
/* The two quiet bands share a row. Everything above them is a single column in
   priority order — a briefing is a sequence, and a 2×2 grid sent the eye down
   the left column and past half the screen. */
.band-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: start; }

/* --- home: availability, the sixth band --------------------------- */
/* REPLACES the Setup band, 3 September 2026. Setup counted engine inputs —
   roll width, cutter gap, printable width, cost — and the engine is parked, so
   166 items sat on Home and none of them blocked anything a person does today.
   Badges are blocking counts, never totals; that band was totals in the clothes
   of work. Availability states what is short or stopped RIGHT NOW: a product or
   material that cannot be supplied, a machine that cannot run. Three groups,
   one sunken list each, the quiet tier's row. A group with no rows drops; the
   band drops when all three do.

   Machine state is context, not a call to action — nothing is typed to fix a
   machine — so it carries .badge-state's quiet outline and no state colour.
   LOW STOCK IS NOT HERE: no threshold exists in the data, and a state the data
   cannot fill is not drawn. The readiness computation behind the old band is
   NOT deleted — see the retired block below; the Machines and Stock screens
   still read it. */
.band-avail .band-head { border-bottom-color: var(--border); }
/* Three groups in one band box, divided by a rule rather than by three boxes. */
.avail-groups { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: stretch; }
.avail-group { display: flex; flex-direction: column; gap: 0; min-width: 0; border-right: 1px solid var(--border); }
.avail-group:last-child { border-right: 0; }
.avail-group-title { font-size: 11px; font-weight: 500; letter-spacing: .04em; color: var(--text-muted); margin: 0; padding: 8px 16px 2px; }
/* The quiet tier's list, one step smaller: the most receded thing on the page. */
.avail-group .band-list { padding: 0; }
.avail-group .stat { padding: 0; border-bottom: 1px solid var(--border); }
.avail-group .stat:last-child { border-bottom: 0; }
/* Denser type, but the 44px hit floor from the base rule stands \u2014 these are
   anchors like every other band row. */
.avail-group .stat-row { gap: 12px; padding: 5px 16px; }
.avail-group .stat-count { font-size: 17px; font-weight: 500; min-width: 34px; color: var(--text-secondary); }
.avail-group .stat-main { padding-top: 2px; }
.avail-group .stat-label { font-size: 12.5px; color: var(--text-secondary); }

/* --- home: setup, the sixth band — RETIRED 3 September 2026 ------- */
/* The BAND retired; the readiness vocabulary and computation did not (see the
   readiness block above and Machines / Stock, which still read it). Rules kept
   in place, unread by any shipping screen, so the shape is on record. */
/* Added 29 August 2026. The five work bands answer "what needs attention
   today". This one answers "what is blocking setup", and against a database
   with nothing feeding it yet, it is effectively the whole screen. It is built
   to shrink: eleven rows at the bottom today, a quiet line under a leading
   Late band in six months. No switchover and no day when somebody decides to
   change screens.

   A LIST, not a card row. .metric-row is repeat(auto-fit, minmax(0, 1fr)) —
   every track fits, so grid makes one column per card and never wraps. The work
   bands hold two or three; setup holds eleven, which on a 1440px screen is
   eleven ~110px columns. The argument does not depend on the number: it was
   nineteen rows when this was written and the shape was wrong then too. The list
   also carries three things the card shape cannot: a row whose screen is not
   built yet (.todo-pending, and no anchor to offer), a three-digit count
   (.todo-count is already sized for it), and eleven rows at one line each. It looks quieter than the five bands
   above, which is right for the most receded tier — this is a different kind of
   work, not the same kind ranked lower.

   TWO SUB-GROUPS, not two bands. product_missing.typeable is a real database
   distinction — someone types a number and it is solved, against a person fixes
   a machine — and it tells the studio what they can clear at a keyboard this
   afternoon. It cannot be a colour flag: colour carries urgency only, OVERDUE
   on Late and CHASING where a reply window is running out. So it is structure,
   one panel each, inside ONE band that appears and vanishes as a unit. */
.band-setup { gap: 12px; }
.band-setup .band-head { border-bottom-color: var(--border); padding-bottom: 5px; }
.band-setup .band-title { font-size: 10.5px; color: var(--text-muted); }
.band-setup .panel-head { flex-wrap: wrap; gap: 4px 12px; }
/* Receded: the quiet tier's size and colour for the counts, not a metric's. */
.band-setup .todo-count { font-size: 17px; color: var(--text-secondary); }
.band-setup .todo-label { font-size: 13.5px; font-weight: 400; }
.band-setup .todo-item { padding: 9px 18px; }
/* A row carrying .todo-blocks is two lines tall, so the count aligns to the
   first line rather than to the middle of both. */
.band-setup .todo-item { align-items: flex-start; }
.band-setup .todo-count { line-height: 1.25; }

/* Home with nothing live. Not a band — a band with a zero in it is a component
   stating that it has nothing to state, and Overnight is the sharp case: "0
   arrived since yesterday evening" over a blank list, every morning, for
   months, from a band whose whole subject is elapsed time. So bands are absent
   when empty, and this one line replaces all five. It is a .hint, the standing-
   guidance weight, because it is a fact about the studio and not a task. */
.band-nowork { padding: 13px 16px 14px; background: var(--surface); border: 1px solid var(--card-border); border-radius: var(--radius); box-shadow: var(--card-shadow); }
.band-nowork p { margin: 0; }
.band-nowork-line { font-size: 14px; color: var(--text-secondary); }

/* Overnight states elapsed time per item rather than a count: a WhatsApp
   message at 11pm sits unanswered until 8am, and Meta's free reply window is
   24 hours from that message. How long it has waited is the fact that matters. */
.overnight-list { display: flex; flex-direction: column; gap: 0; list-style: none; margin: 0; padding: 0; }
/* Overnight rows: lines in the band box, not boxes of their own. */
.overnight-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 9px 16px 10px;
  border-bottom: 1px solid var(--border);
}
.overnight-row:last-child { border-bottom: 0; }
.overnight-row > :first-child { flex: 1 1 auto; min-width: 0; }
.overnight-who { font-size: 13.5px; font-weight: 500; margin: 0; }
/* Carries a class, so it keeps body colour and takes no underline until hover.
   The accent budget is the wordmark period and the primary button; a row link
   that opened a record in accent would spend it forty times a screen. */
.record-link:hover { text-decoration: underline; text-underline-offset: 2px; }
.overnight-what { font-size: 12px; color: var(--text-muted); margin: 2px 0 0; }
.overnight-right { flex: none; display: flex; align-items: baseline; gap: 13px; text-align: right; }
.overnight-waited { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
.overnight-window { font-size: 11.5px; color: var(--text-muted); }

/* --- work rows: jobs and invoices --------------------------------- */
/* One row component for both screens. A job is a list sorted by WHEN, not a
   board of columns to drag cards between: two people with nine live jobs need
   to know what is late, and status is a field on the row rather than a place a
   card lives. The due date leads the row because it is the sort key.

   Cards, not hairlines, per the surface system. No categorical colour: state
   is a .tag, and the only coloured thing on either screen is overdue. */
.work-list { display: flex; flex-direction: column; gap: 7px; }
.work-row {
  display: grid;
  grid-template-columns: 132px minmax(0, 1fr) 150px 132px;
  align-items: center;
  gap: 18px;
  padding: 11px 16px;
  background: var(--surface);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
}
a.work-row:hover { background: var(--surface-raised); border-color: var(--card-border); }
/* MONO, 6 September 2026, BECAUSE GENERAL SANS HAS NO TABULAR FIGURES.
   Measured at 100px: digit 1 is 215.41px where 0 is 368.41px, and
   `font-variant-numeric: tabular-nums` changes those numbers NOT AT ALL — the
   face carries no tnum, so the property was inert wherever it sat on General
   Sans. The system mono stack does have them: every digit 361.23px.
   THIS ONE IS A COLUMN, which is why it moves rather than dropping the
   property. Measured on /invoices: 5 dates sharing one edge; on
   /inquiries/104: 2. Dates that do not line up in a list read as a rendering
   fault. */
.work-when { font-size: 13.5px; font-weight: 600; font-variant-numeric: tabular-nums; }
.work-when-sub { font-size: 11.5px; font-weight: 400; color: var(--text-muted); display: block; margin-top: 1px; }
.work-what { min-width: 0; }
.work-ref { font-size: 12px; color: var(--text-muted); }
.work-name { font-size: 13.5px; font-weight: 500; margin: 1px 0 0; }
.work-meta { font-size: 12px; color: var(--text-muted); margin: 1px 0 0; }
.work-side { font-size: 12.5px; color: var(--text-secondary); min-width: 0; }
.work-money { text-align: right; }
.work-total { font-family: var(--font-mono); font-size: calc(14px * var(--mono-scale)); font-weight: 600; font-variant-numeric: tabular-nums; }
/* MONO: a column. Measured on /invoices — 2 of these sharing a right edge,
   "32.92 left" over "969.30 left". General Sans has no tabular figures (digit
   1 is 215.41px where 0 is 368.41px, and font-variant-numeric changes neither),
   so on that face the property was inert and the outstanding figures did not
   line up under the totals above them. */
.work-split { font-family: var(--font-mono); font-size: calc(11.5px * var(--mono-scale)); color: var(--text-muted); margin: 2px 0 0; font-variant-numeric: tabular-nums; }
/* The late tier's rows carry the strong edge, like Home's lead cards. Size and
   edge, never a fill — a tinted row would put fifteen jobs on one footing. */
/* 12px LEFT, not 13px. The row's text has to land on the same axis as .band-head
   and .metric-card, and the row offsets its content by ONE MORE LAYER than they
   do: the band's 3px edge, plus .work-row's own 1px border. 13px put the text at
   25px against their 24px. A COMPENSATION HAS TO ACCOUNT FOR EVERY LAYER THAT
   OFFSETS, not just the one it was written for — count the border, the padding
   and the edge, and check the number against a sibling rather than deriving it.
   It lives in this shorthand and not in the compensation list higher up: a
   padding-left longhand there is earlier in the sheet at equal specificity
   (0,2,0) and this shorthand resets it, so that fix rendered as no fix at all. */
.band-lead .work-row { border-color: var(--state-overdue); padding: 13px 16px 13px 12px; }
.band-lead .work-when { font-size: 15px; color: var(--state-overdue); }
.band-lead .work-name { font-size: 14.5px; font-weight: 600; }
.group-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.group-count { font-size: 12px; color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* --- issuing an invoice ------------------------------------------- */
/* MIRA does not permit two tax invoices for one transaction, so a correction is
   a credit or debit note and issuing cannot be undone. The weight lives in the
   design, not in a dialog somebody clicks through on reflex: the gate states
   what becomes permanent, what cannot be taken back, and the remedy if the
   figure is wrong. It sits beside the document as it will read, on the preview
   screen reached from a job.

   RETIRED 2 September 2026: .draft-row and .draft-mark. There is no draft
   invoice — the order is the draft, and the preview reads the order. Kept
   commented so the shape is on record; nothing renders them.
.draft-row { display: grid; grid-template-columns: 132px minmax(0, 1fr) 150px 132px auto; align-items: center; gap: 18px; padding: 12px 16px; border: 1px dashed var(--border-strong); border-radius: var(--radius); background: var(--surface-sunken); }
.draft-mark { font-size: 10.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted); }
*/
/*  THE ISSUE RAIL IS A RAISED SURFACE, NOT AN OUTLINED ONE.

    It carried `border: 2px solid var(--text)` — a 2px near-white outline, the
    heaviest treatment on any surface in the system, on a screen that also
    holds a warning notice and a document preview. Three things competed and
    the outline won on weight alone rather than on importance. The one thing
    that should carry weight here is the Issue button, which is the single
    accent primary action.

    The surface role is what distinguishes it now: raised, with the ordinary
    border, and STICKY so it stays with the document as a long preview
    scrolls — which is what an action rail is for and what the outline was
    standing in for. */
.issue-gate {
  display: flex;
  flex-direction: column;
  gap: 13px;
  padding: 19px 22px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-raised);
  box-shadow: var(--shadow-card);
}
@media (min-width: 900px) {
  /*  STICKY ONLY WHERE THERE IS A COLUMN TO BE STICKY IN. Below 900 the rail
      stacks under the document and sticking it would pin a tall form over
      the thing it acts on. 24px clears the topbar. */
  .issue-gate { position: sticky; top: 24px; }
}

/*  PRICE CHANGED — the two figures, scannable, with the carried one marked.
    They were inside a sentence, which meant reading a paragraph to find out
    which number the document would carry. */
.price-changed { display: flex; flex-direction: column; gap: 10px; }
.price-changed__head { margin: 0; font-size: 14px; font-weight: 700; }
.price-changed__pair {
  display: flex; flex-wrap: wrap; gap: 8px 28px; margin: 0;
}
.price-changed__pair > div { display: flex; flex-direction: column; gap: 2px; }
.price-changed__pair dt {
  font-size: 11px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-muted);
}
.price-changed__pair dd { margin: 0; font-size: 19px; font-variant-numeric: tabular-nums; }
/*  The carried figure is the one the document takes. It is marked by WEIGHT,
    not by colour: amber already means "attention" on this notice and red
    means overdue, so a third colour here would say something the palette has
    not agreed. */
.price-changed__carried dt { color: var(--text); }
.price-changed__carried dd { font-weight: 600; }
.price-changed__note { margin: 0; font-size: 13px; line-height: 1.5; }
.issue-title { font-size: 17px; font-weight: 700; letter-spacing: -.01em; margin: 0; }
.issue-figure { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; }
.issue-amount { font-size: 29px; font-weight: 500; font-variant-numeric: tabular-nums; }
.issue-terms { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
.issue-term { display: grid; grid-template-columns: 15px minmax(0, 1fr); gap: 9px; font-size: 13px; line-height: 1.5; }
.issue-term-mark { font-weight: 700; }
.issue-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding-top: 3px; }
.issue-note { font-size: 12px; color: var(--text-muted); margin: 0; }

/* --- documents: invoices, credit notes, debit notes --------------- */
/* Nothing on these screens edits a document. The index is .work-row, shared
   with Jobs; the document itself reuses the quote's .lines-table and .totals so
   a quote and the invoice made from it read the same way. Two seller lines are
   always printed: the registered business name over the legal entity. */
/* ══ HOME'S SHAPES, translated from the approved mockup, 5 September 2026 ══
   app/globals.css + app/page.tsx. TRANSLATED, NOT TRANSPLANTED: no framework,
   no build step, no utility classes — hand-written CSS on the tokens already in
   this sheet. Three shapes the app does not have, which is why Home was built
   as four stacked rows and a flat table.

   TYPE IS THIS SHEET'S SCALE, NOT THE MOCKUP'S. Its most common size is 10px;
   it has thirteen 9px rules and one at 8px. The written brief asked for body
   13.5-14 and metadata 11.5-12 and said not to make everything small because it
   is an operational tool — and a prototype reviewed on a laptop renders at a
   different apparent scale than a studio screen. Every size below is a token:
   --fs-secondary for a card's label, --fs-meta for the line under it, and
   display figures OFF the scale so they dominate. Where the mockup and the brief
   disagree on type, the brief wins; on colour and shape, the mockup wins. */

/* A STATE MARK IS RECORD URGENCY. Not .lane-glyph, which is pricing readiness
   and retired with the engine — the two vocabularies were close enough to merge
   by accident and mean different things. Shape carries the meaning alongside
   colour, so neither depends on hue: filled diamond overdue, hollow circle
   chasing, hollow diamond held, filled dot neutral. */
.state-mark { width: 8px; height: 8px; flex: none; display: inline-block; border: 1.5px solid var(--text-muted); }
.state-mark-overdue { transform: rotate(45deg); border-color: var(--state-overdue); background: var(--state-overdue); }
.state-mark-chasing { border-radius: 50%; border-color: var(--state-chasing); }
.state-mark-held { transform: rotate(45deg); border-color: var(--state-held); }
.state-mark-neutral { border-radius: 50%; border-color: var(--text-muted); background: var(--text-muted); }
/*  A CONFIRMED STATE ON THE MARKER, NOT ON ITS PANEL. Operational is 16 of
    24; a green panel would claim the floor is fine. The count beside it is
    unchanged and carries the rest. */
.state-mark-good { border-radius: 50%; border-color: var(--state-good); background: var(--state-good); }

/*  THE SYSTEM'S OWN LIMITATION, kept OUT of the state pill beside it.
    "Nothing records which machine a job runs on" is a fact about this
    software, not about the two jobs in production — inside the capsule it
    would read as a caveat on the figure. */
.floor-note { display: flex; align-items: flex-start; gap: 7px; }
.floor-note__icon {
  flex: none; inline-size: 15px; block-size: 15px; margin-block-start: 1px;
  border: 1px solid var(--border-strong); border-radius: 999px;
  font-size: 10px; font-weight: 700; line-height: 13px; text-align: center;
  color: var(--text-muted);
}

/* ── 1. THE SIGNAL CARD, FOUR ACROSS ──
   Four counts, equal weight, one glance — the shape Home needs and does not
   have. Four STACKED rows say "read these in order"; four cards side by side
   say "these are the four things", which is what the screen means. */
.signal-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.signal {
  display: flex;
  flex-direction: column;
  min-height: 150px;
  padding: 18px 20px;
  border: 1px solid var(--card-border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--card-shadow);
  color: inherit;
  font: inherit;
  text-align: left;
  text-decoration: none;
}
a.signal:hover, button.signal:hover { background: var(--surface-hover); }
/* THE URGENT CARD IS THE ONE TINTED GROUND IN THE SHEET, and it is two stops of
   an existing state surface rather than a new colour — the tint fades out so the
   card still reads as one of the four, not as an alert box pasted over them.
   Its border is --border-lead, the same edge the lead band uses. Red only ever
   means overdue; nothing else gets a tinted ground. */
.signal-urgent { border-color: var(--border-lead); background: linear-gradient(145deg, var(--state-overdue-surface), var(--surface) 70%); }
/* THE LABEL BAND IS TWO LINES TALL WHETHER IT USES THEM OR NOT. Four counts
   read at one glance is the entire point of this component, so the figures have
   to share a baseline — and they cannot if their position is derived from how
   many lines the copy above them happens to take. "Overdue" was the only label
   short enough not to wrap at 143px AND the only one with a three-line note, so
   the slack landed at a different height in that one card and its figure sat
   16px above the other three: nearly half a figure, visible without measuring,
   reading as one card misaligned rather than as a set.

   Two lines at the label's own line-height, so the reservation follows the type
   scale rather than a pixel guess. */
.signal-top { display: flex; align-items: center; gap: 8px; min-height: calc(2 * 1.45em); font-size: var(--fs-secondary); font-weight: 600; color: var(--text-secondary); }
/* THE FIGURE DOMINATES ITS LABEL — 34px against a 13px label and an 11.5px note,
   which is the whole reason a signal card beats a row of text. Off the type
   scale on purpose; tabular so four cards' digits sit on one baseline grid. */
/* THE AUTO MARGIN MOVED TO THE NOTE. The figure now sits a fixed distance below
   a fixed-height label, so its y is the same in all four cards; the note takes
   the leftover and bottom-aligns, which is where a two- or three-line caption
   wants to be anyway. Never put margin-top:auto on the element whose alignment
   is the component's job — the auto absorbs every difference in the copy. */
/* 620 and 580 ARE GONE, 6 September 2026. The repo hosts four static faces —
   400, 500, 600, 700 — and asked for two weights it does not have. The browser
   does not synthesise those: with static faces available it ROUNDS, measured at
   100px as 550/580/600 all rendering 1133.41px wide and 620/650/700 all
   1164.50px. So .signal-figure was already drawing at 700 and .attention-title
   and .floor-row-count at 600.
   Writing the hosted number changes nothing on screen and makes the sheet true.
   A stylesheet saying 620 while the screen renders 700 is two things that must
   agree with nothing forcing them to, and the sheet was the wrong one.
   test_every_weight_has_a_hosted_face keeps it that way. */
.signal-figure { margin-top: 12px; font-size: 34px; font-weight: 700; line-height: 1; letter-spacing: -.03em; color: var(--text); }
.signal-note { margin: 7px 0 0; margin-top: auto; padding-top: 7px; font-size: var(--fs-meta); line-height: 1.35; color: var(--text-muted); }

/* ── 2. THE NEEDS-ATTENTION ROW, GROUPED ──
   A leading state mark, a reference, the record, the time, a chevron. GROUPED,
   because a flat list of mixed urgency makes the reader sort it themselves: the
   group header names the demand and carries its own count, and the rows below it
   are all the same kind of late. One surface holds the whole group; the rows are
   separated by a hairline, not boxed individually. */
.attention-group { display: flex; flex-direction: column; gap: 9px; }
.attention-group-head { display: flex; align-items: center; gap: 9px; padding: 0 2px; font-size: var(--fs-meta); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--text-secondary); }
.attention-group-count { padding: 2px 7px; border-radius: 999px; background: var(--surface-raised); font-size: var(--fs-meta); font-weight: 500; letter-spacing: 0; color: var(--text-muted); }
.attention-list { display: flex; flex-direction: column; border-radius: var(--radius-lg); background: var(--surface); box-shadow: var(--card-shadow); overflow: hidden; }
.attention-row {
  display: grid;
  grid-template-columns: 8px 100px minmax(0, 1fr) auto 16px;
  align-items: center;
  gap: 14px;
  min-height: 70px;
  padding: 12px 16px;
  border: 0;
  border-bottom: 1px solid var(--border);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  text-decoration: none;
}
.attention-row:last-child { border-bottom: 0; }
a.attention-row:hover, button.attention-row:hover { background: var(--surface-hover); }
/* nowrap AND a 100px track, 6 September 2026, hand-maintained.
   MEASURED, NOT GUESSED. The ref font is monospace, so every reference is the
   same width regardless of its characters: MK-O-FAKE-0070, MK-Q-0901-EB71 and
   MM-M-MMMM-MMMM all render at 96.94px at 11.5px/600. The production format is
   fixed at fourteen characters — `MK-{letter}-{%m%d}-{4 hex}`, from
   services/inquiries.py and services/quotes.py — so 14 is the real maximum and
   not a fixture artefact. The track was 76px, which broke every ref onto two
   lines. 100px is 97 rounded up plus three, enough that a font fallback with
   slightly wider glyphs does not re-break it, and it costs the record column
   24px of a 395px share. */
.attention-ref { font-size: var(--fs-meta); font-weight: 600; color: var(--text-secondary); white-space: nowrap; }
.attention-main { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.attention-title { margin: 0; font-size: var(--fs-body); font-weight: 600; color: var(--text); }
.attention-detail { margin: 0; font-size: var(--fs-meta); color: var(--text-muted); }
/* The time is the only coloured text in the row, it is the reason the row is
   here, and it uses the LIFTED semantic inks — this row sits on --surface and
   can be lifted to raised on hover, so the ink has to clear AA on the lighter
   of the two. That is what the mockup's #d15b61 could not do. */
.attention-time { font-size: var(--fs-meta); font-weight: 600; color: var(--text-secondary); white-space: nowrap; }
.attention-time-overdue { color: var(--state-overdue); }
.attention-time-chasing { color: var(--state-chasing); }
.attention-chevron { width: 16px; color: var(--text-muted); }

/* ── 3. THE STUDIO FLOOR PANEL ──
   One figure, a rule, then the machine groups. On RAISED because it is the
   answer to "what is happening right now" and sits beside the work, not under
   it — the surface step does the separating and the panel needs no outline. */
.floor-panel { padding: 20px 22px; border-radius: var(--radius-lg); background: var(--surface-raised); }
.floor-label { margin: 0; font-size: var(--fs-secondary); color: var(--text-muted); }
.floor-figure { font-size: 38px; font-weight: 600; line-height: 1; letter-spacing: -.03em; color: var(--text); }
.floor-unit { margin: 5px 0 0; font-size: var(--fs-meta); color: var(--text-muted); }
.floor-rule { height: 1px; margin: 18px 0 10px; background: var(--border-strong); border: 0; }
.floor-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 41px; font-size: var(--fs-secondary); color: var(--text-secondary); }
.floor-row-name { display: flex; align-items: center; gap: 9px; min-width: 0; }
/* THIS ONE KEEPS tabular-nums AND IT DOES NOTHING HERE. Written down because
   it was nearly deleted twice for exactly that.

   General Sans carries no tabular figures, so the property is inert on this
   class today — and it must NOT be fixed by moving to var(--font-mono) like
   .work-when, .work-split and .tabular were. This class holds MIXED CONTENT.
   Measured on Home: of 15 uses, EIGHT hold words — "waiting for parts", "in
   for maintenance", "out of service", "low on ink or toner" — and seven hold
   numbers. Mono would set the machine states in monospace, which is a
   regression on the studio floor to fix an alignment nobody can see.

   THERE IS ALSO NO COLUMN TO ALIGN. The numeric ones live in two panels — 9 in
   .floor-panel, 6 in .avail-panel — with different right edges, so they never
   formed one column in the first place.

   IT STAYS RATHER THAN GOING because it is correct the moment either thing
   changes: a face with tnum, or a template that gives the count its own span
   separate from the state word. Deleting it would silently drop a declaration
   that becomes right again. That template change is the real fix and it is not
   this file's to make. */
.floor-row-count { font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }

/* Home's heading eyebrow — the date, above the greeting. */
.home-eyebrow { margin: 0 0 6px; font-size: var(--fs-meta); font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: var(--text-muted); }
.home-work { display: flex; flex-direction: column; gap: 22px; }
.home-aside { display: flex; flex-direction: column; gap: 12px; }

/* AVAILABILITY IS CONTEXT, so it reuses .floor-row rather than .attention-row —
   name on the left, state on the right, no urgency colour and no chevron. Giving
   it the attention row's shape would have flattened the ranking the whole screen
   is built on: a machine awaiting parts is not the same kind of thing as a job
   two days late. On --surface, a step below the floor panel it sits under. */
.avail-panel { padding: 18px 20px; border-radius: var(--radius-lg); background: var(--surface); }
.avail-panel-title { margin: 0; font-size: var(--fs-section); font-weight: 600; }
.avail-panel-note { margin: 4px 0 14px; font-size: var(--fs-meta); line-height: 1.4; color: var(--text-muted); }
.avail-block + .avail-block { margin-top: 12px; }
.avail-block-title { margin: 0 0 2px; font-size: var(--fs-meta); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted); }
/* A SHARED CLASS IS ONLY AS CORRECT AS ITS LAST CONSUMER — the same shape as the
   compensation-list defect recorded above. .floor-row's 41px min-height was
   written for the STATIC machine rows in .floor-panel, which are DIVs and
   correctly sized for text nobody taps. .avail-panel then reused the class as an
   <a>, and its six rows became 41px TAP TARGETS on the phone: below the 44px
   minimum the brief states outright and this file records. The 900px query
   adjusted .avail-panel's padding and never touched the height, so the miss
   survived a responsive pass.

   Corrected AT THE INTERACTIVE CONSUMER so the static rows keep their density.
   Unconditional rather than inside the query: 44px is a touch minimum, a mouse
   does not mind it, and a second number in a media query is another pair that
   can drift. Anything that makes .floor-row interactive inherits this. */
a.floor-row, button.floor-row { min-height: 44px; text-decoration: none; color: var(--text-secondary); }
a.floor-row:hover .floor-row-name, button.floor-row:hover .floor-row-name { color: var(--text); }

/* ══ RETIRED 5 September 2026 — HOME'S SIX BANDS ══
   .band, .band-lead, .band-head, .band-title, .band-note, .band-tier,
   .band-aside, .band-toggle, .band-summary, .band-list, .band-pair,
   .band-quiet, .band-held, .band-running, .band-overnight, .band-act,
   .band-avail, .band-nowork, .metric-row, .metric-card, .metric-figure,
   .metric-main, .metric-cap, .metric-name, .metric-sub, .avail-groups,
   .avail-group, .avail-group-title, .overnight-list, .overnight-row and the
   .stat / .stat-row family.

   THE RANKING WAS RIGHT AND THE SHAPE WAS WRONG. Six boxed sections, each with
   its own head, tier tag and toggle, where the reader wanted one list to read
   down: LATE / OVERNIGHT / WAITING ON US / WAITING ON THEM / IN PRODUCTION /
   AVAILABILITY. Every band stated a COUNT and an age; none of them named the
   job. The order survives as .attention-group's groups — Late, Reply today,
   Waiting on us, Waiting on them — and the rows are now records.

   KEPT, NOT DELETED, and unreferenced by Home. Same treatment as the parked
   engine, the parked readiness views and the light tokens before them: in place,
   labelled with the reason, removed in a later pass with its own guard. Some of
   these classes are still consumed by OTHER screens — .stat-row and .metric-card
   appear on Jobs and the readiness frames — so a blind delete is exactly the
   kind of change that has broken this dashboard twice. Check every consumer
   before removing any of them, and count instances inside the SHELL, not in the
   document: a class with one hit is a specimen, a class with none is dead.
   ══ */

/* Home's two columns: the work, then the floor beside it. The gap is wider than
   --page-gap because these are two different KINDS of thing, not two groups of
   one kind.

   FLEX-WRAP, NOT A GRID AND NOT A SECOND QUERY. As a grid of
   minmax(0,1.65fr) / minmax(270px,.72fr) this was correct at 1440 and broken at
   924 — just above the 900px breakpoint, where the sidebar and the page inset
   leave about 620px: the work column resolved to 295px and .attention-row's
   record column, a minmax(0,1fr) between a 76px reference and the time,
   collapsed to 50px. The record became unreadable while every rule still read as
   right, which is the fr-unit version of the flex-basis defect already recorded
   above — a track that has no floor takes whatever is left.

   Wrapping fixes it WITHOUT a new breakpoint: the work column declares the width
   it needs (420px) and the aside declares its own (270px), and when the two plus
   the gap do not fit, the aside wraps below on its own. No number in this rule
   names a viewport, so it cannot disagree with the 900px query — which is why
   this is not the second media query the one-query rule forbids. */
.home-columns { display: flex; flex-wrap: wrap; gap: 44px; align-items: flex-start; }
.home-work { flex: 1 1 420px; min-width: 0; }
.home-aside { flex: 1 1 270px; min-width: 0; max-width: 420px; }

/* A DASHED BOX SAYS "UNFILLED FIELD" — that is .cell-input-needed's job, and
   on the dark base a dashed near-white rectangle was the loudest thing on an
   otherwise blank screen. An empty state is A SENTENCE ON A QUIET SURFACE:
   sunken ground, no border, title at section size, one action. */
.empty-state { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; padding: 24px; max-width: 560px; border: 0; border-radius: var(--radius-lg); background: var(--surface-sunken); }
.empty-title { font-size: var(--fs-section); font-weight: 600; margin: 0; }
.empty-body { font-size: var(--fs-secondary); line-height: 1.5; color: var(--text-secondary); margin: 0 0 6px; max-width: 52ch; }
.doc-parties { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; padding: 16px 18px; border-bottom: 1px solid var(--border); }
.doc-party-name { font-size: 14px; font-weight: 600; margin: 0; }
.doc-party-line { font-size: 13px; color: var(--text-secondary); margin: 2px 0 0; }
.doc-corrects { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 24px; padding: 13px 18px; border-bottom: 1px solid var(--border); background: var(--surface-sunken); }
/* A reference to another document is a link and looks like one. This is the
   one place :where(.app a[class]) is overridden on purpose. */
.doc-link { font-size: 13px; font-weight: 600; color: var(--text); text-decoration: underline; text-underline-offset: 2px; }
/*  A LINK THAT STANDS ALONE IS A CONTROL. "Corrects INV-…" is the only way
    from a note to the document it corrects, and it measured 93x16 at 390. */
.doc-link--target { display: inline-flex; align-items: center; min-block-size: 44px; }
.receipt-list { list-style: none; margin: 0; padding: 0; }
.receipt-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 12px 18px; border-bottom: 1px solid var(--border); }
.receipt-row:last-child { border-bottom: 0; }
/*  A VOIDED RECEIPT STAYS ON THE LIST, QUIET, with its amount struck through
    so it cannot be read as money that counts (migration 056). */
.receipt-row--voided .receipt-amount { color: var(--text-muted); text-decoration: line-through; }
.receipt-void { color: var(--text-secondary); }
.receipt-void-form { border-top: 1px solid var(--border); margin-top: 12px; padding-top: 12px; }
.receipt-amount { font-size: 14.5px; font-weight: 600; margin: 0; }
.receipt-meta { font-size: 12px; color: var(--text-muted); margin: 3px 0 0; }
/* The only form in the whole area. Sunken, so it reads as a place to type
   inside a panel that is otherwise a record. */
.receipt-form { display: flex; flex-direction: column; gap: 12px; padding: 16px 18px; border-top: 1px solid var(--border-strong); background: var(--surface-sunken); }
.table-data tfoot td { padding: 10px 16px; border-top: 1px solid var(--border-strong); background: var(--surface-sunken); font-weight: 600; }

/* --- money: RETIRED 2 September 2026 ------------------------------- */
/* The first Money screen — money in, money out, expenses, export — is in the
   retired frame at the bottom of the board, and these rules are read only by
   it. Money is customer balances now (.table-data, above). Kept, not deleted:
   expenses are a later phase. The two dark-mode .money-card-net rules stay in
   their blocks so the canonical block and its mirror keep matching. */
.money-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.money-card { display: flex; flex-direction: column; gap: 3px; padding: 16px 18px; background: var(--surface); border: 1px solid var(--card-border); border-radius: var(--radius-lg); box-shadow: var(--card-shadow); }
.money-card-net { background: var(--surface-raised); border-color: var(--border-raised); }
.money-figure { font-size: 27px; font-weight: 400; line-height: 1.15; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.cat-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.cat-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: baseline; font-size: 13px; }
.cat-bar { grid-column: 1 / -1; height: 4px; border-radius: 2px; background: var(--surface-sunken); overflow: hidden; }
.cat-fill { display: block; height: 100%; background: var(--text-secondary); }
.receipt-no { font-size: 11px; font-weight: 600; color: var(--text-secondary); border: 1px solid var(--border-strong); border-radius: 999px; padding: 2px 8px; white-space: nowrap; } /* retired: expenses "no receipt on file" */
.export-bar { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; padding: 15px 18px; background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--radius-lg); }
.export-field { display: flex; flex-direction: column; gap: 5px; }

/* --- inquiry detail: one job, one screen -------------------------- */
.record-stack { display: flex; flex-direction: column; gap: 18px; }
.kv-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); gap: 14px 22px; }
.kv-label { font-size: 10.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted); margin: 0 0 3px; }
.kv-value { font-size: 13.5px; margin: 0; }
.kv-empty { color: var(--text-muted); }

.version-row { display: flex; align-items: flex-start; gap: 14px; padding: 12px 0; border-bottom: 1px solid var(--border); }
.version-row:last-child { border-bottom: 0; }
.version-tag { font-size: 13px; font-weight: 700; min-width: 24px; flex: none; }
.version-main { flex: 1; min-width: 0; }
.version-changed { font-size: 12.5px; color: var(--text-secondary); margin: 3px 0 0; }
.version-row:not(.is-current) .version-tag, .version-row:not(.is-current) .stat-label { color: var(--text-muted); }

/* Four artwork states, drawn as four. Nothing here is a control — this is
   where the file is; the control that moves it sits below with its reason. */
.artwork-track { display: flex; flex-wrap: wrap; gap: 6px; }
.artwork-step { font-size: 12px; padding: 4px 12px; border: 1px solid var(--border); border-radius: 999px; color: var(--text-muted); }
.artwork-step.is-now { border-color: var(--text); background: var(--text); color: var(--bg); font-weight: 600; }
.history { list-style: none; margin: 0; padding: 0; }
.history-item { display: flex; gap: 14px; padding: 9px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
.history-item:last-child { border-bottom: 0; }
.history-date { color: var(--text-muted); flex: none; width: 88px; }
.history-state { font-weight: 600; }
/* The picked reason, not free text. It is a value from a fixed list, so in six
   months the studio can ask which problem costs it the most time. */
.history-reason { display: inline-flex; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px; background: var(--surface-sunken); border: 1px solid var(--border-strong); margin-left: 8px; }
.history-text { font-size: 12.5px; color: var(--text-secondary); margin: 2px 0 0; }

.proof-row { display: flex; align-items: flex-start; gap: 14px; padding: 11px 0; border-bottom: 1px solid var(--border); }
.proof-row:last-child { border-bottom: 0; }
.proof-version { font-size: 13px; font-weight: 700; width: 24px; flex: none; }
.proof-main { flex: 1; min-width: 0; }
.proof-status { font-size: 13px; font-weight: 500; margin: 0; }
.proof-detail { font-size: 12px; color: var(--text-muted); margin: 2px 0 0; }
.proof-row.is-approved .proof-status { font-weight: 700; }
/* The mark names a VERSION. A flag on the latest proof would claim a customer
   approved a file they never saw — v3 exists and nobody has seen it. */
.proof-mark { flex: none; font-size: 10.5px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; padding: 3px 9px; border-radius: 999px; background: var(--text); color: var(--bg); }

/* --- timeline ----------------------------------------------------- */
/* What makes this a record rather than a form. Newest first, one line per
   event, dates never numeric. Six months later this settles what was agreed. */
/*  ── THE TIMELINE ──────────────────────────────────────────────────────
    THREE LEVELS AND ONE QUIET RAIL. Every event used to be the same grey
    marker, the same two lines and the same gap, so a list of fourteen read as
    one block of text with no way in. The marker carries the semantic colour —
    from dashboard/timeline.py, which reads status.py, so an event and the
    pill for the same state cannot disagree — and the RAIL STAYS NEUTRAL. A
    rail tinted per event is a rainbow down the side of a panel.  */
.timeline { list-style: none; margin: 0; padding: 0; }
.timeline-item { display: grid; grid-template-columns: 58px minmax(0, 1fr); gap: 12px; padding-bottom: 16px; }
.timeline-item:last-child { padding-bottom: 0; }
/*  MORE AIR BETWEEN DAYS THAN WITHIN ONE. The date prints once per day, so
    the group needs a boundary that the repeated dates used to provide. */
.timeline-item--day { padding-top: 8px; }
.timeline-item--day:first-child { padding-top: 0; }
.timeline-date { font-size: 11.5px; color: var(--text-muted); text-align: right; padding-top: 3px; }

/*  THE ROW. A <div> when the event opens nothing and an <a> when it does —
    the class below is what separates them, and it is the ONLY place a
    pointer, a hover or a chevron comes from. */
.timeline-body {
  position: relative; display: flex; align-items: flex-start; gap: 12px;
  padding: 3px 4px 3px 0; border-radius: var(--radius); text-decoration: none;
  color: inherit;
}
/*  THE RAIL, drawn from the marker down to the next one. It is --border and
    stays --border in every variant. */
.timeline-body::after {
  content: ""; position: absolute; left: 3px; top: 17px; bottom: -19px;
  width: 1px; background: var(--border);
}
.timeline-item:last-child .timeline-body::after { display: none; }

.timeline-mark {
  inline-size: 7px; block-size: 7px; border-radius: 50%; flex: none;
  margin-block-start: 6px; background: var(--v-mark, var(--border-strong));
  /*  A SOFT RING, NOT A GLOW. It lifts the marker off the rail without
      becoming a light source — 3px of the variant at a tenth. */
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--v-mark, var(--border-strong)) 12%, transparent);
}
.timeline-mark--positive      { --v-mark: var(--state-good); }
.timeline-mark--attention     { --v-mark: var(--lane-amber); }
.timeline-mark--overdue       { --v-mark: var(--lane-red); }
.timeline-mark--informational { --v-mark: var(--state-held); }
/*  NEUTRAL KEEPS THE RAIL'S OWN INK and no ring. Grey on grey with a halo is
    a marker pretending to mean something. */
.timeline-mark--neutral       { --v-mark: var(--border-strong); box-shadow: none; }

.timeline-main { min-width: 0; flex: 1; }
/*  1 · WHAT HAPPENED.  2 · THE PARTICULAR.  3 · WHO.  */
.timeline-title  { display: block; font-size: 13px; font-weight: 600; color: var(--text); }
.timeline-said   { display: block; font-size: 12.5px; color: var(--text-secondary); margin-block-start: 2px; }
.timeline-detail { display: block; font-size: 11.5px; color: var(--text-muted); margin-block-start: 3px; }

/*  ONLY A LINK GETS AN AFFORDANCE. Everything here is read-only; the
    difference between an event and a record you can open has to be visible
    without being loud. */
.timeline-go { color: var(--text-muted); font-size: 15px; flex: none; margin-block-start: 1px; }
.timeline-body--link { cursor: pointer; }
.timeline-body--link:hover { background: color-mix(in srgb, var(--surface-hover) 70%, transparent); }
.timeline-body--link:hover .timeline-go { color: var(--text-secondary); }
.timeline-body--link:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/*  THE DISCLOSURE. A native <details>, so it works with no JavaScript and
    needs no framework. The marker is removed and the summary styled as the
    quiet control it is — never a button, because revealing history is not an
    action on the record. */
.timeline-more { margin-block-start: 14px; }
.timeline-more > summary {
  list-style: none; cursor: pointer; display: inline-flex; align-items: center;
  min-height: 32px; padding: 0 10px; border-radius: var(--radius);
  border: 1px solid var(--border); font-size: var(--fs-secondary);
  color: var(--text-secondary);
}
.timeline-more > summary::-webkit-details-marker { display: none; }
.timeline-more > summary:hover { border-color: var(--border-strong); color: var(--text); }
.timeline-more > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.timeline-more .timeline { margin-block-start: 16px; }
.timeline-more__hide { display: none; }
.timeline-more[open] .timeline-more__show { display: none; }
.timeline-more[open] .timeline-more__hide { display: inline; }

/* --- quote writing ------------------------------------------------ */
/* Three parts: what they asked for, the lines being written, and what the
   studio has charged before. The recall panel is the feature that replaces the
   engine — a person prices this by reading it, so it is a column and not a
   drawer. */
.quote-layout { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 20px; align-items: start; }
.quote-writing { display: flex; flex-direction: column; gap: 18px; }
/* Required by MIRA on anything invoice-shaped that is not an invoice. It is a
   statement on the document, not a warning, so it takes no alarm weight. */
.doc-mark { font-size: 10.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; padding: 5px 10px; border: 1px solid var(--text); border-radius: var(--radius-sm); white-space: nowrap; }
/* Fixed layout, because every cell holds an input at width:100% and auto
   layout has no content to measure — it gave the description 165px and handed
   218px each to Qty and Unit price for "300" and "9.40". The description is
   the widest thing a person types here, so it takes what is left. */
.lines-table { width: 100%; table-layout: fixed; border-collapse: separate; border-spacing: 0; font-size: 13.5px; }
.lines-table th.c-qty { width: 92px; }
.lines-table th.c-unit { width: 108px; }
.lines-table th.c-total { width: 132px; }
.lines-table th.c-remove { width: 40px; }
.lines-table thead th {
  background: var(--surface-sunken);
  border-bottom: 1px solid var(--border-strong);
  padding: 9px 12px;
  text-align: left;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.lines-table th.num, .lines-table td.num { text-align: right; }
.lines-table td { padding: 5px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; }
/*  ONE RIGHT EDGE FOR A DOCUMENT CARD'S FIGURES. The line amounts ended at
    the table cell's padding (12px, 8px on a phone) and the totals at theirs
    (16px), so a card's amounts, subtotal, GST and total sat on three edges a
    few pixels apart — at every width. --figure-edge is the one inset every
    last column, stacked amount and totals row uses. measure-screens.py
    fails on a near miss (ragged_figures). */
.lines-table :is(td, th):last-child { padding-inline-end: var(--figure-edge); }
.line-input { font: inherit; font-size: 13.5px; width: 100%; padding: 7px 9px; border: 1px solid transparent; border-radius: var(--radius-sm); background: transparent; color: var(--text); }
.line-input:hover { border-color: var(--border); }
.line-input:focus { outline: none; border-color: var(--focus); background: var(--surface); box-shadow: 0 0 0 3px var(--surface-sunken); }
.line-input.num { text-align: right; font-family: var(--font-mono); font-size: calc(1em * var(--mono-scale)); font-variant-numeric: tabular-nums; }
.line-total { font-size:1em ; font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }

/*  THE LINE'S PRODUCT CONTROL (5 Oct 2026) — static/combo.js. The list sits
    IN FLOW under its field rather than floating over the table: the table
    scrolls inside .table-card, and a floating list there is clipped by the
    card on a phone. Open, it pushes the rows below it down; closed, it takes
    no room. Every option is 44px tall, so a thumb finds it. */
.line-combo { display: flex; flex-direction: column; gap: 4px; min-inline-size: 0; }
.line-combo__list {
  list-style: none; margin: 0; padding: 4px 0;
  max-block-size: 300px; overflow-y: auto;
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  background: var(--surface);
}
.line-combo__group {
  padding: 10px 12px 4px; font-size: var(--fs-meta); font-weight: 600;
  color: var(--text-muted);
}
.line-combo__option {
  display: flex; align-items: center; min-block-size: 44px; padding: 0 12px;
  cursor: pointer; color: var(--text); overflow-wrap: anywhere;
}
.line-combo__option:hover, .line-combo__option.is-active { background: var(--surface-hover); }
.line-combo__option[aria-selected="true"] { font-weight: 600; }
.line-combo__none { padding: 10px 12px; color: var(--text-secondary); font-size: var(--fs-secondary); }
/*  THE SPECIFICATION BOX (migration 070): a second, quieter field under the
    description — plain text, its breaks kept, 44px tall at the least. */
.line-spec { resize: vertical; min-block-size: 44px; font-size: var(--fs-secondary); color: var(--text-secondary); line-height: 1.45; }
.line-spec-static { display: block; margin-top: 3px; font-size: var(--fs-secondary); color: var(--text-secondary); white-space: pre-line; overflow-wrap: anywhere; }
/*  The custom item's category, picked inline (static/combo.js). */
.line-combo__category { font: inherit; min-block-size: 44px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); padding: 0 8px; }
.line-combo__option--custom { font-weight: 600; border-block-end: 1px solid var(--border); }
/*  A ROW WITH ITS LIST OPEN IS TALL, so every cell starts at the top —
    centred, the quantity and prices floated to the middle of the list. */
.lines-table tbody td { vertical-align: top; }
/*  ON A PHONE THE DESCRIPTION COLUMN IS ~110px, too narrow for a product
    list ("Custom T-" broke over two lines). The open list takes the width of
    the lines card instead, drawn over the empty space beneath the row's
    quantity and prices — sized to the card, so it adds no sideways scroll. */
@media (max-width: 699px) {
  .line-combo__list { position: relative; z-index: 3; inline-size: calc(100vw - 64px); max-inline-size: none; }
  .line-combo__link { inline-size: calc(100vw - 64px); }
  .line-spec { inline-size: calc(100vw - 64px); }
}
.line-combo__link {
  margin: 0; padding-inline: 9px; display: flex; flex-wrap: wrap; align-items: center; gap: 2px 8px;
  font-size: var(--fs-meta); color: var(--text-secondary);
}
.line-combo__link:empty { display: none; }
.line-combo__unlink {
  font: inherit; font-weight: 600; color: var(--text-secondary); background: none; border: 0;
  padding: 0 6px; min-block-size: 44px; min-inline-size: 44px; cursor: pointer; text-decoration: underline;
  text-underline-offset: 2px;
}
.line-combo__unlink:hover { color: var(--text); }
.lines-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; border-top: 1px solid var(--border); }
.totals { display: flex; flex-direction: column; gap: 8px; padding: 14px var(--figure-edge); border-top: 1px solid var(--border-strong); background: var(--surface-sunken); }
.total-row { display: flex; align-items: baseline; justify-content: space-between; gap: 24px; font-size: 13.5px; }
.total-row.is-total { padding-top: 9px; border-top: 1px solid var(--border-strong); }
.total-label { color: var(--text-secondary); }
.total-row.is-total .total-label { color: var(--text); font-weight: 700; }
.note-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.note-area { font: inherit; font-size: 13px; width: 100%; min-height: 76px; resize: vertical; padding: 9px 11px; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); }
.note-area:focus { outline: none; border-color: var(--focus); box-shadow: 0 0 0 3px var(--surface-sunken); }

.recall-list { list-style: none; margin: 0; padding: 0; }
.recall-item { padding: 12px 16px; border-bottom: 1px solid var(--border); }
.recall-item:last-child { border-bottom: 0; }
.recall-item:hover { background: var(--surface-hover); }
.recall-spec { font-size: 13px; font-weight: 500; margin: 0; }
.recall-meta { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px; font-size: 12px; color: var(--text-muted); margin: 5px 0 0; }
/*  WHAT WAS QUOTED, under what it cost (5 Oct 2026), so like is compared with like. */
.recall-spec-text { margin: 6px 0 0; font-size: 12px; color: var(--text-secondary); white-space: pre-line; overflow-wrap: anywhere; }
/*  A custom item's matches are the same KIND of work, not the same product. */
.recall-loose { margin: 0; padding: 10px 16px 0; font-size: 12px; color: var(--text-secondary); }
.recall-amount { font-family: var(--font-mono); font-size: calc(14px * var(--mono-scale)); font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text); }
/* An outcome is weight and case, never colour: won and lost are history, not
   urgency, and the two coloured states are spoken for. */
.recall-outcome { font-size: 10.5px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; }
.recall-won { color: var(--text); }
.recall-sent { color: var(--text-secondary); }
.recall-lost { color: var(--text-muted); }
.recall-reason { font-size: 12px; color: var(--text-secondary); margin: 4px 0 0; }

/* --- nav icons ---------------------------------------------------- */
/* Inline SVG, vendored. No icon font, no sprite, no external request.

   Why inline over a sprite: a sprite needs an id lookup and a <use> href, which
   in Flask means either a template global or a hard-coded path per item, and
   <use> across a shadow boundary or a data: URL is where sprites start needing
   workarounds. Inline SVG needs nothing — no font file to serve, no build step,
   no FOUT, and it inherits currentColor so the active state comes free from the
   rule that already colours the label. In the repo these are ONE Jinja macro,
   {{ nav_icon('jobs') }} in nav.html, so the markup is written once.

   The family: 24×24 box, 1.6 stroke, round caps and joins, no fill, one weight
   throughout. Sized 17px in the sidebar and 20px in the mobile strip — the
   geometry is coarse enough to hold at both and would not survive smaller. */
/* Stroke-width is a CSS property in user units, so the RENDERED stroke is
   size/24 × stroke-width. Holding that near 1.35 device px keeps the family
   looking like one weight at every size, and is why the strip and the sidebar
   carry different numbers rather than the same one:

       17px → 1.9      20px → 1.62      16px → 2.0

   THE FLOOR IS 16px, and it is set by the internal gaps, not by the stroke.

   The criterion, stated so it can be checked rather than asserted: NO TWO
   STROKE CENTRELINES INSIDE A GLYPH SIT CLOSER THAN 3.5 UNITS. At 16px with
   stroke 2.0 that leaves at least 1 device px of open gap — a stroke edge to
   stroke edge measurement, not a centre to centre one, which is where the first
   version of this rule went wrong. T-junctions are exempt: they are meant to
   connect.

   Every glyph is drawn to that, and it cost three things worth naming. The
   money note grew 2 units taller so its circle clears the frame. The house
   became a single closed silhouette, because a roof crossing the wall tops is a
   filled corner at every size. The price tag lost its hole outright: below about
   18px the hole's own interior closes before its clearance to the tag wall does,
   so no position or radius saves it — the tag now reads by its asymmetric
   outline and right-hand point, against Stock's symmetric diamond.

   Below 16px the sheet's body lines and the tray's divider go first. Do not
   shrink the strip past 16px without redrawing the set. */
.nav-icon { width: 17px; height: 17px; flex: none; stroke: currentColor; fill: none; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.mobile-nav .nav-icon { width: 20px; height: 20px; stroke-width: 1.62; }
.nav-icon-20 { width: 20px; height: 20px; stroke-width: 1.62; }
.nav-icon-16 { width: 16px; height: 16px; stroke-width: 2; }

/* 900 is the number the Jinja template comment already assumes: below it the
   sidebar becomes a bottom bar. Both halves of the swap live in this one query,
   so the two navigations can never appear together.

   LAST IN THE APP SHEET ON PURPOSE. Every rule in here overrides a component
   rule at equal specificity, so it wins only by coming later. Moved 3
   September 2026 from beside .mobile-nav, where it lost to .work-row and
   .lines-table. Keep it here; the board block below is mockup-only and is
   dropped on extraction, so this stays the final block of make.css. */
/* THE 700px QUERY SITS ABOVE THE 900px ONE ON PURPOSE.
   test_the_900px_query_is_the_last_block_in_the_sheet requires the 900px block
   to be the final thing in the file: it re-declares `.app`'s grid at the same
   specificity as the desktop rule and wins only by source order, so anything
   appended below silently restores the 232px sidebar column on a phone. These
   two blocks share no selector with it, so the order between them is free —
   the invariant is not. */
/* AN ORPHANED `@media (max-width: 700px) {` STOOD HERE, and it swallowed the
   rest of the file. 12 Sep 2026: moving the 700px blocks above the 900px query
   lifted their BODIES and left one opening line behind, so every rule after it
   — the whole list-screen section, ~330 lines — was nested inside a max-width
   query and applied only below 700px.
   Nothing failed. The stylesheet parsed, the suite passed, and a brace count
   said 716/716 because it counted braces inside comments. The symptoms all
   appeared on the desktop: record cards rendering under the table at 1686,
   names centred in their cells, the truncation and empty-value rules inert.
   COUNT BRACES WITH COMMENTS STRIPPED. The file now ends at depth 0 and
   test_the_stylesheet_blocks_are_balanced checks it. */
  



/* --- ONE control band, not four ------------------------------------ */
/* `.chips` and `.filter-form` were written into four templates and NEVER
   EXISTED IN THIS FILE — they stacked as plain blocks at default spacing,
   which is what four bands of controls were. One row, wrapping naturally,
   search growing into whatever is left. No submit button: every other filter
   applies on click, and Enter already submits. */
/*  `.list-controls` and `.list-search` were deleted on 15 Sep 2026. The last
    two templates using them — the Stock and Machines filter partials — moved
    to `.make-toolbar`, and a rule with no selector left to match is the same
    two-systems problem as a template nobody can reach. Their behaviour, minus
    the stacking, is in `.make-record-page .make-toolbar` below. */

/* --- WHERE MONO SURVIVES, AND WHY -----------------------------------
   12 Sep 2026. Mono exists for ONE reason: the vendored General Sans has no
   tabular figures — measured, 28.7% spread across the ten digits at 200px,
   and font-variant-numeric changes nothing — so a COLUMN of amounts staggers.

   A card is not a column. Rf 7,452.00 and Rf 507.62 sit in separate boxes
   with separate left edges and align with nothing, so the code face buys
   nothing there and costs the page's typeface.

   MOVED TO GENERAL SANS, each with what it is:
     .make-record-card__money   a customer card's balance — one per card
     .inquiry-price             a Kanban card's price — inline in a footer
     .tabular                   a TIN input on two customer forms
     .route-step-index          a step number, left-aligned, one per step
     .route-step-width          renders in no template
     .todo-count                renders in no template
     .line-total                renders in no template

   KEPT, AND EVERY ONE NAMES ITS COLUMN:
     .money            invoice document line totals, right-aligned in <td>
     .cell-num         numeric grid cells on the parked setup screens
     .cell-mono        Total / Agreed / Charged Rf on customer and product
                       detail, and the money table
     .num              right-aligned numeric <td> across the data tables
     .work-total       the jobs list amount column — .work-money is
                       text-align:right and the rows stack, measured on
                       /invoices as "32.92 left" over "969.30 left"
     .work-split       the second line of that same column
     .recall-amount    the quote screen's recall panel, amounts stacked
     .line-input.num   invoice line inputs, a column of typed figures

   The test is one question: CAN YOU NAME THE COLUMN? If not, it moves.   */

/* ===================================================================
   THE RECORD CARD — Customers, Products, Stock and Machines.
   12 September 2026, from the card-component handoff.
   ===================================================================

   THE OLD CARD CLASSES ARE DELETED, NOT OVERRIDDEN. Two earlier passes left
   .card-grid, .rec-card, .rec-state, .rec-name, .rec-sub, .rec-foot,
   .rec-figure, .rec-figure-sub, .rec-link, .rec-control, .rec-confirm,
   .rec-fact, .rec-none, .rec-state-act and .list-summary in this file; all
   fifteen are gone, along with the table-era rules these records inherited.
   Stacking a third pass on top of two is how a component stops being one.

   EVERY COLOUR IS A TOKEN. The handoff gives hex values; four of them are
   already the app's own — #211f1e --surface, #2b2826 --surface-raised,
   #f2ae35 --lane-amber, #e0dad3 --text — and the rest are bound to the
   nearest existing tier rather than admitted as new greys. A fifth grey two
   units from a fourth is how a palette stops being a system:

     handoff    bound to             delta        why
     #e0dad3    --text               0            exact
     #211f1e    --surface            0            exact
     #2b2826    --surface-raised     0            exact
     #f2ae35    --lane-amber         0            exact
     #38332f    --border-raised      2,1,2        indistinguishable
     #282421    --surface-hover      1,2,3        indistinguishable
     #352b1c    --lane-amber-surface 9,10,9       the amber ground, one tier
     #aaa19a    --text-secondary     9,11,12      the supporting-prose tier
     #c6beb7    --text-secondary     19,18,17     NO EXACT TIER — see below
     #62594f    --border-strong      29,25,18     NO EXACT TIER — see below

   TWO HAVE NO EQUIVALENT AND ARE BOUND ANYWAY, deliberately. #c6beb7 sits
   between --text and --text-secondary, and #62594f between --border-strong
   and nothing; each would be a new tier holding one rule. The badge reads a
   half-step quieter than intended and the hover border a half-step darker,
   which is a smaller cost than a palette nobody can reason about.          */

/* NO WIDTH CAP ON THE LAYOUT. The handoff's ~1180px max was applied to these
   four screens and to no others, so Customers, Products, Stock and Machines
   stayed locked while Inquiries, Jobs, Invoices and Money grew — measured at
   2200: .page was 1180 on the four and 1937 on the other four, because only
   .make-record-page carried the cap.

   A CAP ON THE LAYOUT IS THE WRONG PLACE FOR IT. What a long line actually
   hurts is READING, and reading is a property of prose, not of a grid. Capped
   at 1180 a card grid is stuck at three columns with a thousand pixels empty
   beside it, which is the one thing a grid exists not to do. So the cap moves
   to the text: .page-note already had `max-width: 62ch` and was the only
   prose measured at a sane width on ANY screen — 72ch at 2200 — while
   .notice ran to 299ch and .work-meta to 228ch on the uncapped screens.

   The measure is applied to prose everywhere and the layout is free
   everywhere, so all eight screens now agree about what a wide viewport is
   for: more records, not longer lines. */
.make-record-page {
  inline-size: 100%;
  font-family: var(--font);
}
/* THREE, AND NO MORE. 3 / 2 / 1 at every width.

   THIS REVERSES AN EARLIER INSTRUCTION RATHER THAN CORRECTING A MISTAKE.
   The comment that stood here described four columns at 1600 and five at
   2000, with the arithmetic that justified them — content width is the
   viewport less the 232px sidebar and the page's own 68px gutter, so 2200
   gave 1900 across five at 369px each, which kept the card at its natural
   size. That was built to an approved direction and it did what it said.

   The design authority changed its mind on 20 Sep 2026: five columns
   violates the 3/2/1 card rule it wants applied consistently across
   Products, Customers, Stock and Machines. Its call to make. Recorded here
   because a rule with no history is one the next person re-derives from
   scratch and may well re-reverse.

   THE COST OF THE REVERSAL, MEASURED, because it is the reason the old
   direction existed: three columns across 1900px is 622px a card, and a
   record card holds an icon, a name, a sub-line and a status badge. That is
   a slab. So the grid is CAPPED rather than stretched — the same treatment
   the authority asked for on job rows in the same brief, for the same
   reason: "do not allow rows to stretch into unusably empty full-width
   slabs". At the cap a card is ~400px, which is inside the 340-370 the old
   comment was protecting.

   Remove the cap and you get 622px cards, not four columns. The two
   decisions are separate. */
.make-record-page .make-record-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px;
  align-items: stretch;
  max-inline-size: 1280px;
}
.make-record-page .make-record-card {
  box-sizing: border-box;
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  gap: 0;
  min-inline-size: 0;
  /* A MINIMUM, NOT A HEIGHT. It grows for a long name, a select, or a
     confirmation; a fixed height clips, and space-between on the card would
     distribute the three groups evenly, which is the thing the reference is
     not. Only the footer has margin-top: auto. */
  min-block-size: 188px;
  margin: 0;
  padding: 18px;
  border: 1px solid var(--border-raised);
  border-radius: 13px;
  background: var(--surface);
  box-shadow: 0 3px 8px #00000020;
  color: var(--text);
  text-align: start;
  text-transform: none;
}
.make-record-page .make-record-card--linked:hover {
  background: var(--surface-hover);
  border-color: var(--border-strong);
}
.make-record-page .make-record-card__top {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 8px;
  margin: 0 0 20px;
}
.make-record-page .make-record-card__top > svg {
  inline-size: 18px; block-size: 18px; flex: 0 0 18px;
  margin-block-start: 3px;
  color: var(--text-secondary);
}
.make-record-page .make-record-card__status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-inline-size: 0;
  max-inline-size: calc(100% - 26px);
  margin-inline-start: auto;
  padding: 5px 8px;
  border: 0;
  border-radius: 6px;
  background: var(--surface-raised);
  color: var(--text-secondary);
  font-size: 11.5px;
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: normal;
  text-transform: none;
  white-space: normal;
}
.make-record-page .make-record-card__status > span { min-inline-size: 0; overflow-wrap: anywhere; }
.make-record-page .make-record-card__status::before {
  content: "";
  box-sizing: border-box;
  inline-size: 5px; block-size: 5px; flex: 0 0 5px;
  border: 1px solid currentColor;
  border-radius: 50%;
}
/*  THE --attention MODIFIER WAS REMOVED 21 Sep 2026, when the last template
    using it moved to .pill-status. It coloured the badge amber and filled its
    dot; the pill does both, in five variants instead of one, from a mapping
    that is not written in the markup. .make-record-card__status itself STAYS —
    product detail still uses it for a plain count, which is a number and not a
    state. A modifier with no user renders nothing and reads as a live rule. */
.make-record-page .make-record-card__body { margin: 0; }
.make-record-page .make-record-card__name {
  margin: 0;
  color: var(--text);
  font-size: 15px;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -0.15px;
  text-transform: none;
  overflow-wrap: anywhere;
}
/* SIX PIXELS. The name and its category are ONE group — the implementation
   this replaces rendered four evenly separated lines, which reads as four
   facts of equal weight rather than an identity and its detail. */
.make-record-page .make-record-card__secondary {
  margin: 6px 0 0;
  color: var(--text-secondary);
  font-size: 12.5px;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: normal;
  text-transform: none;
  overflow-wrap: anywhere;
}
.make-record-page .make-record-card__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: auto 0 0;
  padding-block-start: 18px;
  color: var(--text-secondary);
  font-size: 11.5px;
  font-weight: 400;
  line-height: 1.5;
  text-transform: none;
}
.make-record-page .make-record-card__footer-content { min-inline-size: 0; overflow-wrap: anywhere; }
.make-record-page .make-record-card__footer > svg {
  inline-size: 16px; block-size: 16px; flex: 0 0 16px;
  margin-inline-start: auto;
  color: var(--text-secondary);
}
/* THE WHOLE CARD IS THE TARGET. The arrow is decorative — not a second link. */
.make-record-page .make-record-card__link { color: inherit; font: inherit; text-decoration: none; }
.make-record-page .make-record-card__link::after {
  content: ""; position: absolute; inset: 0; z-index: 1; border-radius: 13px;
}
.make-record-page .make-record-card__link:focus-visible::after {
  outline: 2px solid var(--focus); outline-offset: 3px;
}
/* CONTROLS SIT ABOVE THE STRETCHED LINK AND OUTSIDE THE ANCHOR. The machine
   select and its confirmation live here. */
.make-record-page .make-record-card__controls {
  position: relative; z-index: 2; min-inline-size: 0; max-inline-size: 100%;
  /*  THE ROW VARIANT'S SPACING, ON EVERY CARD. Only the row (12px) and the
      board (14px) set any; a grid card's control — Stock's "Count stock",
      a machine's status select — sat 0px under the text above it. */
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin-block-start: 12px;
}
.make-record-page .make-record-card__controls :is(select, button, input, a) {
  box-sizing: border-box; min-block-size: 44px; min-inline-size: 44px; max-inline-size: 100%;
}
.make-record-page .machine-confirm { margin-block-start: 10px; padding-block-start: 10px;
  border-block-start: 1px solid var(--border); display: flex; flex-wrap: wrap; gap: 8px; }
.make-record-page .machine-confirm-text { flex: 1 1 100%; margin: 0; font-size: 11.5px;
  line-height: 1.5; color: var(--text-secondary); }
.make-record-page .make-record-card__money-label { display: block; }
.make-record-page .make-record-card__money {
  display: block;
  margin-block-start: 3px;
  color: var(--text);
  font-size: 18px;
  line-height: 1.3;
  font-weight: 500;
  /* The money face stays where alignment needs it; the label and the prose
     around it are General Sans. */
  font-variant-numeric: tabular-nums;
}

/* --- THE RECORD ROW: the card, laid out as a row --------------------
   15 Sep 2026, for Jobs.

   WHY A ROW AND NOT THE GRID. Jobs is ordered `late DESC, due_today DESC,
   due_date, ref` — a queue, deliberately. A 3/2/1 grid reads left-to-right
   then down, so the most urgent job sits beside the second and above the
   fifth, and "what is late" stops being the shape of the page. The card's
   padding, radius, border, surface, badge and identity grouping all carry
   over; the grid is the one thing that does not.

   IT SHARES THE CARD'S TOKENS BY SELECTOR, NOT BY COPY. Every declaration
   below is a LAYOUT difference. The surface, border, radius, shadow, status
   badge, name and secondary text come from the .make-record-card rules above,
   which this markup also carries — two components with hand-copied padding is
   the drift this project keeps writing down.

   THE WHOLE ROW IS THE TARGET, and the status buttons sit above it in
   __controls, which already exists for the machine select. A row that is
   navigation has to be as big as a row; a 96x15 ref link inside it is
   navigation wearing prose, and padding that ref to 44px would break the
   identity grouping it belongs to. */
/*  THE LIST IS A CONTAINER, AND THE ROW ASKS IT — not the viewport.

    The row's four tracks are 412px of fixed width, released in the 900px
    query. That is right when the list is the page, and wrong the moment one
    sits in a narrow column: inquiry detail puts a record list inside a 241px
    panel at 924, where the viewport query does not fire and the panel clipped
    the row instead. A viewport width is not a statement about how much room
    the component has. */
.make-record-page .make-record-list {
  display: flex; flex-direction: column; gap: 10px;
  container-type: inline-size; container-name: recordlist;
}
/*  AND THE PANEL AROUND IT STOPS CLIPPING. `.panel` carries `overflow:
    hidden` for its corners; a stacked row in a 241px column still has content
    that does not fit — a status badge, a long file name — and hidden turns
    that into text cut off rather than text reachable. Scroll is what the rest
    of this sheet does with wide content in a narrow box: Money's balances and
    the document line tables both. */
.make-record-page .panel:has(.make-record-list) { overflow-x: auto; }
.make-record-page .make-record-card--row {
  display: grid;
  grid-template-columns: 104px minmax(0, 1fr) 168px 140px;
  align-items: start;
  gap: 0 20px;
  /* A ROW HAS NO MINIMUM HEIGHT. 188px is right for a card in a grid, where
     the cards in a line must agree; sixteen jobs at 188px is 3,000px of
     scrolling for a list whose whole argument is that you can see the queue. */
  min-block-size: 0;
  padding: 16px 18px;
}
.make-record-page .make-record-card--row > * { min-inline-size: 0; }
/* A row whose caller has no date drops the date track rather than leaving it
   blank — see the macro's note. */
.make-record-page .make-record-card--row-dateless {
  grid-template-columns: minmax(0, 1fr) 168px 140px;
}

/*  AND IT SITS HERE, BELOW THE RULES IT OVERRIDES, BECAUSE A CONTAINER
    QUERY ADDS NO SPECIFICITY.

    It was written above them and was dead for the whole of its life. Both
    it and `.make-record-page .make-record-card--row` are (0,2,0); the
    unconditional rule came later in the file, so it won, and the card kept
    `104px minmax(0,1fr) 168px 140px` at every container width. Measured on
    20 Sep 2026 at a 924px viewport, on inquiry detail's 256px panel: the
    resolved grid was `104px 0px 168px 140px` — the flexible track crushed
    to nothing and every control in it collapsed to its 44px minimum.

    It LOOKED like it worked, because the `@media (max-width: 900px)` block
    at the foot of this file sets the same one-column grid and does come
    last. So every width where anyone checked — 390 — was being released by
    the media query, and the container query was never the thing doing it.
    The one case it exists for is the one case neither rule covered.

    test_the_record_row_container_query_can_actually_win holds the order. */
@container recordlist (max-width: 620px) {
  .make-record-page .make-record-card--row,
  .make-record-page .make-record-card--row-dateless {
    grid-template-columns: minmax(0, 1fr);
    gap: 10px 0;
    padding: 18px;
  }
  .make-record-page .make-record-row__money,
  .make-record-page .make-record-row__money-void { text-align: start; }
  /*  STACKED, THE STATUS SITS WITH THE NAME. The body is one grid cell and
      its controls are inside it, so the status column — next in the source —
      landed below the actions, far from the job it describes. The body
      yields its children to the grid and they are ordered: date, name,
      status, what it is, money, then what you can do to it. */
  .make-record-page .make-record-card--row .make-record-card__body { display: contents; }
  .make-record-page .make-record-card--row .make-record-row__when { order: 0; }
  .make-record-page .make-record-card--row .make-record-card__name { order: 1; }
  .make-record-page .make-record-card--row .make-record-row__status { order: 2; }
  .make-record-page .make-record-card--row .make-record-card__secondary { order: 3; }
  .make-record-page .make-record-card--row .make-record-row__money-cell { order: 4; }
  .make-record-page .make-record-card--row .make-record-card__controls { order: 5; }
}
/* The badge is top-right of a card; in a row it belongs in its own column,
   left-aligned with the column under it rather than pushed to the edge. */
.make-record-page .make-record-card--row .make-record-card__status { margin-inline-start: 0; }
.make-record-page .make-record-row__when {
  color: var(--text); font-size: 13px; font-weight: 600; line-height: 1.35;
}
.make-record-page .make-record-row__flag {
  display: block; margin-block-start: 4px;
  color: var(--text-secondary); font-size: 11.5px; font-weight: 500;
}
/* Amber is asked for, never inherited from the slot. See the macro's note. */
.make-record-page .make-record-row__flag--attention { color: var(--lane-amber); }
/*  OVERDUE IS RED AND NOTHING ELSE IS. DESIGN.md's palette: red is reserved
    for overdue, amber covers every other kind of attention. `late` on a job
    row painted amber until 20 Sep 2026, which left amber meaning both. */
.make-record-page .make-record-row__flag--overdue { color: var(--lane-red); }
.make-record-page .make-record-row__money {
  text-align: end; color: var(--text); font-size: 15px; font-weight: 600;
}
.make-record-page .make-record-row__money-void {
  text-align: end; color: var(--text-muted); font-size: 12.5px;
}
/* Controls go BELOW the identity, not beside it: they act on the record the
   name identifies, and a button on the same line as a name reads as part of
   the name. */
.make-record-page .make-record-card--row .make-record-card__controls {
  display: flex; flex-wrap: wrap; gap: 8px; margin-block-start: 12px;
}

/* --- the inquiry card's control band -------------------------------- */
/* The board's cards carry more interactive furniture than any other in the
   system: up to four buttons, two links and a native date picker. They stack
   full width inside a 270px lane rather than wrapping, because a lane is too
   narrow for two controls side by side and a half-width "Won — open the job"
   wraps to three lines. */
.make-record-page .pipeline .make-record-card__controls {
  display: flex; flex-direction: column; gap: 6px; margin-block-start: 14px;
}
.make-record-page .pipeline .make-record-card__controls .btn { inline-size: 100%; white-space: normal; }
/* The chase date and its button are ONE action and read as one: the field
   above, the verb under it. */
.make-record-page .inquiry-chase { display: flex; flex-direction: column; gap: 6px; }
.make-record-page .inquiry-chase .input { inline-size: 100%; }
/* A lane is a fixed 270px track, so a card inside one has no business
   carrying the grid card's 188px floor — an empty-ish inquiry would be mostly
   air, and four lanes of it is the screen. */
.make-record-page .pipeline .make-record-card { min-block-size: 0; padding: 14px; }
.make-record-page .pipeline .make-record-card__top { margin-block-end: 12px; }
.make-record-page .pipeline .make-record-card__footer { padding-block-start: 12px; }

/*  THE QUOTE'S LINE EDITOR IS A GRID OF CONTROLS, and every cell of it is a
    real field somebody types a price into. They measured 108x36 and 34x32 at
    390 — under the tap minimum on the one screen in this system where a wrong
    number becomes a document that cannot be withdrawn.

    A SENT QUOTE'S FIELDS ARE `readonly`, NOT RESTYLED. They keep the field
    treatment because they are still fields on a draft-shaped screen; the
    immutability rule in this pass is about an ISSUED DOCUMENT, and an issued
    document has no fields at all — see
    test_an_issued_document_is_never_dressed_as_a_form. */
.make-record-page .line-input { min-block-size: 44px; min-inline-size: 44px; }
/*  BOTH DIMENSIONS. Setting only the height left the qty field 34px WIDE at
    390 — still under the minimum, in the other direction, and the check said
    so. The table scrolls inside `.table-card`, so a wider column costs
    nothing but the scroll that is already there. */
/*  A VERSION IS A ROW, AND ITS REFERENCE IS THE ROW'S LINK — 112x17 before
    this, the same defect as the Jobs ref and fixed the same way rather than
    by padding the text. */
.make-record-page .version-row a {
  display: flex; align-items: center; min-block-size: 44px;
}
/*  And the card holding them scrolls rather than clips: `.table-card` carries
    `overflow: hidden` for its corners, which at 924 was cutting the amount
    columns off instead of letting them scroll. */
.make-record-page .table-card { overflow-x: auto; }

/*  A WAY OUT OF A REFUSAL IS A CONTROL. `.todo-link` is the link beside each
    blocker on the issue screen — "Open Customers", "Write the quote" — and it
    measured 101x19 at 390. It is the only thing a person can do on a screen
    that is refusing them, so it takes a control's target size rather than a
    sentence's. */
.make-record-page .todo-link {
  display: inline-flex; align-items: center;
  min-block-size: 44px; padding-inline: 12px;
}

/*  THE WAY OUT OF AN EMPTY LIST IS THE ONLY CONTROL ON THE SCREEN.

    "Show all customers" measured 110x18 inside the notice — a link in a
    sentence, which the tap-target rule normally exempts, and rightly: a
    paragraph of 44px words is worse than a small link. This is the exception
    the exemption has to make room for. On a screen with nothing else on it,
    that link is not prose with a link in it; it is the screen's one action,
    and somebody on a phone has to hit it. */
/*  A NOTICE LINK IS A RECOVERY, NEVER THE PRIMARY ACTION.

    An empty search result reads "Nothing matches. Show all customers", and a
    bare <a> inside .app takes --accent — so that link rendered in the accent on a
    screen whose "Add customer" button is already the one accent primary action.
    Two accent things on a screen is the same as none: the budget is the
    wordmark's period and ONE primary action.

    Neutral with an underline, which is what says "link" without claiming to
    be the thing to do. Every empty state in the system takes this shape —
    Customers, Products, Stock, Machines all render the same pattern — so the
    rule goes on the component rather than into four templates. */
.make-record-page .make-notice a {
  display: inline-flex; align-items: center;
  min-block-size: 44px; padding-inline: 2px;
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/*  TWO LOUDER NOTICES, FOR THE ONE SCREEN THAT NEEDS THEM.

    `.make-notice` is the quiet band every other screen uses. Issue is the one
    place in this system where somebody is about to do something that cannot
    be undone — a tax invoice cannot be reissued, GST Act s.50(a) — so a
    refusal and the two-price warning are given a left edge and a surface
    rather than being left to read like the page's own commentary.

    NEITHER IS THE ACCENT. The accent on that screen is the Issue button and
    nothing else; a warning painted the same colour as the action competes
    with the thing it is warning about. Amber for "read this before you press
    it", the text colour for "this did not happen". */
.make-record-page .make-notice--warn {
  border-inline-start: 3px solid var(--lane-amber);
  background: var(--lane-amber-surface);
  color: var(--text);
}
.make-record-page .make-notice--stop {
  border-inline-start: 3px solid var(--text);
  background: var(--surface-raised);
  color: var(--text);
}

/* --- the page toolbar and result count ------------------------------ */
.make-record-page .make-toolbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 0 0 12px;
}
.make-record-page .make-toolbar :is(input, select) { min-height: 46px; }
/*  THE FILTERS DO NOT STRETCH ACROSS THE TOOLBAR. A 220px basis keeps
    Availability and Category beside the search rather than sharing the row
    equally with it — the search is the wide one.

    THIS WAS AN INLINE `style="flex:0 1 220px"` ON BOTH SELECTS until
    22 Sep 2026, and an inline style beats every rule in this sheet that is
    not !important. So the 900px query below, which makes every toolbar
    control full width on a phone, could not reach them: at 390 the search
    filled the row and the two filters sat at 220px beside nothing. It looked
    deliberate, which is why it survived. The rule lives here now, where the
    media query can override it. */
.make-record-page .make-toolbar select { flex: 0 1 220px; }
.make-record-page .make-toolbar .make-search { flex: 1 1 260px; min-inline-size: 200px; }
/*  CHIPS DO NOT STRETCH. Stock and Machines filter with chips rather than a
    select — four and six states, each worth seeing at a glance — and they
    moved off `.list-controls` onto this toolbar on 15 Sep 2026. That rule
    carried `flex: none` for its chips and this one did not, so on the way
    across every chip would have grown to fill the row. The search still takes
    what is left, which is what `margin-inline-start: auto` does here. */
.make-record-page .make-toolbar .chip { flex: none; }
.make-record-page .make-toolbar .make-search { margin-inline-start: auto; }
.make-record-page .make-count { margin: 0 0 12px; font-size: var(--fs-secondary); color: var(--text-secondary); }
.make-record-page .make-notice {
  max-width: 72ch;
  margin: 0 0 12px; padding: 12px 16px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-sunken);
  font-size: var(--fs-secondary); color: var(--text-secondary);
}

@media (max-width: 1199px) {
  .make-record-page .make-record-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 699px) {
  .make-record-page .make-record-grid { grid-template-columns: minmax(0, 1fr); gap: 16px; }
  .make-record-page .make-record-card { min-block-size: 176px; padding: 20px; }
  .make-record-page .make-record-card__top { margin-block-end: 16px; }
  .make-record-page .make-toolbar { gap: 12px; }
  .make-record-page .make-toolbar :is(input, select) { font-size: 16px; flex: 1 1 100%; }
}


/* --- the parked pricing engine, on product detail ------------------- */
/* A <details>, so it needs no JavaScript and no new component. CLOSED on a
   fresh navigation: the screen opens on what the product IS, not on a
   checklist for an engine nobody is running.

   These three rules were deleted by accident on 12 Sep — the block had been
   moved next to the card CSS to keep the 900px query last, and the slice that
   removed the old card classes took it too. Found by diffing every selector
   against the previous commit rather than by looking at the page, which is the
   only reason it was found at all: a <summary> with no rules still renders. */
.engine-park { margin-top: 20px; border: 1px solid var(--border);
               border-radius: var(--radius-lg); background: var(--surface-sunken); }
.engine-park > summary { position: relative; padding: 16px 20px; cursor: pointer;
                         list-style: none; min-height: 44px;
                         display: flex; flex-direction: column; justify-content: center; gap: 3px; }
.engine-park > summary::-webkit-details-marker { display: none; }
.engine-park > summary::after { content: "Show"; position: absolute; right: 20px; top: 18px;
                                font-size: var(--fs-meta); color: var(--text-muted); }
.engine-park[open] > summary::after { content: "Hide"; }
.engine-park > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.engine-park-title { font-size: var(--fs-section); font-weight: 600; color: var(--text); }
.engine-park-note { font-size: var(--fs-secondary); color: var(--text-secondary); max-width: 62ch; }
.engine-park > .panel { margin: 0 16px 16px; }

/* --- a form body has a working width ------------------------------- */
/* Prose gets a measure because a long line is hard to read. A FORM NEEDS ONE
   FOR A DIFFERENT REASON: an input stretched across 2200px is a target the
   eye has to travel, and a label at one end with its field at the other stops
   reading as a pair. 760px is wide enough for an address and narrow enough
   that the label, the field and its hint stay one object.
   The RECORD GRIDS are deliberately not capped — they gain columns instead,
   which is what the width is for. */
.quiet-link { color: var(--text-secondary); text-decoration: underline;
              text-underline-offset: 2px; }
.quiet-link:hover { color: var(--text); }

.form-card .input, .form-card select, .form-card textarea { max-inline-size: 100%; }
/*  A CARD INSIDE A PANEL PAINTS A SECOND BOX. `.form-card` carries a surface,
    a border and a shadow, which is right when the form IS the page and wrong
    when a panel has already drawn one: Stock's movement form rendered as a
    760px rectangle floating inside a wider panel, with a seam down the middle
    of an empty right-hand half. The width is what the form wants from this
    class there; the box is the panel's job. */
/*  INSIDE ANY PANEL, NOT ONLY AS ITS CHILD, AND THE INSET GOES WITH THE BOX.
    `.panel > .form-card` caught one shape. A form-card that IS a panel body
    (`.panel-body.form-card`), or sits inside one, kept its 20px padding on
    top of the body's 16px — every field 4–8px right of the panel's own
    labels — and the calculator drew a card inside a card. The panel owns the
    box and the inset; the form-card keeps only its column rhythm. */
.panel .form-card { background: none; border: 0; box-shadow: none; border-radius: 0; }
.panel .panel-body .form-card, .panel .form-card .form-card { padding: 0; }
.panel .panel-body.form-card { padding: 14px 16px; }
/* Two short fields pair only when there is room for both to stay legible. */
@media (min-width: 1100px) {
  .form-card .field-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; }
}

/* --- prose gets a measure, everywhere ------------------------------- */
/* Measured at 2200 before this existed: .notice ran to 299 characters on one
   line on Money and .work-meta to 228 on Jobs, while .page-note — the only
   prose class that already had `max-width: 62ch` — sat at 72. A line that
   long is not read, it is scanned back to find where it started.
   72ch rather than 62: these are single-line facts beside a record, not
   paragraphs, and 62 breaks them where nothing wants breaking. */
.notice, .work-meta, .make-notice, .list-summary,
.panel-note, .field .hint, .recall-spec { max-width: 72ch; }

/* --- pagination ----------------------------------------------------- */
.pager { display: flex; align-items: center; gap: 10px; margin: 16px 0 0;
         font-size: var(--fs-secondary); color: var(--text-secondary); }
.pager-spacer { flex: 1; }

@media (max-width: 700px) {
  .list-search { flex: 1 1 100%; margin-left: 0; order: -1; }
  /*  `.app select` SINCE 21 Sep 2026. This named `.status-select`, so it
      raised the ONE select that already looked right and left the other
      thirty-six at 40px on a 700px screen.

      SCOPED, NOT BARE, AND THAT IS NOT DECORATION. Written as `select` it
      loses to `.app select` on specificity — 0,0,1 against 0,1,1 — and being
      later in the sheet does not save it. Every form select measured 40px at
      390 with this rule sitting right there looking correct. */
  .app select, .login-screen select, .btn-sm { min-height: 44px; }
}

/* ══════════════════════════════════════════════════════════════════════
   PAPER. 17 Sep 2026.

   THE MECHANISM, NOT THE LAYOUT. Hasan's pass designs what a quotation and
   an invoice look like; this is the block it lands in, and everything here
   is about getting the document onto a page at all — the shell off, the
   ground white, the ink black, the table unbroken across pages.

   WHY IT SITS ABOVE THE 900px QUERY AND NOT AT THE FOOT OF THE FILE.
   test_the_900px_query_is_the_last_block_in_the_sheet asserts the mobile
   query is the LAST @media, because `.app { grid-template-columns: 1fr }`
   beats the desktop rule only by source order and anything appended below
   silently restores the 232px sidebar on a phone. A print block below it
   would not actually do that — print and screen never both apply — but the
   guard cannot know that, and weakening a guard to make room for a new rule
   is how the thing it guards comes back. So the rule moved instead.

   THIS IS THE BROWSER'S Save as PDF PATH, deliberately. WeasyPrint is a
   later decision taken with the dashboard's move off RAILPACK onto a
   Dockerfile — it cannot install Pango and Cairo, which the running image
   does not carry. The CSS is the same either way, which is why this is not
   throwaway work.
   ══════════════════════════════════════════════════════════════════════ */
/* ══════════════════════════════════════════════════════════════════════
   THE PRINTED DOCUMENT — its own layout, not the screen restyled.

   HIDDEN ON SCREEN, AND THE ONLY THING SHOWN IN PRINT. The screen above it
   is written for staff and reads like it: "Correct it on the inquiry, not
   here", "Recorded as sent by Local Dev", a payment state that changes the
   day a receipt arrives. Restyling that for paper is a game of finding the
   next sentence that should not have left the building. This is a separate
   document, so there is no next one.

   MILLIMETRES, BECAUSE THE TARGET IS A4. The rest of this sheet is px — a
   screen has no millimetres — and a document does: 15mm of side margin is a
   real distance on a real page and survives a guillotine.

   IT MATCHES THE PUBLISHED TEMPLATE, which is Hasan's own invoice with the
   MIRA corrections applied. Sizes, rules and spacing are read from it rather
   than re-derived, because it is the approved shape and not a suggestion.
   ══════════════════════════════════════════════════════════════════════ */
.sheet { display: none; }

@media print {
  /*  THE SCREEN GOES, THE DOCUMENT COMES. Both in one place so neither can
      be changed without the other being obvious.

      `.app` ENTIRELY, and the document is its SIBLING — see base.html's
      `document` block. The first build hid `.app > *` with the sheet nested
      inside, which hides the sheet's own ancestor: it printed four blank
      pages while every template rendered correctly on screen. A child cannot
      display its way out of a hidden parent. */
  .app { display: none !important; }
  .sheet { display: flex !important; }

  .sheet {
    flex-direction: column;
    /*  NO WIDTH AND NO MIN-HEIGHT. @page owns the paper; a width here would
        fight it, and the published template's 210mm belongs to its own
        on-screen preview where there is no @page to defer to. */
    padding: 0;
    background: #fff;
    color: #111111;
    font-family: var(--font);
    /*  NO LIGATURES ON PAPER (29 Sep 2026). General Sans joins "fi" into one
        glyph, and a PDF's text is its glyphs: "rufiyaa" printed as
        "ruﬁyaa" and a search of the document for the word found nothing.
        Both properties, because a renderer honouring only one still joins.
        Naming liga and clig alone leaves tabular figures as they are. */
    font-variant-ligatures: none;
    font-feature-settings: "liga" 0, "clig" 0;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  /*  THE SHEET'S OWN INK. Named here rather than reusing the app's tokens:
      those describe a dark interface, and every one of them is overridden to
      near-white above. A document's greys are its own. */
  .sheet {
    --doc-ink:   #111111;
    --doc-ink-2: #3d3d3d;
    --doc-ink-3: #6e6e6e;
    --doc-rule:  #e2e2e2;
    --doc-firm:  #111111;
    --doc-band:  #f6f6f6;
  }

  /* ---- letterhead ---- */
  .sheet-top { display: flex; align-items: flex-start; justify-content: space-between;
    gap: 12mm; padding-bottom: 6mm; }
  .sheet-wordmark { font-size: 34px; font-weight: 700; letter-spacing: -.022em;
    line-height: .9; margin: 0; color: var(--doc-ink); }
  /*  THE WORDMARK PERIOD IS THE ACCENT on paper too — .wordmark-dot, the
      screen's own rule, since the mark became _wordmark.svg (27 Sep 2026).
      .sheet-dot is gone with the typed text it coloured. */
  .sheet-contact { text-align: right; font-size: 9.5px; line-height: 1.65;
    color: var(--doc-ink-2); padding-top: 2mm; }
  .sheet-contact b { font-weight: 600; color: var(--doc-ink); }

  /* ---- title ---- */
  .sheet-title-row { display: flex; align-items: baseline; justify-content: space-between;
    gap: 8mm; border-top: 1.5px solid var(--doc-firm); padding-top: 4mm; margin-top: 1mm; }
  .sheet-title { margin: 0; font-size: 23px; font-weight: 700; letter-spacing: .005em;
    color: var(--doc-ink); }
  .sheet-title-note { font-size: 9.5px; color: var(--doc-ink-3); text-align: right;
    line-height: 1.5; }
  /*  MIRA'S SENTENCE ON A QUOTATION, and it has to be seen: full ink, the
      weight of the meta values, beside the title rather than under it. */
  .sheet-not-invoice { margin: 0; font-size: 12px; font-weight: 700;
    color: var(--doc-ink); text-align: right; }

  /* ---- the project, under the title (079) ---- */
  .sheet-project { margin: 3mm 0 0; font-size: 12px; font-weight: 600; color: var(--doc-ink); }
  .sheet-project .sheet-eyebrow { display: inline; margin: 0 2mm 0 0; }

  /* ---- parties ---- */
  .sheet-parties { display: grid; grid-template-columns: 1fr 1fr; gap: 10mm; margin-top: 7mm; }
  .sheet-eyebrow { font-size: 8.5px; font-weight: 600; letter-spacing: .11em;
    text-transform: uppercase; color: var(--doc-ink-3); margin: 0 0 2.5mm; }
  .sheet-party { font-size: 11px; line-height: 1.55; color: var(--doc-ink-2); }
  .sheet-party-name { font-size: 13px; font-weight: 600; color: var(--doc-ink);
    display: block; margin-bottom: 1mm; }
  .sheet-tin { display: block; margin-top: 1.5mm; }

  /* ---- meta ---- */
  .sheet-meta { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
    margin: 7mm 0 0; border: 1px solid var(--doc-rule); background: var(--doc-band); }
  .sheet-meta > div { padding: 3.5mm 4mm; border-right: 1px solid var(--doc-rule); }
  .sheet-meta > div:last-child { border-right: 0; }
  .sheet-meta dt { font-size: 8.5px; font-weight: 600; letter-spacing: .09em;
    text-transform: uppercase; color: var(--doc-ink-3); margin: 0 0 1.5mm; }
  .sheet-meta dd { margin: 0; font-size: 11px; font-weight: 600; color: var(--doc-ink);
    font-variant-numeric: tabular-nums; }
  /* SIX FACTS IN A ROW (079: date of supply, due date) — a number never
     breaks across two lines, so the cells give up padding and a size step. */
  .sheet-meta:has(> div:nth-child(6)) > div { padding: 3mm 2.5mm; }
  .sheet-meta:has(> div:nth-child(6)) dd { font-size: 10px; white-space: nowrap; }

  /* ---- a credit note's correction ---- */
  .sheet-corrects { margin-top: 6mm; }
  .sheet-corrects-line { margin: 0; font-size: 11px; line-height: 1.55; color: var(--doc-ink-2); }
  .sheet-corrects-line strong { color: var(--doc-ink); font-weight: 600; }

  section.sheet-spec { margin-top: 7mm; white-space: normal; color: inherit; }  /* the block; a line's own .sheet-spec span is below */
  .sheet-spec p:not(.sheet-eyebrow) { margin: 0; font-size: 10.5px; line-height: 1.6;
    color: var(--doc-ink-2); max-width: 150mm; }

  /* ---- lines ---- */
  /*  THE REPEATED HEADER STARTS WHERE PAGE 1 DOES. Page 1's first ink is
      the wordmark, 20.9mm from the top edge — its line box sits 2.9mm into
      the 18mm margin. The header Chrome repeats on a continuation page sat
      at 18.6mm, jammed 2.3mm higher. Measured from the rendered PDF, 26 Sep
      2026. So the header cells carry the 2.3mm and the table's own margin
      gives it back, which leaves page 1 exactly where it was. */
  .sheet-lines { width: 100%; border-collapse: collapse; margin-top: 4.7mm; }
  .sheet-lines thead { display: table-header-group; }
  .sheet-lines thead th { font-size: 8.5px; font-weight: 600; letter-spacing: .09em;
    text-transform: uppercase; color: var(--doc-ink-3); text-align: left;
    padding: 2.3mm 0 2.5mm; border-bottom: 1.5px solid var(--doc-firm); }
  .sheet-lines thead th.num { text-align: right; }
  .sheet-lines tbody td { padding: 4mm 0; border-bottom: 1px solid var(--doc-rule);
    font-size: 11px; line-height: 1.55; vertical-align: top; color: var(--doc-ink-2); }
  .sheet-lines tbody td.num { text-align: right; white-space: nowrap;
    font-variant-numeric: tabular-nums; padding-left: 6mm; }
  .sheet-desc { font-weight: 600; display: block; color: var(--doc-ink); }
  /*  The specification under it (migration 070): the line's own breaks kept. */
  .sheet-spec { display: block; white-space: pre-line; margin-top: 1mm; color: var(--doc-ink-2); }
  /*  A LINE DOES NOT SPLIT ACROSS A PAGE, and the header repeats when the
      table does. Both matter on the 40-line invoice the print script builds
      precisely to reach a third page. */
  .sheet-lines tbody tr { break-inside: avoid; }

  /* ---- totals ---- */
  .sheet-totals { margin: 5mm 0 0 auto; width: 82mm; font-size: 11px; }
  .sheet-totals .row { display: flex; justify-content: space-between; gap: 8mm;
    padding: 2.2mm 0; color: var(--doc-ink-2); }
  .sheet-totals .row span:last-child { font-variant-numeric: tabular-nums; }
  .sheet-totals .grand { border-top: 1.5px solid var(--doc-firm); margin-top: 2mm;
    padding-top: 3mm; font-size: 14px; font-weight: 700; color: var(--doc-ink); }

  .sheet-note { margin-top: 7mm; }
  .sheet-note p { margin: 0; font-size: 10.5px; line-height: 1.6;
    color: var(--doc-ink-2); max-width: 150mm; }

  /* ---- foot ---- */
  /*  margin-top:auto PUSHES THE FOOT TO THE BOTTOM of a short document and
      lets it follow the content on a long one. */
  /*  ONE BLOCK, NEVER SPLIT (25 Oct 2026): quote MK-Q-1025-649A printed
      "Page 1 of 2" with page 2 holding only the registered-name line. The foot
      moves as a whole, so a page 2 always carries the payment block; and its
      spacing is tight enough that four lines and the foot fit on one A4. */
  .sheet-foot { margin-top: auto; padding-top: 5mm; break-inside: avoid; }
  .sheet-foot-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10mm;
    border-top: 1px solid var(--doc-rule); padding-top: 5mm; }
  /*  ONE COLUMN WHEN THERE IS NO PAYMENT BLOCK — a credit note and a
      quotation. A 1fr 1fr grid with one cell filled leaves half the
      foot empty and reads as something that failed to render. */
  .sheet-foot-grid--one { grid-template-columns: 1fr; }
  .sheet-bank, .sheet-terms { font-size: 10.5px; line-height: 1.7; color: var(--doc-ink-2); }
  .sheet-acct { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--doc-ink); }
  .sheet-terms strong { color: var(--doc-ink); font-weight: 600; }
  .sheet-legal { margin-top: 3mm; padding-top: 2mm; border-top: 1px solid var(--doc-rule);
    font-size: 9px; line-height: 1.6; color: var(--doc-ink-3); }
}

@media print {
  /* A4 WITH MARGINS. 18mm is the narrowest a Male' print shop's own paper
     guillotine leaves reliably, and a tax invoice that loses its TIN to a
     trim is not a tax invoice. */
  @page { size: A4; margin: 18mm 16mm;
    /*  The running foot — the document's number and "Page n of N", written
        by _document_sheet.html. Literal values, not tokens: a margin box is
        not inside any element, so nothing it could inherit a token from is
        certain across engines. The family is --font's. */
    @bottom-right { font-family: "General Sans", system-ui, -apple-system,
        "Helvetica Neue", Helvetica, Arial, sans-serif;
      font-size: 8.5px; color: #6e6e6e; vertical-align: middle; }
  }

  /*  A LIGHT CANVAS UNDER THE PAPER, 26 Sep 2026. `:root` says
      `color-scheme: dark`, unconditionally, and Chrome paints the page
      canvas from it — so every printed document since the first had an
      rgb(18,18,18) band in the 18mm margin round a white sheet. Every PDF
      check read the TEXT, which was perfect; only a picture of the page
      showed it. html and body are white below; the canvas is not theirs. */
  :root { color-scheme: light; }

  /* LIGHT GROUND, DARK INK. The screen is a dark surface because somebody
     reads it for eight hours; a legal document is printed on paper, and a
     dark theme on paper is either invisible or a page of ink. Tokens are
     overridden rather than rules rewritten, so every component follows. */
  :root, .app, .login-screen {
    --bg: #ffffff;
    --surface: #ffffff;
    --surface-raised: #ffffff;
    --surface-sunken: #ffffff;
    --surface-hover: #ffffff;
    --text: #000000;
    --text-secondary: #333333;
    --text-muted: #555555;
    --border: #999999;
    --border-raised: #999999;
    --border-strong: #666666;
    --card-border: #999999;
    --card-shadow: none;
    --focus: #000000;
    /*  THE STATE COLOURS GET PAPER VALUES TOO, 21 Sep 2026. A tax invoice
        carries a "Settled" pill, and the screen's values are a near-black
        green ground (#163126) with pale green text — correct at 10:1 on a
        dark surface and, on white A4, a heavy block of ink around two words.
        The dark-mode values were not wrong; they were being printed.

        Tokens are overridden rather than the pill's rules rewritten, so the
        component is unchanged and every variant follows. Measured on white:
          green text   #17492f on #e8f6ed   9.28:1   (AA needs 4.5)
          green dot    #2e7d50 on #e8f6ed   4.52:1   (non-text needs 3)
          green border #4a8a63 on white     4.11:1   (non-text needs 3)
          amber text   #7a4b00 on #fdf1dd   6.63:1
          red text     #9a1b16 on #fde8e7   7.03:1
        Each stays well past its threshold, which is the point of restating
        them rather than letting the ground change underneath the ink. */
    --state-good: #2e7d50;
    --state-good-surface: #e8f6ed;
    --state-good-border: #4a8a63;
    --state-good-text: #17492f;
    --lane-amber: #7a4b00;
    --lane-amber-surface: #fdf1dd;
    --lane-red: #9a1b16;
    --lane-red-surface: #fde8e7;
  }
  html, body { background: #ffffff; color: #000000; }

  /* THE SHELL IS NOT PART OF THE DOCUMENT. Navigation, the account footer,
     and the controls that act on the record — none of them exist on paper,
     and a control printed is a control somebody cannot press. */
  .sidebar, .mobile-bar, .mobile-nav, .head-actions, .page-actions,
  .make-toolbar, .flash, .todo-link, .btn, form.receipt-form,
  .make-record-card__controls, .issue-gate, .version-row a,
  .file-row__open, .file-row__actions,
  nav, .nav-count,
  /*  ANYTHING THE SCREEN CALLS INTERNAL. The quote's internal note is
      labelled "never printed" and printed, because a label is a promise and
      not a mechanism. `.print-never` is the mechanism. */
  .print-never { display: none !important; }

  /*  A CONTROL IS NOT A VALUE. The quote screen is an EDITOR — its lines,
      its product and its notes are a form — and a printed <select> is a
      dropdown arrow on paper, a printed <textarea> is a box with a resize
      handle in the corner. The value stays and the control's chrome goes:
      no border, no background, no arrow, no fixed height. The customer reads
      what was written, not the machinery it was written in. */
  /*  THE SCOPED FORMS ARE LISTED TOO, 21 Sep 2026. The declarations here
      that carry `!important` won anyway; the ones that do not — `font:
      inherit` above all — lost to `.app select`'s own 14px, so a printed
      select kept a screen control's type size while every field beside it
      inherited the document's. Same rule, matched specificity. */
  select, .app select, .login-screen select,
  textarea, input, .input, .line-input, .note-area {
    -webkit-appearance: none; appearance: none;
    border: 0 !important; background: transparent !important;
    padding: 0 !important; box-shadow: none !important;
    resize: none; min-block-size: 0 !important; block-size: auto;
    color: inherit; font: inherit;
  }

  /* ONE COLUMN. The shell is a 232px sidebar plus content; with the sidebar
     gone the track would still be there, printing the document in the
     right-hand two thirds of the page. */
  .app { display: block; grid-template-columns: none; }
  /*  A PAGE THAT IS ALWAYS AT LEAST ONE VIEWPORT TALL IS ALWAYS AT LEAST ONE
      PAGE TALL. `.app` carries `min-height: 100%` so the sidebar reaches the
      bottom of a short screen; in print that resolves against the page box
      and pushed a one-page invoice onto two — the second holding nothing but
      the bottom of a box. Found on paper; invisible in every browser window,
      because on screen it is doing exactly what it was written to do. */
  html, body, .app, .page, .make-record-page { min-height: 0; height: auto; }
  /*  And nothing trails the last element into a new page. */
  .page > *:last-child, .record-stack > *:last-child { margin-block-end: 0; }
  .page, .make-record-page { padding: 0; gap: 12px; }
  /* The document and its aside stack: receipts and the issue control are
     screen furniture, and what remains of the aside belongs after the
     document rather than beside it. */
  .quote-layout, .page-columns { display: block; }
  /*  THE STACK DOES NOT PRINT, AND THIS IS A CORRECTNESS ARGUMENT RATHER
      THAN A LAYOUT ONE.

      `.record-stack` holds Receipts, the Outstanding figure and Corrections.
      None of them is part of the document. Outstanding is DERIVED — total
      minus receipts — and it changes every time money arrives, so printing it
      inside an issued invoice puts a mutable figure on an immutable record,
      and the copy in the customer's hand starts disagreeing with the system
      the moment they pay. Corrections are their own documents, each with its
      own number, and a credit note is not a footnote to the invoice it
      corrects.

      It is also what forced a second page on an invoice that fits on one:
      bisected by printing the same document with the stack removed — 2 pages
      became 1.

      If the studio ever wants a statement of account, that is a document of
      its own with its own heading, not this one with extra sections. */
  .record-stack, .quote-layout > *:not(:first-child) { display: none; }

  /* NO CARD CHROME. A panel is a surface that groups things on a screen. On
     paper the page is the surface, and a border around every group reads as
     a form to be filled in — which an issued document must never look like. */
  .panel, .make-record-card, .table-card, .form-card, .make-notice {
    border: 0; border-radius: 0; box-shadow: none; background: transparent;
    padding-inline: 0;
  }
  .panel-head { border: 0; padding-inline: 0; }

  /* A SCROLL CONTAINER CLIPS ON PAPER. `.table-scroll` and the 900px
     `display: block` rule both exist so a wide table can be reached by
     scrolling; there is no scrolling on a page, so the overflow becomes a
     cut edge. Released, and the table is allowed to be a table again. */
  .table-scroll, .table-card { overflow: visible !important; }
  .table-data, .lines-table { display: table !important; overflow: visible !important;
    inline-size: 100%; border-collapse: collapse; }

  /* THE HEADER REPEATS ON EVERY PAGE, and a line never straddles two.
     A long invoice's second page without column headings is a list of
     numbers nobody can read, and a description split across a page break is
     a description that appears twice and says half of itself each time. */
  thead { display: table-header-group; }
  tfoot { display: table-footer-group; }
  tr, .make-record-card, .receipt-row, .total-row { break-inside: avoid; }
  .totals, .doc-parties { break-inside: avoid; }
  h1, h2, .panel-title { break-after: avoid; }

  /* Mono stays: a column of amounts still has to line up. General Sans has
     no tabular figures — 28.7% spread across the ten digits, measured — and
     that is as true in ink as on a screen. */
  .cell-mono, .money { font-variant-numeric: tabular-nums; }

  /* A LINK'S HREF IS NOT PRINTED. The usual `a::after { content: attr(href) }`
     would put `/invoices/25` in the middle of a legal document; these are
     internal paths that mean nothing to a customer. */
  a { color: inherit; text-decoration: none; }
}

/*  RECORDING AN ANSWER: A ROW OF CONTROLS UNDER A LABEL THAT NAMES IT.
    Two callers, and both got here the same way. The proof's two answers and
    the artwork judge's two answers were first written as .inquiry-chase — a
    vertical stack written for the board card's single chase-date input. Five
    controls in one column read as ONE long form: "Who approved it / How /
    [Record the approval] / Colour off / What has to change / [Record what is
    wrong]", with each button a long way from the fields it submits. Measured
    at 1400 and at 390; it was the same shape at both, and on the artwork row
    the column was narrow enough to cut the note's placeholder off mid-word.

    THAT LAST CLAUSE READ AS A DEFECT THAT HAD BEEN FIXED, AND IT HAD NOT.
    The reshape moved the controls; it left the note input 22px short of its
    own placeholder, and on 20 Sep 2026 it was still cutting it. The number
    that decides it is below, with the measurement that chose it. Leaving the
    sentence here because the history is real — noting what it does not mean.

    THE LABEL IS WHAT MAKES THEM TWO THINGS. Without it the separation is only
    whitespace, and whitespace reads as a gap inside one form. */
.answer-set { display: flex; flex-direction: column; gap: 14px;
              margin-block-start: 10px; inline-size: 100%; }
.answer-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
/*  THE BASIS IS THE WIDEST PLACEHOLDER, MEASURED, NOT A ROUND NUMBER.
    160px was a guess, and it was 22px short of the text it had to hold. The
    four placeholders these rows carry, measured in the control's own font on
    20 Sep 2026:

        "Who approved it"                103px
        "What to tell the customer"      160px
        "How — WhatsApp, in person"      177px
        "What has to change (optional)"  188px   <- the widest
                                        + 22px   the input's own padding
                                        ------
                                          210px
                                       -> 215px   rounded up, see below

    At 390 the note input was landing at 170px — sharing a line with a 126px
    button — against the 182px "What to tell the customer" needs, and cut its
    own label off mid-word. A control that cannot show the question it is
    asking is a control nobody can answer.

    WHY A BASIS AND NOT A MINIMUM. `min-inline-size: 210px` would hold the
    text and then overflow any container narrower than that; the basis makes
    the item WRAP to its own line first and shrink only if the row itself is
    genuinely too narrow, which is what flex-wrap is already here for. Desktop
    is unchanged: at 1400 the row is 582px, all three still fit on one line,
    and grow to the same 222/222/126 they resolved to before.

    WHY 215 AND NOT THE 210 THE MEASUREMENT GIVES. The guard that holds this
    has no font metrics — the suite has no browser — so it estimates a
    placeholder's width at 6.5px per character. That is a hair over this
    font's real 6.48, which is the right direction for a guard but means an
    exactly-fitting basis fails it. The five px are that headroom, stated here
    rather than left as a number that looks arbitrary and gets rounded away.

    Raise it only by measuring a new placeholder, and if one ever needs more
    than the panel can give, shorten the placeholder rather than the box. */
.answer-row .input { flex: 1 1 215px; inline-size: auto; min-inline-size: 0; }
.answer-row .btn { flex: 0 0 auto; }
.answer-label { color: var(--text-muted); font-size: 12px;
                letter-spacing: 0.04em; text-transform: uppercase;
                margin-block-end: 4px; }
/*  The row component's controls slot is a flex column sized to its contents;
    an answer set inside one has to be told to take the body track, or it
    renders at ~320px in a 600px column with the rest of the row empty. */
.make-record-card__controls .answer-set { inline-size: 100%; }


/* ===================================================================
   MONEY — the two totals, and the table that becomes records
   =================================================================== */

/*  THE TWO FIGURES THE SCREEN EXISTS TO ANSWER. Derived in money.py from one
    read of the per-currency balances; see the note there for why this is two
    sums and not the tfoot's one net figure — and one tile per currency. */
.money-totals {
  display: flex; flex-wrap: wrap; gap: 12px;
  margin-block-end: 16px;
}
/*  TWO ACROSS AT EVERY WIDTH, INCLUDING 390.

    `flex: 1 1 220px` needed 452px for the pair, so at 390 they stacked into
    two full-width slabs and pushed the balances table — the evidence the
    summary is a summary OF — below the fold. The brief asked for a compact
    summary; a compact summary that hides the table is not one.

    140px basis and a smaller figure below 700: "Rf 9,684.59" measures ~118px
    at 20px, so the pair fits the 358px a 390 viewport leaves with room to
    spare. The figure stays tabular so the two align. */
.money-tile {
  flex: 1 1 140px; min-inline-size: 0;
  padding: 16px 18px;
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface-raised);
}
/*  ONE TILE PER CURRENCY SINCE 29 SEP 2026 (migration 061), so five tiles on
    fixture data: owed now and credit per currency, suppliers per currency.
    At 220px the fifth wrapped and stretched across the whole row, a slab
    reading as a heading — seen on the capture. 180px puts five on one row
    at 1400, and no tile grows past half the row when one is left over. */
@media (min-width: 700px) {
  .money-tile { flex-basis: 180px; max-inline-size: calc(50% - 6px); }
}
@media (max-width: 699px) {
  .money-tile { padding: 13px 15px; }
  /*  FITS AT 390, measured rather than assumed. The note above sized 20px
      for "Rf 9,684.59"; the balance grew to "Rf 10,267.79" and the figure
      ran past its tile's edge — overflow INSIDE an element, which the
      page-level overflow check cannot see. 4.6vw is 17.9px at 390 and the
      longest balance this studio will see in a year still fits the 141px a
      tile's content box has there. */
  .money-tile__figure { font-size: min(20px, 4vw); }
  /*  AND A TILE NEVER SHRINKS BELOW ITS FIGURE. If a balance is ever too
      long for two across, the tiles stack rather than the figure running
      past the edge — first pass at 4.6vw still overran by 10px at 390. */
  .money-tile { min-inline-size: min-content; }
  .money-tile__label { font-size: 10.5px; }
}
/*  THE UNIT IS A FIXED WIDTH IN THE BALANCES TABLE (29 Sep 2026). Each
    amount carries its own currency now, and "USD" is narrower than "MVR":
    in the stacked 390 layout, where amounts sit left-aligned, two figures
    of the same length landed 2px apart — measure-screens' ragged check. */
.money-table .money-unit { display: inline-block; inline-size: 2.6em; }
.money-tile__label {
  margin: 0 0 6px; font-size: 11px; font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted);
}
.money-tile__figure { margin: 0; font-size: 26px; font-variant-numeric: tabular-nums; }
/*  CREDIT IS QUIETER, NOT COLOURED. Money the studio owes is a real figure and
    a rarer one; it is secondary to what is owed IN, and it is not a warning.
    Red is overdue and amber is attention — neither describes a credit. */
.money-tile__figure--quiet { color: var(--text-secondary); }

/*  A SETTLED CUSTOMER RECEDES IN EVERYONE. Zero is not nothing — they are
    listed because the filter says everyone — but they are not what a person
    opened this screen for. */
.money-table tbody tr.is-settled td { color: var(--text-muted); }
.money-table tbody tr.is-settled .cell-strong { font-weight: 500; }

/*  ── BELOW 1100px THE TABLE BECOMES ONE RECORD PER CUSTOMER ─────────────
 *
 *  NOT A SCROLLER, AND THAT IS THE WHOLE POINT. `.table-data` used to become
 *  `display: block; overflow-x: auto` below 900 and sit inside `.table-scroll`
 *  above it. Measured on the 20 Sep captures: at 924 the Documents column was
 *  CLIPPED, and at 390 the Balance column — the one thing this screen is for —
 *  was off the visible edge. A sideways scroller answers "can you reach it"
 *  and the question is "can you see it".
 *
 *  1100 rather than 900: five columns stop fitting well before the shell
 *  breakpoint, which is why 924 was already clipping. The breakpoint belongs
 *  to the table's own content, not to the sidebar.
 *
 *  THE ORDER IS THE BRIEF'S: name and balance on one line, invoiced and
 *  received beneath as supporting evidence, Documents trailing. Balance is
 *  never below the fold of its own row and never off the side.
 */
@media (max-width: 1099px) {
  .money-table, .money-table tbody, .money-table tr, .money-table td {
    display: block; inline-size: 100%;
  }
  /*  THE HEADER ROW GOES, and its words come back on the cells that need
      them via `data-label` — written in the markup, not invented here.
      `display: none`, not a visually-hidden clip: the rows are already
      `display: block` at this width, so the table's own semantics are gone
      and a retained <thead> associates with nothing. It also read as a
      finding to measure-screens.py's clipped check, correctly — a 1px box
      with content scrolling inside it is what that check looks for, and
      "it is deliberate" is the excuse that makes a check useless. */
  .money-table thead { display: none; }
  .money-table { overflow-x: visible; }

  .money-table tbody tr {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 2px 12px;
    padding: 16px 18px;
    border-block-end: 1px solid var(--border);
  }
  .money-table tbody tr:last-child { border-block-end: 0; }
  .money-table tbody td { border-block-end: 0; block-size: auto; padding: 0; }

  /*  LINE ONE — who, and what they owe. */
  .money-cell-who { grid-column: 1; grid-row: 1; }
  .money-cell-balance {
    grid-column: 2; grid-row: 1;
    text-align: end; font-size: 17px;
  }
  /*  LINE TWO — the evidence, quieter, with its column names attached. */
  /*  ONE LINE EACH, LABEL LEFT AND AMOUNT RIGHT (29 Sep 2026). Invoiced sat
      left-aligned beside Received, so its right edge moved with its length;
      with a currency on every amount (migration 061), figures one digit
      apart landed 7px apart — measure-screens' ragged check. Every amount in
      a record now ends on the record's right edge, the balance's too. */
  .money-cell-sub {
    grid-column: 1 / -1;
    display: flex; justify-content: space-between; gap: 12px;
    font-size: 12.5px; color: var(--text-secondary);
  }
  .money-cell-sub[data-label="Invoiced"] { grid-row: 2; }
  .money-cell-sub[data-label="Received"] { grid-row: 3; }
  .money-cell-sub::before {
    content: attr(data-label) " ";
    font-family: var(--font); color: var(--text-muted);
  }
  /*  LINE THREE — the action. */
  .money-cell-docs { grid-column: 1 / -1; grid-row: 4; margin-block-start: 10px; }
  .money-cell-docs:empty { display: none; }

  /*  THE TOTALS ROW KEEPS ITS COLUMNS' MEANING when there are no columns. */
  .money-table tfoot, .money-table tfoot tr, .money-table tfoot td { display: block; }
  .money-table tfoot tr {
    display: flex; flex-wrap: wrap; gap: 4px 16px; padding: 12px 18px;
  }
  /*  The row carries the inset, as the body rows do; the cell's own 16px on
      top of it put the totals' figures 16px inside the column above. */
  .money-table tfoot td { padding: 0; }
  .money-table tfoot td:empty { display: none; }
}


/* ===================================================================
   QUOTE — a sent version is a record, not a form
   =================================================================== */

/*  STATIC LINE CELLS. A sent quote rendered every cell as <input readonly>
    and the product as <select disabled>. Both keep a control's box, border
    and affordance, so the document said "you can edit this" and then refused
    — while "Revise — creates v2", the one honest editing path, competed with
    four boxes that looked like they took typing. */
.line-static {
  display: block; padding: 9px 12px;
  font-size: 13.5px; line-height: 1.45; color: var(--text);
}
.lines-table .num .line-static { text-align: end; font-variant-numeric: tabular-nums;
  /*  NO SECOND INSET IN A FIGURE CELL. The cell has its padding; 12px more
      each side left "3,200.00" 40px at 390, and it overflowed its cell 13px
      past the edge every other figure on the card ends on. */
  padding-inline: 0; }
.note-static {
  margin: 0; padding: 0; font-size: 14px; line-height: 1.55;
  color: var(--text); white-space: pre-wrap;
}

/*  THE COMPACT DOCUMENT LABEL. Same words' meaning, a fraction of the
    weight: a required disclaimer should not outrank the quote's own
    reference and state. */
.doc-mark--compact {
  border-color: var(--border-strong); color: var(--text-secondary);
  font-weight: 600; letter-spacing: .05em; text-transform: none;
  font-size: 11.5px; padding: 4px 9px;
}

/*  INTERNAL SETUP, ABOVE THE DOCUMENT AND NOT INSIDE IT. The browser's own
    <details>; this repo has no JavaScript for a toggle and needs none.
    Closed it is one quiet line, which is the right size for a studio-only
    field that most edits never touch. */
.internal-band {
  border: 1px dashed var(--border-strong); border-radius: var(--radius);
  background: var(--surface-sunken);
}
.internal-band__summary {
  padding: 11px 16px; cursor: pointer;
  font-size: 12px; font-weight: 600; letter-spacing: .04em;
  color: var(--text-secondary);
  min-block-size: 44px; display: flex; align-items: center;
}
.internal-band__body {
  padding: 4px 16px 16px;
  display: flex; flex-direction: column; gap: 12px;
}
.internal-band__row { max-inline-size: 420px; }

/*  A SENT QUOTE'S INTERNAL NOTE IS NOT PART OF THE DOCUMENT. Dashed, sunken
    and outside the document panel, so nothing about it reads as the offer. */
.internal-note {
  border-style: dashed; border-color: var(--border-strong);
  background: var(--surface-sunken);
}

/*  THE RECALL RAIL IS REFERENCE, NOT THE DOCUMENT.

    ONE RECORD GETS A ROW, NOT A CARD. `.recall-item` in a 340px panel with a
    single entry was a tall mostly-empty surface competing with the quote it
    is meant to support. With one row the panel collapses to the height of
    its content.

    AND IT DROPS BELOW THE DOCUMENT BEFORE EITHER IS CRAMPED. The 340px rail
    beside a minmax(0,1fr) document means the writing column is the one that
    gives way; at 1200 it is under 700px with a four-column line table in it.
    A container query would be better but .quote-layout is the container, so
    the width being asked about IS the viewport here. */
@media (max-width: 1199px) {
  .quote-layout { grid-template-columns: minmax(0, 1fr); }
  .quote-layout > .quote-aside, .quote-layout > :not(.quote-writing) { order: 2; }
}

/* ===================================================================
   ISSUED INVOICE — state, one correction control, lines that stack
   =================================================================== */

/*  THE STATE, BESIDE THE REFERENCE. Derived in invoices.py; see _doc_state.
    NEUTRAL BY DEFAULT AND AMBER FOR ONE CASE. Red is overdue and this screen
    knows nothing about due dates — an unpaid invoice is not late, it is
    unpaid. "Corrected" takes amber because it changes what the document
    means and is the one a person must not miss. */
.doc-state {
  display: inline-block; vertical-align: middle;
  margin-inline-start: 10px; padding: 4px 10px;
  border: 1px solid var(--border-strong); border-radius: 999px;
  background: var(--surface-sunken);
  font-size: 12px; font-weight: 600; letter-spacing: .02em;
  color: var(--text-secondary);
}
.doc-state--corrected { color: var(--lane-amber); border-color: var(--lane-amber); }

/*  ONE SECONDARY CONTROL FOR BOTH CORRECTIONS, on the browser's own
    disclosure. Three equally weighted header actions made correcting an
    immutable tax document look routine. */
.correct-menu { position: relative; }
.correct-menu__summary { cursor: pointer; list-style: none; }
.correct-menu__summary::-webkit-details-marker { display: none; }
.correct-menu__body {
  position: absolute; inset-inline-end: 0; z-index: 20;
  margin-block-start: 8px; padding: 14px; inline-size: 260px;
  display: flex; flex-direction: column; gap: 9px; align-items: stretch;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-raised); box-shadow: var(--shadow-pop);
}
/*  At phone width there is nothing to float over, so it becomes a block —
    an absolutely positioned 260px panel inside a 358px column is how a
    layout acquires a horizontal overflow. */
@media (max-width: 699px) {
  .correct-menu__body { position: static; inline-size: auto; }
}

/*  A SETTLED DOCUMENT OFFERS NO RECEIPT FORM (live Round 2, 29 Sep 2026) —
    the collapsed "Record another receipt" and its quiet variant are gone
    with it. The over-payment refusal is the backstop. */

/*  ── THE DOCUMENT'S LINES STACK BELOW 700px ────────────────────────────
 *
 *  Four columns in 358px gave Description ~120px and squeezed Qty, Unit and
 *  Amount into what was left, so a description wrapped to four lines beside
 *  three cramped figures. The description takes its own line; the three
 *  numbers form one compact row beneath it, each under its own name.
 */
@media (max-width: 699px) {
  .doc-lines, .doc-lines tbody, .doc-lines tr { display: block; inline-size: 100%; }
  .doc-lines thead { display: none; }
  .doc-lines { overflow-x: visible; }
  .doc-lines tbody tr {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 6px 10px;
    padding: 14px var(--figure-edge);
    border-block-end: 1px solid var(--border);
  }
  .doc-lines tbody tr:last-child { border-block-end: 0; }
  .doc-lines tbody td,
  .doc-lines tbody td:last-child { display: block; border: 0; padding: 0; block-size: auto; }
  .doc-lines__desc { grid-column: 1 / -1; font-size: 13.5px; line-height: 1.45; }
  .doc-lines__qty, .doc-lines__unit, .doc-lines__amount {
    text-align: start; font-size: 13px;
  }
  .doc-lines__qty::before, .doc-lines__unit::before, .doc-lines__amount::before {
    content: attr(data-label);
    display: block; font-family: var(--font);
    font-size: 10.5px; font-weight: 600; letter-spacing: .05em;
    text-transform: uppercase; color: var(--text-muted);
  }
  .doc-lines__amount { text-align: end; }
}

/*  THE TIMELINE STACKS BELOW THE WORK, BEFORE EITHER IS CRAMPED.

    `.page-columns` is `minmax(0, 1fr) 330px`, released only at 900. Between
    900 and ~1200 the 330px rail is fixed and the working column is what gives
    way: at a 924px viewport, inside the 232px sidebar shell, inquiry detail's
    main column came out at 256px — which is where the artwork judge band
    collapsed to four 44px squares. The timeline is HISTORY; the panels beside
    it are the current task, and history must not squeeze the task.

    1200 rather than a container query because `.page-columns` IS the
    container here — the thing being asked about is the width it gets, and
    that comes from the viewport less the shell. */
@media (max-width: 1199px) {
  /*  1199 IS LOAD-BEARING FOR SOMETHING THREE SECTIONS AWAY. The proof
      response fields have no width of their own — they take what this
      column leaves. Measured at a 924 viewport on the one inquiry with a
      proof awaiting an answer:

          released here at 1199   .version-row 574px   both placeholders fit
          released at       899   .version-row 224px   BOTH CUT

      "How — WhatsApp, in person" needs 177px in 164px, and "What has to
      change (optional)" needs 188px in 164px. Lower this and the clipping
      returns silently, in a band nobody opens, on a control that only
      renders while a proof is unanswered.
      test_the_timeline_releases_early_enough_for_the_proof_controls. */
  .page-columns { grid-template-columns: minmax(0, 1fr); }
}

/*  AN EMPTY SECTION SAYS WHAT WOULD BE THERE, IN ONE LINE. It was a card
    reading "Not built yet" — a statement about the system to a person asking
    about the job. */
.panel-empty-line { padding-block: 12px; }
.panel-empty-line .hint { margin: 0; }

/* ===================================================================
   ARTWORK — the decision first, the history after, the form on request
   =================================================================== */

/*  TWO ANSWERS, ONE OPEN AT A TIME. Both branches rendered permanently —
    five controls for a question with two answers — and the refusal's
    machinery was on screen before anybody had said there was a problem. */
.judge { display: flex; flex-direction: column; gap: 10px; margin-block-start: 10px; }
.judge__yes { display: flex; }
.judge__no {
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-sunken);
}
.judge__no-summary {
  padding: 11px 14px; cursor: pointer;
  min-block-size: 44px; display: flex; align-items: center;
  font-size: 13px; font-weight: 600; color: var(--text-secondary);
}
.judge__no-form { padding: 0 14px 14px; }

/*  ADDING A FILE IS NOT READING THE HISTORY. Collapsed unless there is no
    history to read. */
.add-file { border-block-start: 1px solid var(--border); }
.add-file__summary {
  padding: 14px 18px; cursor: pointer;
  min-block-size: 44px; display: flex; align-items: center;
  font-size: 13px; font-weight: 600; color: var(--text-secondary);
}
.add-file[open] .add-file__summary { border-block-end: 1px solid var(--border); }

/*  A COLLAPSED SECTION SAYS IT OPENS (live Test 2, 28 Sep 2026). "Request a
    new file" and "Add newer artwork" read as plain headings: the browser's
    triangle had been styled away with nothing in its place. One mark for
    every <summary> on the page — a chevron that points right when closed and
    down when open. Drawn with borders in currentColor, so it follows the
    summary's own colour in both themes and needs no icon file. It turns
    instantly — no motion, as everywhere in this sheet. */
.disclosure { list-style: none; cursor: pointer; gap: 8px; }
.disclosure::-webkit-details-marker { display: none; }
.disclosure::before {
  content: ""; flex: none; display: inline-block;
  inline-size: 7px; block-size: 7px; margin-inline-end: 2px;
  border-inline-end: 2px solid currentColor; border-block-end: 2px solid currentColor;
  transform: rotate(-45deg);
}
[open] > .disclosure::before { transform: rotate(45deg) translate(-2px, -1px); }
.disclosure:hover { color: var(--text); }

/*  A CONFIRM TICK UNDER A WARNING (live Test 2, 28 Sep 2026): a shared
    WhatsApp number, a promised date in the past. The warning says what is
    true; the tick is a person saying they meant it. 44px tall, so it can be
    hit with a thumb. */
.check-row { display: flex; align-items: center; gap: 10px; min-block-size: 44px;
  font-size: var(--fs-secondary); color: var(--text); cursor: pointer; }
.check-row input { inline-size: 18px; block-size: 18px; flex: none; accent-color: var(--accent); }

/*  EVERY CUSTOMER ON THE INQUIRY'S NUMBER, each beside the inquiry's own
    contact name (live Test 2) — one row per match, divided so two matches
    read as two choices. */
.match-list { display: flex; flex-direction: column; gap: 12px; }
.match-row + .match-row { border-block-start: 1px solid var(--border); padding-block-start: 12px; }

/*  WHAT THE FILE IS — a thumbnail and its facts, no verdict (live Test 2). */
.file-facts { display: flex; align-items: center; gap: 12px; margin-block: 8px; min-inline-size: 0; }
.file-thumb {
  flex: none; inline-size: 56px; block-size: 56px; object-fit: contain;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-sunken);
}
.file-facts__text { margin: 0; font-size: var(--fs-secondary); color: var(--text-secondary);
  overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }

/*  ONE ROW FOR A FILE, ARTWORK OR PROOF (phone test, 4 Oct 2026). On a
    phone "Open the file" floated halfway down beside the buttons — it shared
    a wrapping controls box with the facts and the forms. A single column in
    one order at every width: date and name, state, judged, thumbnail and
    facts, "Open the file" on its own line, then the actions — stacked full
    width on a phone, side by side from 700. */
.file-list { display: flex; flex-direction: column; }
.file-row {
  display: flex; flex-direction: column; gap: 6px; min-inline-size: 0;
  padding: 14px 16px; border-block-end: 1px solid var(--border);
}
.file-row:last-child { border-block-end: 0; }
.file-row__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; min-inline-size: 0; }
.file-row__when { font-size: var(--fs-secondary); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.file-row__name { margin: 0; font-size: 15px; font-weight: 600; color: var(--text); overflow-wrap: anywhere; }
.file-row__state { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
.file-row__state-text { font-size: var(--fs-secondary); color: var(--text-secondary); }
.file-row__judged, .file-row__meta { margin: 0; font-size: var(--fs-secondary); color: var(--text-secondary); }
.file-row .file-facts { margin-block: 4px; }
/*  A PROOF THAT IS NOT THE NEWEST reads a step down, as the version rows do. */
.file-row--past .file-row__name { color: var(--text-secondary); }
/*  THE LINK IS A LINK: the prose-link colour from :where(.app a), an icon,
    and its own line — 44px tall so a thumb finds it, and only as wide as its
    words so it never reads as one more button. */
.file-row__open { margin: 0; }
.file-open {
  display: inline-flex; align-items: center; gap: 8px; min-block-size: 44px;
  font-size: var(--fs-secondary); font-weight: 600; text-decoration: none;
}
.file-open:hover span { text-decoration: underline; text-underline-offset: 3px; }
.file-open svg { flex: none; inline-size: 16px; block-size: 16px; }
.file-row__actions {
  display: flex; flex-direction: column; align-items: stretch; gap: 10px;
  min-inline-size: 0; margin-block-start: 4px;
}
.file-row__actions :is(select, button, input, a, summary) {
  box-sizing: border-box; min-block-size: 44px; max-inline-size: 100%;
}
/*  EVERY BUTTON IN THE ACTIONS, full width on a phone — the answer forms'
    "Record the approval" sits two levels down and was the one left short. */
.file-row__actions .btn { inline-size: 100%; }
.file-row__actions .judge { margin-block-start: 0; }
@media (min-width: 700px) {
  .file-row__actions { flex-direction: row; flex-wrap: wrap; align-items: flex-start; }
  .file-row__actions .btn { inline-size: auto; }
  .file-row__actions .judge { flex-direction: row; flex-wrap: wrap; align-items: flex-start; flex: 1 1 100%; }
  .file-row__actions .judge__no { flex: 1 1 260px; }
  /*  The answer set is two labelled forms; it takes the full row beneath the
      send, so its fields keep the room the 1199 note above measured for. */
  .file-row__actions .answer-set { flex: 1 1 100%; }
}

/*  WHERE THE JOB STANDS, AT THE TOP OF A WON INQUIRY. The full panel stays
    below Artwork and Proofs, which is right for its detail; this answers the
    question a person actually arrives with. Not a panel — a strip, so it
    does not compete with the sections it sits above. */
.job-strip {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px 18px;
  padding: 13px 18px; margin-block-end: 2px;
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface-raised);
}
.job-strip__main { display: flex; align-items: baseline; gap: 10px; min-inline-size: 0; }
.job-strip__label {
  margin: 0; font-size: 10.5px; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--text-muted);
}
.job-strip__ref { margin: 0; font-size: 15px; font-weight: 600; }
.job-strip__facts {
  display: flex; align-items: baseline; gap: 14px; flex: 1 1 auto;
  font-size: 13px; color: var(--text-secondary);
}
.job-strip__state { color: var(--text); font-weight: 500; }
.job-strip .btn { flex: 0 0 auto; }

/* ===================================================================
   THE BOARD ON A PHONE — one lane at a time, and it snaps
   =================================================================== */

/*  TWO NARROW LANES AT 390 IS NEITHER. `.pipeline` is
    `grid-auto-columns: minmax(180px, 1fr)` with `overflow-x: auto`, so a
    358px viewport showed about two 180px lanes — each too narrow for the
    card inside it, and the second one cut. Every control on a board card
    then had ~150px to live in.

    ONE LANE PER VIEWPORT, WITH A GLIMPSE. 84% leaves ~57px of the next lane
    visible at 390, which is what says "this scrolls" without a second lane
    pretending to be usable. Scroll-snap makes each swipe land on a lane
    rather than between two.

    THE BOARD IS THE ONLY THING THAT SCROLLS SIDEWAYS. `.pipeline` already
    carries `overflow-x: auto`, so the horizontal scroll is the component's
    and the page body's is untouched — which the overflow assertion at 390
    and 924 checks.

    NOT BELOW 900. The shell breakpoint is about the sidebar; this is about
    whether a lane can hold a card, and a lane is too narrow well before the
    sidebar goes. 700 is where the record grid already drops to one column. */
@media (max-width: 699px) {
  .pipeline {
    grid-auto-columns: 84%;
    gap: 16px;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: 0;
    /*  NO scroll-behavior HERE. It was `smooth` until 26 Sep 2026, which
        animated every lane change: motion, and the no-motion guard in
        test_dashboard.py could not see it. It can now. */
  }
  .pipeline-column { scroll-snap-align: start; scroll-snap-stop: always; }
  /*  A lane's own controls get the full lane. They already stack — see the
      control-band note above — and at 84% of 358 that is ~300px, which is
      what a full-width "Won — open the job" needs. */
  .pipeline-body { padding: 12px; gap: 12px; }
}

/*  SYSTEM — THE EXPLANATION BELONGS WITH THE THING IT EXPLAINS.

    `.section-head` is `justify-content: space-between`, which is right for a
    list screen where the note is a one-line aside. On System it put "The job
    runs weekly and writes a row when a dump is confirmed in the bucket" at
    the far right of a 2200px page, a screen-width away from the status it
    describes — two fragments of one sentence with the whole viewport between
    them.

    Stacked, and the pair boxed with its status. The page holds two checks and
    nothing else, so it stays compact rather than spreading to fill the width
    it happens to have. */
.system-check {
  gap: 10px;
  padding: 16px 18px;
  max-inline-size: 760px;
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface-raised);
}
.system-check__head {
  display: block;
}
.system-check__head .section-note { margin-block-start: 4px; max-inline-size: 60ch; }
.system-check .notice { margin: 0; }

/*  ── WIDTH CAPS: MORE VIEWPORT IS NOT MORE ROOM TO FILL ────────────────

    AN OPERATIONAL ROW HAS A USEFUL WIDTH AND A SILLY ONE. `.make-record-list`
    measured 1884px at 2200 — a job row whose four tracks are 104 + 1fr + 168
    + 140, so the 1fr took 1,472px to hold a reference and a customer name.
    Sixteen of those is a page of near-empty slabs.

    1180 IS CHOSEN, NOT ROUNDED. At 1400 the list already resolves to 1084px
    and reads well, so the cap must be above that or it would change a width
    nobody complained about; 1180 leaves the name track ~768px, which is far
    past what a reference plus a customer name needs and still stops the
    slab. Raise it only if a real name is seen wrapping. */
.make-record-page .make-record-list { max-inline-size: 1180px; }

/*  A FORM FIELD HAS A READABLE WIDTH AND IT IS NOT THE VIEWPORT.
    `.form-card` already caps at 760px, but the credit/debit note builds its
    fields straight into a panel body, so nothing held them: measured at 2200,
    the amount input was 918px and the reason textarea 1850px. A single line
    of typed text in an 1,850px box is a field you lose your place in.

    Applied to the panel body that holds fields rather than to `.input`, so
    the cap is on the CONTENT COLUMN — a field and its hint stay aligned
    instead of each being clipped to its own width. */
.make-record-page .panel-body > .form-grid,
.make-record-page .panel-body > .field { max-inline-size: 760px; }

/* ===================================================================
   STOCK HEALTH — a categorical strip, never a percentage
   =================================================================== */

/*  NOT A PROGRESS BAR, AND THE REASON IS DATA rather than taste. The system
    records a reorder level and no maximum capacity, so "60% full" would need
    a scale nobody has ever written down — it would invent the exact kind of
    figure this screen exists to refuse.

    THREE SEGMENTS, ONE LIT. Out / Low / In stock. The two configuration
    states — Count needed, Reorder level needed — light none of them and take
    the dashed track instead, because a material nobody has counted has no
    health to show and must never render as healthy.

    COLOUR NEVER CARRIES THE MEANING ALONE. Every card prints the written
    status under the strip, and the strip carries the same words as an
    aria-label. A person who cannot tell amber from ivory loses nothing. */
.stock-figure { margin: 0; font-size: 15px; font-weight: 600; color: var(--text); }
.stock-reorder { margin: 3px 0 0; font-size: 12.5px; color: var(--text-secondary); }
.stock-machine { margin: 8px 0 0; font-size: 12.5px; color: var(--text-muted); }

.stock-strip {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 3px; margin-block-start: 12px;
}
.stock-strip__seg {
  block-size: 6px; border-radius: 3px;
  /*  The unlit state is the track, not an absence — a segment that vanished
      would make the strip change width with the status. */
  background: var(--surface-sunken);
  border: 1px solid var(--border);
}
/*  UNKNOWN IS DASHED AND EMPTY. No segment is lit, and the dashes say the
    track itself is not a measurement. */
.stock-strip--unknown .stock-strip__seg {
  background: none; border-style: dashed;
}

/*  OUT IS THE STRONGER AMBER AND LOW THE ORDINARY ONE. Both are amber
    because both are "attention" in this palette; red is reserved for overdue
    work and green is not in the system at all. In stock is quiet ivory —
    the text colour — because "fine" should not compete with "act on this". */
.stock-strip__seg--out.is-on {
  background: var(--lane-amber); border-color: var(--lane-amber);
}
.stock-strip__seg--low.is-on {
  background: var(--lane-amber-surface); border-color: var(--lane-amber);
}
/*  THE HEALTHY SEGMENT IS GREEN SINCE 21 Sep 2026. It was --text-muted,
    which said "this is the third segment" and nothing about health. Only the
    LIT segment takes it: an unlit track stays neutral, and a material nobody
    has counted lights none of them at all. */
.stock-strip__seg--in.is-on {
  background: var(--state-good); border-color: var(--state-good);
}

.stock-state {
  margin: 6px 0 0; font-size: 12px; font-weight: 600;
  letter-spacing: .03em; color: var(--text-secondary);
}

/* ===================================================================
   INQUIRIES — the scope switch, and stage as metadata
   =================================================================== */

/*  OPEN / WON / LOST / ALL, directly under the title. Not chips and not
    the accent: this is navigation between views of the same records, and "New
    inquiry" stays the one primary action on the screen. The current item is
    filled the way an active chip is, so the switch reads as one control with
    a selected member rather than four links. */
.scope-switch {
  /*  ONE LINE, ALWAYS. It wrapped inside a 999px-radius pill at 390, leaving
      "All 44" alone on a second line inside a giant capsule. It scrolls
      inside itself if it ever cannot fit, and at phone width the items
      tighten so four fit in 358px. */
  display: flex; flex-wrap: nowrap; gap: 6px;
  overflow-x: auto; scrollbar-width: none;
  margin-block-end: 14px;
  padding: 4px; inline-size: fit-content; max-inline-size: 100%;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface-sunken);
}
.scope-switch__item {
  display: inline-flex; align-items: center; gap: 7px;
  min-block-size: 44px; padding: 0 16px;
  border-radius: 999px;
  font-size: 13.5px; font-weight: 600;
  color: var(--text-secondary); text-decoration: none;
}
.scope-switch__item.is-current { background: var(--text); color: var(--bg); }
.scope-switch__count { font-variant-numeric: tabular-nums; opacity: .75; }

/*  THE SECONDARY FILTER ROW IS QUIETER THAN THE SWITCH ABOVE IT. "Waiting a
    day 0" is a filter that can produce one answer, and it was sitting at the
    same weight as Won and Lost. It stays — a chip reading 0 is how you know
    the filter is not hiding anything — at the size it deserves. */
.make-toolbar--secondary { margin-block-start: -4px; }
.make-toolbar--secondary .chip-sm {
  min-block-size: 44px; padding: 0 13px; font-size: 12.5px;
}

/*  ── STAGE AS METADATA: A 3px RAIL, NOT A TINT ─────────────────────────
 *
 *  Six stages, six muted edges. The card surface stays warm neutral — tinting
 *  whole cards produces a rainbow board where everything shouts and nothing
 *  reads. The written stage is always present, on the lane heading and in the
 *  card's own text, so the colour repeats information and never carries it.
 *
 *  WON IS IVORY AND LOST IS GREY, deliberately not green and red. Green is
 *  not in this palette at all, and red means one thing here — overdue work.
 *  An inquiry the studio lost is not an error state.
 */
.make-record-card--railed { position: relative; overflow: hidden; }
.make-record-card--railed::before {
  content: ""; position: absolute; inset-block: 0; inset-inline-start: 0;
  inline-size: 3px; background: var(--stage-rail, transparent);
}
.stage-dot {
  display: inline-block; inline-size: 8px; block-size: 8px;
  border-radius: 2px; margin-inline-end: 8px;
  background: var(--stage-rail, var(--border-strong));
  vertical-align: middle;
}

/*  The six values, set once and read by both the rail and the dot. */
.stage-rail--new_inquiry   { --stage-rail: #6b7a8f; }   /* muted slate    */
.stage-rail--need_info     { --stage-rail: #8a6d3f; }   /* muted amber    */
.stage-rail--ready_to_quote{ --stage-rail: #7d6a9c; }   /* muted lavender */
.stage-rail--quote_sent    { --stage-rail: #5a7490; }   /* muted blue     */
.stage-rail--won           { --stage-rail: #c9b68a; }   /* warm ivory     */
.stage-rail--lost          { --stage-rail: #5c5651; }   /* dim grey       */

/*  ── WHY WAITING-FOR-CUSTOMER'S RAIL CANNOT BE MISTAKEN FOR A CHASE ────
 *
 *  Amber is now both a stage and a warning: `need_info` takes a muted amber
 *  rail, and the same card can carry an amber "Chase 3 Sep" badge. The badge
 *  has to stay the stronger of the two, and "muted is surely enough" is the
 *  kind of assumption this project keeps writing down after it turns out not
 *  to be. So it was measured rather than assumed:
 *
 *      badge  --lane-amber           #f2ae35   L* 75.7   contrast 8.50:1
 *      rail   .stage-rail--need_info  #8a6d3f   L* 47.9   contrast 3.39:1
 *
 *  Both against --surface #211f1e, the card's own ground. The rail is 27.8
 *  L* points darker and carries 2.5x less contrast — it is not a slightly
 *  quieter amber, it is half the lightness.
 *
 *  THE PART THAT ACTUALLY SETTLES IT is not the colour at all: the badge
 *  carries WORDS and the rail carries none. One says "Chase 3 Sep" in 12px
 *  bold on a filled pill; the other is a 3px edge with no text. A person
 *  reading the card gets the chase from the words whether or not they can
 *  tell the two ambers apart.
 *
 *  If the rail is ever lightened past roughly L* 60 it starts competing, and
 *  test_the_stage_rail_stays_quieter_than_the_chase_badge holds the pair.
 */
.outcome .section-title { display: flex; align-items: center; }

/* ===================================================================
   THE STATUS PILL — one component, five variants, never a control
   =================================================================== */

/*  NON-INTERACTIVE BY CONSTRUCTION. No hover, no cursor, no shadow, no
    transition: a status a person can hover is a status they will try to
    click. The controls that CHANGE a state live elsewhere and look like
    controls — this only says what a record is.

    999px, a dot, then words. The dot is aria-hidden because the words are
    already there; a screen reader reading "bullet In stock" would be the
    decoration claiming to be information. */
.pill-status {
  display: inline-flex; align-items: center; gap: 7px;
  min-block-size: 28px; padding: 0 12px;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface-sunken);
  /*  ITS OWN FACE, NOT THE CELL'S. Money's balance column is .cell-mono, so
      an inherited family rendered "In credit" and "Paid off" in the code face
      on that one screen while every other pill in the system used General
      Sans. A component that changes typeface depending on where it lands is
      not one component — and the tabular figures the mono face exists for are
      about a COLUMN of numbers, which a label is not part of. */
  font-family: var(--font);
  font-size: 12.5px; font-weight: 600; line-height: 1.2;
  font-variant-numeric: normal;
  color: var(--text-secondary);
  /*  NEVER TRUNCATED. A state that reads "Reorder level need…" is a state
      nobody can act on, and the wording is the accessible half of every
      indicator here. It wraps the layout before it clips the word. */
  white-space: nowrap;
}
.pill-status__dot {
  inline-size: 7px; block-size: 7px; border-radius: 999px;
  background: currentColor; flex: none;
}

/*  THE LARGER VARIANT, for a figure that is the point of its panel — Home's
    "2 in production". Same component, same rules. */
.pill-status--metric {
  min-block-size: 40px; padding: 0 18px; gap: 10px;
  font-size: 16px; font-weight: 600;
}
.pill-status--metric .pill-status__dot { inline-size: 9px; block-size: 9px; }

/*  ══ THE FIVE VARIANTS, DEFINED ONCE ═══════════════════════════════════
    Each variant sets three custom properties and nothing else. The pill reads
    them below; the metric panel reads the same ones further down. That is the
    whole reason they exist: a variant's colour is one decision, in one place,
    and a card cannot disagree with the pill sitting inside it.

    --v-mark     the marker and any hued ink
    --v-surface  the tint behind it
    --v-line     the border

    EVERY READ OF --v-mark CARRIES A NEUTRAL FALLBACK, 23 Sep 2026. It is
    set only by a variant class, so an element that reads it without one — a
    template that forgot the modifier, a status_variant() that returned a
    word no rule matches — got `background: ` with nothing after it: no dot,
    no ring, no wash, and no error anywhere. The fallback is the NEUTRAL
    variant's own ink, so a missing variant renders as "no claim" rather than
    as nothing. tests/ refuses a bare read.

    INFORMATIONAL GAINED A HUE ON 22 Sep 2026 and this is a deliberate
    change. It was --border-strong on --surface-raised, which is grey — and
    grey is what NEUTRAL means here, so the two states that had to be
    distinguishable ("Sent", a real workflow position, against "nothing
    recorded") were separated only by a border weight. --state-held already
    existed for exactly this and was unused since the lanes were retired. */
.pill-status--positive,      .metric-panel--positive      { --v-mark: var(--state-good);
  --v-surface: var(--state-good-surface); --v-line: var(--state-good-border); }
.pill-status--attention,     .metric-panel--attention     { --v-mark: var(--lane-amber);
  --v-surface: var(--lane-amber-surface); --v-line: var(--lane-amber); }
/*  RED, AND NOTHING ELSE IN THIS SYSTEM IS RED. Overdue work only. */
.pill-status--overdue,       .metric-panel--overdue       { --v-mark: var(--lane-red);
  --v-surface: var(--lane-red-surface); --v-line: var(--lane-red); }
.pill-status--informational, .metric-panel--informational { --v-mark: var(--state-held);
  --v-surface: var(--state-held-surface); --v-line: var(--state-held); }
.pill-status--neutral,       .metric-panel--neutral       { --v-mark: var(--text-muted);
  --v-surface: var(--surface-sunken); --v-line: var(--border); }

.pill-status--positive {
  background: var(--v-surface); border-color: var(--v-line);
  color: var(--state-good-text);
}
.pill-status--positive .pill-status__dot { background: var(--v-mark, var(--text-muted)); }

.pill-status--attention {
  background: var(--v-surface); border-color: var(--v-line); color: var(--v-mark, var(--text-muted));
}

.pill-status--overdue {
  background: var(--v-surface); border-color: var(--v-line); color: var(--v-mark, var(--text-muted));
}

/*  INFORMATIONAL — a workflow state that is neither good nor bad yet.
    Quieter than attention and distinct from neutral, so "Sent" does not read
    as "nothing recorded". */
.pill-status--informational {
  background: var(--v-surface); border-color: var(--v-line); color: var(--state-held-text);
}

/*  NEUTRAL is the default and the fallback: unknown, inactive, cancelled,
    lost, not recorded. It inherits the base rule and is named so a template
    can ask for it rather than omitting the variant and hoping. */
.pill-status--neutral { }


/* ══════════════════════════════════════════════════════════════════════
   THE METRIC PANEL — one number that carries the screen.

   NOT .metric-card, WHICH ALREADY EXISTS AND IS A ROW. This is the whole
   surface: a marker, a label, one large figure, one sentence, and optional
   content after a divider. It is the "Jobs currently in production" shape,
   generalised — that card was built by hand on Home and the brief asks for it
   as a component so the next one is not built by hand too.

   THE NUMBER IS THE POINT. --fs-display, near-white, tight, and visibly
   larger than everything around it. The label is small, the sentence quieter
   than both. A metric whose number does not win in under a second is a card
   that should not have been promoted.

   WHAT THE VARIANT CONTROLS: the marker, a faint border tint and a restrained
   wash. Nothing else. The number stays --text in every variant, because a
   green number reads as a different KIND of number rather than as a good one,
   and because the colour then carries meaning on its own — which §7 forbids
   and the marker plus the words already handle.

   THE WASH IS DECORATION AND IS TREATED AS SUCH. It is a ::before, it is
   aria-hidden by construction, and it has pointer-events: none so it cannot
   take a click or a focus ring. It is sized in PERCENTAGES so it scales with
   the card instead of becoming a fixed patch in a corner at 2200. */
.metric-panel {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: 20px 22px;
  border-radius: var(--radius-lg);
  background: var(--surface-raised);
  /*  A FAINT TINTED EDGE, not a coloured outline. colour-mix keeps it derived
      from the variant rather than a sixth set of hand-picked hex values. */
  border: 1px solid color-mix(in srgb, var(--v-line) 22%, var(--card-border));
}
/*  THE WASH. Two stops, from the upper left, gone by about two thirds across
    — so the number sits ON the tint and the content below it sits on the
    ordinary dark surface. 10% is deliberately low: the card must still read
    as the same dark card as everything beside it. */
.metric-panel::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(118% 92% at 0% 0%,
      color-mix(in srgb, var(--v-mark, var(--text-muted)) 10%, transparent) 0%,
      color-mix(in srgb, var(--v-mark, var(--text-muted)) 4%, transparent) 38%,
      transparent 68%);
}
/*  NEUTRAL GETS NO WASH AT ALL. A grey gradient on a grey card is the
    decorative case §10 asks to be removed: it adds no information and makes
    the card look like it is trying to say something. */
.metric-panel--neutral::before { background: none; }

.metric-panel__head { display: flex; align-items: center; gap: 9px; margin: 0 0 2px; }
.metric-panel__mark {
  inline-size: 7px; block-size: 7px; border-radius: 999px;
  background: var(--v-mark, var(--text-muted)); flex: none;
}
.metric-panel__label { margin: 0; font-size: var(--fs-secondary); color: var(--text-muted); }
.metric-panel__value {
  display: block;
  font-family: var(--font);
  font-size: var(--fs-display);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -.03em;
  /*  NEAR-WHITE IN EVERY VARIANT. See the note above. */
  color: var(--text);
  margin: 2px 0 0;
}
/*  A UNIT IS NOT THE FIGURE. "Rf" beside 9,684.59 at 52px would read as part
    of the number; it takes the label's size and sits on the baseline. */
.metric-panel__unit {
  font-size: var(--fs-section); font-weight: 500;
  color: var(--text-muted); margin-inline-end: 6px; letter-spacing: 0;
}
/*  A TRAILING UNIT SITS ON THE OTHER SIDE. "9 rolls", not "rolls 9". */
.metric-panel__unit--after { margin-inline-end: 0; margin-inline-start: 7px; }
.metric-panel__note {
  display: flex; align-items: flex-start; gap: 7px;
  margin: 9px 0 0; font-size: var(--fs-meta); color: var(--text-muted);
  max-width: 52ch;
}
.metric-panel__note-icon {
  inline-size: 13px; block-size: 13px; border-radius: 999px; flex: none;
  margin-block-start: 1px;
  border: 1px solid var(--border-strong); color: var(--text-muted);
  font-size: 9px; font-weight: 700; line-height: 11px; text-align: center;
}
.metric-panel__rule { height: 1px; margin: 18px 0 10px; background: var(--border-strong); border: 0; }

/* --- the studio assistant (7 Oct 2026, ASSISTANT.md) ------------------
   The panel is a conversation: the person's turns right, the assistant's
   left, both on the page's own surfaces. No accent — the accent is the one
   Send button. */
.assistant { display: flex; flex-direction: column; gap: 14px; }
.assistant-log {
  display: flex; flex-direction: column; gap: 10px;
  max-height: 62vh; overflow-y: auto; padding: 4px 0;
}
.assistant-empty { color: var(--text-muted); margin: 0; }
.assistant-msg {
  max-width: 44rem; padding: 10px 14px; border-radius: var(--radius-lg);
  border: 1px solid var(--border); background: var(--surface);
}
.assistant-msg--user { align-self: flex-end; background: var(--surface-sunken); }
.assistant-msg--assistant { align-self: flex-start; }
.assistant-who { margin: 0 0 4px; font-size: var(--fs-meta); color: var(--text-muted); }
.assistant-text-block { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.assistant-text-block + .assistant-text-block { margin-top: 6px; }
.assistant-attachment { margin: 6px 0 0; font-size: var(--fs-meta); color: var(--text-muted); }
.assistant-wait { margin: 0; color: var(--text-muted); font-size: var(--fs-secondary); }
.assistant-form { display: flex; flex-direction: column; gap: 8px; }
.assistant-text { width: 100%; resize: vertical; min-height: 84px; }
.assistant-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.assistant-off { margin: 0 0 12px; }
.assistant-card {
  align-self: stretch; padding: 12px 14px; border-radius: var(--radius-lg);
  border: 1px solid var(--border-strong); background: var(--surface-raised);
}
.assistant-card__title { margin: 0 0 6px; font-weight: 600; }
.assistant-changes { margin: 0 0 10px; padding-left: 18px; }
.assistant-change__what { color: var(--text-secondary); }
.assistant-change__before { text-decoration: line-through; color: var(--text-muted); }
.assistant-change__after { font-weight: 600; }
/* A NEW RECORD (14 Oct 2026): plain labelled lines, a gap between groups. */
.assistant-card__when { font-weight: 400; color: var(--text-muted); }
.assistant-changes--new { list-style: none; padding-left: 0; }
.assistant-alerts { list-style: none; margin: 0 0 16px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.assistant-alert { display: block; min-block-size: 44px; padding: 10px 14px; border: 1px solid var(--border);
                   border-radius: var(--radius); background: var(--surface); color: var(--text); text-decoration: none; }
a.assistant-alert:hover { background: var(--surface-hover); }
.assistant-alert__summary { display: block; }
.assistant-alert__undelivered { display: block; margin-top: 4px; font-size: var(--fs-meta); color: var(--lane-amber); }
.assistant-alert--new { border-color: var(--border-strong); background: var(--surface-raised); }
.assistant-alert__kind { display: block; font-size: var(--fs-meta); color: var(--text-muted); margin-bottom: 2px; }
.assistant-alert__when { display: block; font-size: var(--fs-meta); color: var(--text-muted); margin-top: 2px; }
.assistant-alert-prefs { margin-top: 12px; }
.staff-whatsapp { display: flex; gap: 8px; align-items: center; flex-wrap: nowrap;
                  flex-basis: 100%; }
.staff-whatsapp .input { width: 12rem; max-width: 100%; min-width: 0; }
.staff-whatsapp .btn { flex: none; }
/*  WHO PRICES, WHO HANDLES ORDERS (096): two ticks under the number. The
    44px target is the label row (.check-row), so the box itself keeps the
    page's tick size — the card's 44px control rule is for buttons and selects. */
.staff-orders { flex: 1 1 100%; display: flex; flex-direction: column; align-items: flex-start; gap: 0; }
.make-record-page .make-record-card__controls .staff-orders .check-row input {
  min-block-size: 0; min-inline-size: 0; }

/*  AN INQUIRY'S ITEMS (migration 075, 11 Oct 2026): what it asks for, one
    row per thing, on the inquiry page and the board. Rows divided by a
    rule, not boxed — they are a list inside one panel. What an item lacks
    is said in the state colour for "waiting", never red: a missing size is
    a question to ask, not an error. Every control at least 44px tall. */
.inquiry-items { display: flex; flex-direction: column; gap: 14px; }
.inquiry-items__notice { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }
.inquiry-items__notice p { margin: 0; flex: 1 1 24ch; }
.inquiry-items__locked, .inquiry-items__before { font-size: var(--fs-secondary); color: var(--text-secondary); margin: 0; }
.inquiry-item-list { list-style: none; margin: 0; padding: 0; }
.inquiry-item { padding: 14px 0; border-bottom: 1px solid var(--border); }
.inquiry-item:first-child { padding-top: 0; }
.inquiry-item__title { font-size: 14px; font-weight: 600; margin: 0 0 2px; }
.inquiry-item__facts { font-size: 13px; color: var(--text-secondary); margin: 0; }
.inquiry-item__spec { font-size: 13px; margin: 6px 0 0; white-space: pre-line; }
.inquiry-item__notes { font-size: 12.5px; color: var(--text-muted); margin: 4px 0 0; }
.inquiry-item__missing { font-size: 12.5px; font-weight: 600; color: var(--state-chasing); margin: 6px 0 0; }
.inquiry-item__edit { margin-top: 6px; }
.inquiry-item__edit > .disclosure { display: inline-flex; align-items: center; min-block-size: 44px;
  font-size: 13px; color: var(--text-secondary); }
.inquiry-item__form { display: flex; flex-direction: column; gap: 10px; padding: 8px 0; }
.inquiry-item__remove { display: flex; justify-content: flex-end; }
.inquiry-item__add > .disclosure { font-weight: 600; color: var(--text); }
.inquiry-needed-by { display: flex; flex-direction: column; gap: 6px; padding-top: 8px;
  border-top: 1px solid var(--border); }
.inquiry-needed-by__row { display: flex; gap: 10px; align-items: center; }
.inquiry-needed-by__row .input { max-inline-size: 220px; }
.inquiry-needed-by__passed { font-size: 12.5px; font-weight: 600; color: var(--state-chasing); margin: 0; }
.inquiry-card-items { margin: 0; }


/*  A MIGRATION'S FILE NAME WRAPS (11 Oct 2026): "076_the_single_product_
    columns_are_gone.sql" is one word to a browser, and while it was pending
    it pushed System 157px past a phone's edge. It may break anywhere. */
.migration-name { overflow-wrap: anywhere; }

/*  A LIST CUT AT ITS CAP SAYS SO (12 Oct 2026): "Showing the 20 most
    recent of 34", under the list, with the way to the rest. Secondary
    text, never a warning — the list is fine, it is just longer. */
.list-count { font-size: var(--fs-secondary); color: var(--text-secondary); margin: 10px 16px 14px; }
.list-count a { display: inline-flex; align-items: center; min-block-size: 44px; margin-inline-start: 6px; }

/* --- the customer inbox (12 Oct 2026, migration 080) ------------------
   A list of conversations; in one, the customer's messages left and the
   studio's right, on the page's own surfaces. No accent but the one Send. */
.inbox-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.inbox-row__link { display: grid; grid-template-columns: 1fr auto; gap: 2px 12px; padding: 12px 14px;
  min-block-size: 44px; border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface); color: var(--text); text-decoration: none; }
.inbox-row.is-unread .inbox-row__who { font-weight: 700; }
.inbox-row__who { font-weight: 600; }
.inbox-row__number, .inbox-row__at { color: var(--text-muted); font-size: var(--fs-meta); }
.inbox-row__link .pill { justify-self: start; }
.inbox-row__last { grid-column: 1 / -1; color: var(--text-secondary); overflow-wrap: anywhere; }
.inbox-log { display: flex; flex-direction: column; gap: 10px; }
.inbox-msg { max-width: 44rem; padding: 10px 14px; border-radius: var(--radius-lg);
  border: 1px solid var(--border); background: var(--surface); }
.inbox-msg--in { align-self: flex-start; }
.inbox-msg--out { align-self: flex-end; background: var(--surface-sunken); }
.inbox-msg__who { margin: 0 0 4px; font-size: var(--fs-meta); color: var(--text-muted); }
.inbox-msg__body { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.inbox-link { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.inbox-msg__status { margin: 6px 0 0; display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  font-size: var(--fs-meta); color: var(--text-secondary); }

/* --- who has a customer chat, and what each send did (12 Oct 2026, 081) --
   The owner line is the chat's state NOW; the status line under a send is
   what Meta did with it, in words, with Meta's reason when it failed. */
/*  ROW, NOT .panel-body's COLUMN (12 Oct 2026, the Phase A walk): left to
    inherit the column, the line's 20rem basis became 20rem of HEIGHT. */
.chat-owner { flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; }
.chat-owner__line { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; flex: 1 1 20rem; }
.chat-owner__actions { display: flex; flex-wrap: wrap; gap: 8px; }
.send-status { margin: 0 0 8px; }
.totals-wait { margin: 0 0 6px; }
.send-status__line { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.send-status__said { color: var(--text-secondary); }
.send-status__meta { margin: 2px 0 0; font-size: var(--fs-meta); color: var(--text-muted); }
/*  A SEND, ITS STATUS AND ITS FALLBACKS ARE ONE BLOCK on its own full row —
    inside a file row's action strip they were squeezed side by side. */
.send-block { flex: 1 1 100%; inline-size: 100%; display: flex; flex-direction: column; gap: 4px; }
.send-fallbacks .form-actions { flex-wrap: wrap; }
.send-fallbacks { margin-top: 12px; padding-top: 10px; border-top: 1px dashed var(--border); }
.send-fallbacks__title { margin: 0 0 6px; font-size: var(--fs-meta); color: var(--text-muted); }

/* --- a product's other names (12 Oct 2026, migration 079) ------------- */
.alias-list { list-style: none; margin: 0 0 12px; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.alias-list li { display: inline-flex; align-items: center; gap: 6px; padding: 2px 4px 2px 10px;
  border: 1px solid var(--border); border-radius: 999px; }
.alias-add { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.alias-add .input { flex: 1 1 14rem; min-width: 0; }

/* --- documents issued elsewhere (12 Oct 2026) -------------------------
   One panel per PDF; the proposals inside it are the assistant's own cards. */
.import-doc { margin-top: 12px; }
.import-doc .panel-body { display: flex; flex-direction: column; gap: 10px; }
.import-reply { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.import-reply .input { flex: 1 1 16rem; min-width: 0; }


/* AN ORDER'S SHORT NAME LEADS ITS ROW (D2, 15 Oct 2026): "E82D · …". */
.short-name { font-weight: 600; letter-spacing: .02em; font-variant-numeric: tabular-nums; }

@media (max-width: 900px) {
  /*  THE NUMBER STAYS A NUMBER ON A PHONE. 46px, not badge scale — the whole
      point of promoting a metric is that it is identifiable while scanning,
      and a phone is where scanning happens. Label, figure and sentence are
      already vertically ordered, so nothing needs to move.

      IN THIS QUERY AND NOT A SECOND ONE. Opening another `@media (max-width:
      900px)` block put two queries with the same condition in the sheet, and
      `_query_900` in the suite reads THE 900px query — it found the new one,
      which has no .page-head rule, and reported a defect fixed in September
      as back. Two blocks that must agree, with nothing forcing them to. */
  :root { --fs-display: 46px; }
  .metric-panel { padding: 18px 16px; }

  /* THE GRID MUST COLLAPSE WITH THE SIDEBAR. Reported 3 September 2026 on a
     390px phone: the sidebar was display:none but the 232px column it sat in
     was still there, so every screen rendered in a strip 232px narrower than
     the viewport — one word per line in .work-row, tables clipped, a document
     number on three lines. Every mobile symptom that day followed from this
     one rule. The selector is .app and must stay .app through extraction: the
     shell wrapper keeps its class in the repo, only tokens move to :root. */
  .app {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
  }
  .app > .sidebar { display: none; }
  .app > .mobile-bar { display: flex; }
  .app > .mobile-nav { display: flex; }
  /* The breadcrumb moves into .mobile-bar's second line, so the desktop topbar
     goes — with it visible, the crumb appeared twice. */
  .main > .topbar { display: none; }
  /* The strip is fixed, so the shell reserves its height or the last row of
     every table sits under it. */
  .app { padding-bottom: calc(var(--strip-h) + env(safe-area-inset-bottom, 0px)); }

  /* THE AXIS MOVES ONCE. --page-x is 16px at phone width and the topbar, the
     mobile header and every section follow it — no component restates its own
     phone padding. Page furniture checked at 390px in the board's iframe
     frames. */
  /* BOTH SELECTORS, and this is the specificity trap this sheet already
     documents at the token blocks. The base is .app, .app[data-mode="dark"],
     so on a wrapper that CARRIES the attribute the matching rule is (0,2,0); a
     bare .app in here is (0,1,0) and loses no matter what order it is in. A
     media query does not change specificity. The result was 34px page margins
     inside a 390px phone frame — every dark frame, which is every frame that
     ships. Match the base's shape: [data-mode] ties with it and wins on order,
     because this query is the last block of the sheet. */
  :root, :root[data-mode] { --page-x: 16px; --page-gap: 20px; }
  .page { padding: 14px var(--page-x) 32px; }
  .page-head { flex-direction: column; align-items: stretch; gap: 12px; }
  /* FLEX-BASIS FOLLOWS THE FLEX DIRECTION. The desktop rule gives the head's first
     child flex: 1 1 340px — a WIDTH while the head is a row, and a HEIGHT the moment
     this query turns it into a column. Every phone screen reserved a third of a
     viewport of empty space above its content (Home 254px, Inquiries 331px, Jobs
     275px) and nothing in the sheet looked wrong. Any basis set for a row has to be
     released here; do not add one without an auto beside it. */
  .page-head > :first-child { flex: 1 1 auto; }
  /*  A LONE ACTION IS NOT A FULL-WIDTH CONTROL. `align-items: stretch` above
      makes every child of the column as wide as the head, so a bare
      `<a class="btn btn-quiet">Back to the board</a>` — which is what the
      inquiry, the material and the customer screens put there — became a
      390px-wide box with its label centred in the middle of the page. It
      read as a primary action, and a centred one, which is neither what it
      is nor where it belongs.

      Releasing the stretch on everything after the title block leaves each
      action its own width, aligned to the content's left edge. The touch
      target is untouched: .btn keeps its 44px minimum in this query. */
  .page-head > :not(:first-child) { align-self: flex-start; }
  /*  AND ITS LABEL SITS ON THE CONTENT EDGE. A quiet action is a text link
      wearing a button's box, so its 11px of horizontal padding pushed "Back
      to the board" 11px inside the eyebrow and the customer line above it —
      deliberate-looking on its own and wrong beside them.

      THE PADDING GOES, THE TOUCH TARGET DOES NOT: .btn keeps its 44px
      min-height in this query, and the target now starts at the edge rather
      than 11px into the gutter. Scoped to a DIRECT child of the head, so the
      actions inside .page-actions — which sit in a row and need their
      separation — are untouched. */
  .page-head > .btn-quiet { padding-inline: 0; }

  /*  THE DATE MOVES ABOVE ITS DAY ON A PHONE. A 58px column beside a 342px
      content width is a sixth of the line taken from descriptions that are
      already the longest text on the screen — "Proof v2 approved by Mariyam,
      Dhiraagu" wrapped to two lines to leave room for "9 Sep".

      ONE COLUMN, AND THE DATE IS ITS OWN ROW. It renders only on the first
      event of a day, so on every other row the span is genuinely empty —
      `{%- ... -%}` trims the whitespace, which is what makes :empty work —
      and an empty row would otherwise open a gap in the middle of a group.

      THE RAIL IS UNAFFECTED: it is drawn by .timeline-body, which is still
      one element per event whatever the grid does around it. */
  .timeline-item { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .timeline-date {
    text-align: start; padding: 0 0 7px 19px;
    font-weight: 600; color: var(--text-secondary);
  }
  .timeline-date:empty { display: none; }
  /* HOME'S SHAPES REFORMAT, they do not scale. Four across becomes two across —
     four in a row at 390 gives each card 80px and the figure stops dominating
     anything. The attention row MOVES its reference to its own line — it used
     to drop the column entirely, on the reasoning that the title and customer
     identify the record and the ref is for a desktop scan; the review found
     that a phone row with no reference cannot be matched to the job in your
     hand. It keeps its mark, so urgency survives the narrowing. The floor
     panel goes full width under the work rather than beside it. */
  .signal-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 9px; }
  .signal { min-height: 132px; padding: 15px 16px; }
  .signal-figure { font-size: 31px; }
  /* ============================================================
     THE FIRST HAND EDIT TO THIS FILE — 6 September 2026.

     Kept marked, though hand edits are now the norm and this is no longer
     unusual: it is the rule that crossed the line. Everything above and below
     it came out of design/extract_css.py; this one was written here, and
     writing it is what made the "GENERATED — do not hand-edit" header false
     and forced the retirement recorded at the top of this file.

     WHY IT HAD NOWHERE ELSE TO GO: the design authority is the external UI/UX
     reviewer, working from a mockup and screenshots. Claude Design is out of
     the loop, nobody maintains the .dc.html, and an extraction run after this
     point would have silently reverted this rule.

     WHAT IT FIXES: the reference was `display: none` here and its grid track
     was dropped, so a row at 390 showed a title, a customer and a time but
     nothing naming WHICH record it was. The review requires the reference
     visible at both widths — quiet, but present, because it is what makes a
     row identifiable.

     WHY A ROW AND NOT A COLUMN. Measured at 390 before choosing: the ref
     "MK-O-FAKE-0070" needs 97px on one line in 11.5px mono, and the whole
     row has 171px to share between the reference and the record once the
     mark, time, chevron and four gaps are taken. A 76px track would break
     the reference across two lines AND leave the title 95px — both worse
     than hiding it. So the ref takes its own line above the record and the
     record keeps its width. Cost: the row grows from 78px to 108px, which
     is what one more line of text costs.
     ============================================================ */
  .attention-row {
    grid-template-columns: 8px minmax(0, 1fr) auto 16px;
    grid-template-rows: auto auto;
    gap: 11px;
    row-gap: 4px;
    min-height: 78px;
    padding: 13px 14px;
  }
  /* Every child is placed explicitly. Auto-placement put the mark and the ref
     on the same line and pushed the chevron onto a third row. */
  .attention-row > .state-mark { grid-column: 1; grid-row: 2; }
  .attention-ref { display: block; grid-column: 2 / -1; grid-row: 1; }
  .attention-main { grid-column: 2; grid-row: 2; }
  .attention-time { grid-column: 3; grid-row: 2; }
  .attention-chevron { grid-column: 4; grid-row: 2; }
  /* One column below 900: the basis values above are widths in a row, and this
     turns the row into a column where a basis would be a HEIGHT — so both are
     released here, the same release .page-head's first child needs. */
  .home-columns { flex-direction: column; gap: 30px; }
  .home-work, .home-aside { flex: 0 0 auto; max-width: none; }
  .floor-panel { padding: 18px; }
  .floor-figure { font-size: 34px; }
  .avail-panel { padding: 16px 18px; }
  .head-actions { flex-wrap: wrap; }
  .page-title { font-size: 24px; }
  /* The one place the wordmark appears at phone width is the LOGIN card — the mobile
     header carries the screen title, not the brand, and the sidebar that holds the
     wordmark is display:none in this query. 27px against a 320px card reads as a
     logo dropped on the page; 22px reads as the card's own head. If a wordmark is
     ever added to the mobile bar it takes this size, not .wordmark's. */
  .login-head .wordmark { font-size: 22px; }
  .page-title-ref { font-size: 21px; }
  /* EVERY INTERACTIVE CONTROL IS 44px AT THE TOUCH, whatever it looks like.
     Buttons were 38px, chips 37px, selects and the switch smaller again. The
     visual size can stay compact on desktop; on a phone the BOX cannot. Each
     of these is a control a thumb has to land on at arm's length from a
     machine. */
  /*  `.app select` for the same specificity reason recorded at the 700px
      query: a bare `select` here loses to the component's own rule. */
  .btn, .btn-sm, .chip, .input, .search, .app select, .login-screen select,
  .segmented-option,
  .list-filter-clear, .constant-note-edit, .cell-input, .tag-remove {
    min-height: 44px;
  }
  /*  A TABLE'S ROW LINK IS A CONTROL TOO. Suppliers' name measured 78×35 at
      390 — the only way from the list to the record. */
  .table-data tbody th > a { display: inline-flex; align-items: center; min-height: 44px; }
  .tag-remove { min-width: 44px; border-radius: var(--radius); }
  /* Two figures per row at phone width, and the leading one keeps the axis. */
  .metric-strip { row-gap: 14px; }
  .metric { flex: 1 1 40%; padding: 2px 14px; }
  .metric:nth-child(odd) { border-left: 0; padding-left: 0; }
  .metric-card { padding-right: 14px; }
  .avail-group { border-right: 0; border-bottom: 1px solid var(--border); }
  .avail-group:last-child { border-bottom: 0; }
  /* .page-columns WAS MISSING FROM THIS QUERY, and it was the worst of the
     three. Hand-fixed 6 September 2026.

     `minmax(0, 1fr) 330px` at 390 leaves 327px of content box. The 330px
     aside plus the 20px gap needs 350, so the 1fr track resolved to ZERO —
     `0px 330px` — and every record panel on Inquiry Detail rendered 2px wide
     (its two borders) at its full height, with its content clipped by
     overflow: hidden. What they asked for, Quotes, Artwork, Proofs, The job
     and Invoice were all a sliver; the Timeline had the whole row.

     THE SAME SHAPE THREE TIMES NOW: .app's 232px sidebar column outliving the
     hidden sidebar, .home-columns' fr tracks collapsing the record column at
     924, and this. A fixed track beside a flexible one needs a rule in this
     query or the flexible one is what gives way.

     AND IT PASSED EVERY CHECK. A 2px element overflows nothing, and nothing
     inside a 2px column is 44px wide, so the overflow check and the
     tap-target check were both correct and both blind. That is why
     test_no_region_collapses_below_its_content now measures MINIMUM WIDTH. */
  .page-columns { grid-template-columns: minmax(0, 1fr); }
  /* .route-layout is 250px + 1fr and belongs to the PARKED route builder
     (SPEC §4b) — no template renders it today, and it is reachable by URL.
     Parked means it must still work when it comes back, so it is released
     here rather than left as the fourth instance of this bug waiting for
     whoever un-parks it. */
  .route-layout { grid-template-columns: minmax(0, 1fr); }
  .quote-layout { grid-template-columns: minmax(0, 1fr); }
  .doc-parties { grid-template-columns: minmax(0, 1fr); }
  .doc-corrects { grid-template-columns: minmax(0, 1fr); gap: 10px; }
  /* .work-row: four columns become two rows. When and money share the first
     line, what the row is takes the second, the side note the third. */
  .work-row { grid-template-columns: minmax(0, 1fr) auto; gap: 4px 12px; padding: 12px 14px; }
  .work-row > :first-child { grid-column: 1; grid-row: 1; }
  .work-row > .work-money { grid-column: 2; grid-row: 1; }
  .work-row > .work-what { grid-column: 1 / -1; grid-row: 2; }
  .work-row > .work-side { grid-column: 1 / -1; grid-row: 3; }
  /* .lines-table: the fixed number columns shrink so the description keeps a
     readable share of 358px. */
  .lines-table { font-size: 12.5px; }
  .lines-table th.c-qty { width: 50px; }
  .lines-table th.c-unit { width: 78px; }
  .lines-table th.c-total { width: 88px; }
  .lines-table td, .lines-table th { padding-left: 8px; padding-right: 8px; }
  .lines-table :is(td, th):last-child { padding-inline-end: var(--figure-edge); }
  /*  .form-grid IS ONE COLUMN ON A PHONE, EVERYWHERE. Only the receipt form
      released it, so the calculator's product select truncated to "— choose
      a p" and paired fields with labels of different lengths sat at
      different heights. .note-pair is the same two-up grid by another name. */
  .form-grid, .note-pair { grid-template-columns: minmax(0, 1fr); }
  .scope-switch__item { padding: 0 11px; }
  /* .table-data: headers never break mid-word. Wider than the viewport, the
     table scrolls sideways inside its panel rather than clipping BALANCE MVR. */
  .table-data { display: block; overflow-x: auto; }
  .table-data thead th { white-space: nowrap; }
  .receipt-form .form-grid { grid-template-columns: minmax(0, 1fr); }
  .overnight-row { flex-wrap: wrap; }
  .overnight-right { flex: 1 0 100%; justify-content: flex-start; text-align: left; }
  /* The quiet pair and the availability groups stack. Side by side at 390 each
     column was ~160px and every label wrapped to four or five lines. */
  .band-pair, .avail-groups { grid-template-columns: minmax(0, 1fr); }
  /* Tier and toggle sit on the TITLE's line; the note takes its own line under
     both, so a head is two lines at most instead of three. */
  .band-aside { order: 2; }
  .band-note { order: 3; flex: 1 0 100%; }

  /* THE RECORD ROW BECOMES ONE COLUMN, IN READING ORDER. Its four tracks are
     104 + 168 + 140 = 412px of fixed width beside the flexible one, so at 390
     the name — the thing you are looking for — would get the 110px left over.
     Released HERE and not in a query of my own at 899px: every other release
     is in this block, and test_every_fixed_column_is_released_below_900px
     reads this one. A second breakpoint doing the same job one pixel away is
     two things that must agree with nothing forcing them to. */
  .make-record-page .make-record-card--row {
    grid-template-columns: minmax(0, 1fr);
    gap: 10px 0;
    padding: 18px;
  }
  .make-record-page .make-record-row__money,
  .make-record-page .make-record-row__money-void { text-align: start; }
  /*  STACKED, THE STATUS SITS WITH THE NAME. The body is one grid cell and
      its controls are inside it, so the status column — next in the source —
      landed below the actions, far from the job it describes. The body
      yields its children to the grid and they are ordered: date, name,
      status, what it is, money, then what you can do to it. */
  .make-record-page .make-record-card--row .make-record-card__body { display: contents; }
  .make-record-page .make-record-card--row .make-record-row__when { order: 0; }
  .make-record-page .make-record-card--row .make-record-card__name { order: 1; }
  .make-record-page .make-record-card--row .make-record-row__status { order: 2; }
  .make-record-page .make-record-card--row .make-record-card__secondary { order: 3; }
  .make-record-page .make-record-card--row .make-record-row__money-cell { order: 4; }
  .make-record-page .make-record-card--row .make-record-card__controls { order: 5; }
}

