/*
 * The Hub stylesheet. Tokens, and every component in the vocabulary.
 *
 * LIVES AT public/shared/hub.css so a Hub page can link it as
 * /shared/hub.css. docs/design/hub.css is a one line import of this file, not
 * a copy, so the design sheet and the app cannot end up on two stylesheets.
 *
 * The neutral palette won the two way comparison and is now the default,
 * defined in the palette block below. The warm alternative is kept at
 * palette-warm.css as reference.
 *
 * WHAT LIVES WHERE, AND WHY THE SPLIT IS NOT ARBITRARY.
 *
 *   invariants block   the spacing scale, the single radius, the type scale
 *                      and the transition duration. A palette cannot reach
 *                      these.
 *   palette block      colour and typeface, and nothing else.
 *   everything below   the components.
 *
 * That is the split that makes "one radius everywhere" a property of the code
 * rather than a thing somebody has to remember. A palette cannot introduce a
 * second radius or a sixth type size, because it has no token to do it with.
 *
 * COLOUR BUDGET. Grey for structure, ink for content, the accent for one
 * primary action per view. State colour appears only on badges, and only as a
 * dot or a tint, never as page furniture. If a screen here shows more than
 * three colours plus its badges, it is wrong.
 *
 * Never use em dashes, per the working conventions in CLAUDE.md.
 */

/* -------------------------------------------------------------------------
 * Invariants. Not overridable by a palette, deliberately.
 * ---------------------------------------------------------------------- */
:root {
  /* Spacing scale. Seven steps, and nothing off it. Every margin, padding and
     gap in this file resolves to one of these. */
  --s4: 4px;
  --s8: 8px;
  --s12: 12px;
  --s16: 16px;
  --s24: 24px;
  --s32: 32px;
  --s48: 48px;

  /* One radius. Buttons, inputs, selects, cards, badges, banners, table cards. */
  --radius: 6px;

  /* Type scale. Five sizes. There is no sixth.
       t1  stat numbers and the page title
       t2  component group titles
       t3  body and every control
       t4  footnotes, section headers, table meta
       t5  labels and badges, always small caps */
  --t1: 32px;
  --t2: 20px;
  --t3: 14px;
  --t4: 12px;
  --t5: 11px;

  --lh-tight: 1.2;
  --lh-body: 1.5;

  /* Control geometry. 40px is the height of every input, select and button, so
     a row of mixed controls sits on one line with no optical correction. */
  --control-h: 40px;
  --control-px: var(--s12);
  --chart-h: 120px;
  --row-h: 44px;

  /* One duration, one easing. */
  --dur: 150ms;
  --ease: cubic-bezier(0.2, 0, 0.2, 1);
}

/* -------------------------------------------------------------------------
 * Palette: NEUTRAL. The default, and the one that won.
 *
 * Near white ground, cool grey structure, dark ink content. Inter Tight
 * throughout, so the display face and the UI face are the same family.
 *
 * MAGENTA IS FOR ACTIONS AND FOCUS ONLY, NEVER FOR TEXT. Brand #eb008b carries
 * white at 4.28:1, under the 4.5:1 AA needs for normal text, so the primary
 * button fills with #c2007a (5.88:1) and the brand value is kept for the focus
 * ring, where the 3:1 non text threshold applies and it passes. That is why
 * this palette has both an accent and an accent-solid.
 *
 * --c-border-strong IS THE CONTROL BORDER AND IT IS DARKER THAN IT LOOKS LIKE
 * IT SHOULD BE. It draws the edge of every input, select and button, and WCAG
 * 1.4.11 wants 3:1 for the visual information that identifies a control. The
 * obvious light grey (#c7ccd3) measures 1.61:1 on a card, which is both a
 * failure and the grey on grey mush this design avoids. #878e96 is 3.31:1.
 * --c-border stays light, because a divider is decoration and identifies
 * nothing.
 *
 * The warm alternative is kept at palette-warm.css. It is REFERENCE, not a
 * theme: nothing links it, and swapping palettes means replacing this block.
 * ---------------------------------------------------------------------- */
:root {
  --palette-label: "Neutral";

  --font-ui: "Inter Tight", system-ui, -apple-system, sans-serif;
  --font-display: "Inter Tight", system-ui, -apple-system, sans-serif;

  --c-bg: #fbfbfc;
  --c-surface: #ffffff;
  --c-surface-2: #f4f5f7;
  --c-input-bg: #ffffff;

  --c-ink: #14161a;
  --c-ink-2: #454b54;
  --c-ink-3: #61686f;

  --c-border: #e3e6ea;
  --c-border-strong: #878e96;

  --c-accent: #eb008b;
  --c-accent-solid: #c2007a;
  --c-accent-hover: #a80069;
  --c-on-accent: #ffffff;
  --c-focus: rgba(235, 0, 139, 0.40);
  --c-ordinal: #61686f;

  --shadow: 0 1px 2px rgba(16, 20, 28, 0.05);

  --c-state-active: #17803d;
  --c-state-slipping: #a16207;
/* DORMANT MOVED DEEPER, AND THE REASON IS THE MAP RATHER THAN THE BADGE.
     It was #b45309, which against slipping's #a16207 measures 1.02:1: the same
     lightness, 9.5 degrees of hue apart. On a badge that is invisible as a
     problem, because the word "dormant" is written next to the dot. On the map
     the dot is the whole label, and two 11px dots at identical lightness are
     one colour to anybody reading quickly and to anybody with the commoner
     kinds of colour blindness. #9a3412 is 1.48:1 against slipping with 20.5
     degrees of hue, and 7.31:1 on white against 5.02:1 before, so the badge
     got better on the way past. */
  --c-state-dormant: #9a3412;
  --c-state-lost: #b91c1c;
  --c-state-none: #7c848c;
/* THE MAP'S REVENUE MODE COLOURS BY NOTHING BUT "THIS IS AN ACCOUNT", because
   in that mode SIZE carries the revenue and the fill only has to say the dot
   is a real account rather than an unlabelled one. It is a token rather than
   a literal in map.html so it is measured with the rest of the palette: the
   value it replaces, #5a9fd4, was picked against the inverted basemap and is
   2.86:1 on the light one, under the 3:1 a non-text marker needs. */
  --c-marker-revenue: #1f6aa8;

  /* A PROSPECT IS NOT AN ACCOUNT STATE, which is why this is not a sixth
     --c-state-*. It is the one non-state data encoding the Hub spends a
     colour on: a lead the company has never sold, drawn on the map as a
     violet diamond and tagged violet in the route list and the popup.

     The value moved here from map.html, where it was #a97bdf, chosen against
     a near black basemap. On the near white one it measures 3.19:1, which
     clears the 3:1 a marker fill needs and FAILS the 4.5:1 its two text uses
     need, and it is used both ways. #8250c4 is the same hue at 5.39:1 on
     white and 4.94:1 on --c-surface-2, so one value now serves the fill and
     the label. The diamond SHAPE is what tells a lead from an account, per
     the comment on .pk below; the colour is the second cue and darkening it
     costs nothing that shape was not already carrying. */
  --c-prospect: #8250c4;

  --c-warn-bg: #fdf6e7;
  --c-warn-border: #f0dfae;
  --c-warn-ink: #7a5300;

  --c-danger-bg: #fdeeec;
  --c-danger-border: #f3c9c4;
  --c-danger-ink: #9f1d16;
}

/* -------------------------------------------------------------------------
 * Compatibility aliases for the shared JS modules.
 *
 * WHY THIS BLOCK EXISTS. nav.js, contact.js, entries.js, interests.js and
 * assets.js inject their own CSS, and that CSS names the OLD dark palette
 * tokens. A page that links this stylesheet and drops its inline `:root` would
 * leave those names unset, and an unset custom property does not fall back: it
 * is invalid at computed value time, so the declaration is dropped and the
 * property inherits. The nav would render, in the wrong colour, with no error
 * anywhere. That is this project's characteristic failure and it would arrive
 * on the first converted page.
 *
 * So the old names keep working, pointed at the new ones. Delete an alias only
 * when nothing in public/shared/ names it any more, which is a grep.
 *
 * --purple IS A PLACEHOLDER AND NOT A DESIGNED ANSWER. interests.js uses
 * magenta and purple to tell an item chip from a category chip, and the
 * neutral palette has one accent and no purple. Pointing it at ink keeps the
 * category chip inside the colour budget. The item chip no longer uses the
 * accent at all (see the interest chips bridge): light grey against dark
 * grey, each chip labelled with its kind.
 *
 * --prospect IS THE ONE ALIAS NOT NAMED BY public/shared/*.js, so section 0 of
 * check-contrast.mjs does not require it. map.html's own template literals
 * name it, in the popup badge and the route-sheet tag, and both are JS a
 * design pass may not edit. Keeping the old short name pointed at the palette
 * entry is what let the value change without touching either of them.
 * ---------------------------------------------------------------------- */
:root {
  --bg-base: var(--c-bg);
  --bg-elevated: var(--c-surface);
  --bg-elevated-2: var(--c-surface-2);
  --bg-input: var(--c-input-bg);
  --text-primary: var(--c-ink);
  --text-secondary: var(--c-ink-2);
  --text-tertiary: var(--c-ink-3);
  --magenta: var(--c-accent-solid);
  --purple: var(--c-ink-2);
  --red: var(--c-danger-ink);
  --orange: var(--c-state-dormant);
  --green: var(--c-state-active);
  --gold: var(--c-state-slipping);
  --prospect: var(--c-prospect);
}

/* -------------------------------------------------------------------------
 * Reset and page.
 * ---------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  padding: var(--s32) var(--s24) var(--s48);
  background: var(--c-bg);
  color: var(--c-ink);
  font-family: var(--font-ui);
  font-size: var(--t3);
  line-height: var(--lh-body);
  font-feature-settings: "cv05" 1;
  -webkit-font-smoothing: antialiased;
}

.wrap { max-width: 1040px; margin: 0 auto; }

/* Numbers align in columns wherever they are stacked. */
.num, .tbl td.num, .stat-value, .count b, .kpi { font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* -------------------------------------------------------------------------
 * Page header: title, identity line, nav.
 * ---------------------------------------------------------------------- */
.page-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s16); margin-bottom: var(--s16);
}
/* THE BRAND MARK, 2026-10-16: the Soft Stuff logo left of every page title
   and on the sign-in card, so a page reads as the company's own tool. A
   transparent copy of the launcher's mark with a dark rim that follows the
   shape (/shared/ssd-logo.png), so the white lettering holds on the light
   ground. Not a link: the nav is the way around, and a second way home in
   the header would be a hardcoded nav link (scripts/verify_nav.mjs). */
.brand-mark { width: 48px; height: 48px; flex: none; display: block; align-self: center; }
.page-head > .brand-mark + div { margin-right: auto; }
.brand-mark--gate { width: 56px; height: 56px; margin: 0 0 var(--s12); }
@media (max-width: 600px) { .page-head > .brand-mark { width: 40px; height: 40px; } }
.page-title {
  font-family: var(--font-display);
  font-size: var(--t1); line-height: var(--lh-tight);
  font-weight: 600; letter-spacing: -0.01em; margin: 0;
}
.identity { margin: var(--s4) 0 0; font-size: var(--t4); color: var(--c-ink-3); }
.identity .sep { margin: 0 var(--s4); }

.nav {
  display: flex; flex-wrap: wrap; gap: var(--s4);
  padding-bottom: var(--s8); margin-bottom: var(--s24);
  border-bottom: 1px solid var(--c-border);
}
.nav a, .nav .cur {
  font-size: var(--t4); font-weight: 500; letter-spacing: 0.01em;
  padding: var(--s4) var(--s8); border-radius: var(--radius);
  text-decoration: none; white-space: nowrap;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.nav a { color: var(--c-ink-3); }
.nav a:hover, .nav a.is-hover { background: var(--c-surface-2); color: var(--c-ink); }
.nav a:focus-visible, .nav a.is-focus { outline: 2px solid var(--c-accent); outline-offset: 1px; }
/* The current page is not a link, and colour alone does not say so: it also
   loses the pointer and gains a rule beneath it. */
.nav .cur {
  color: var(--c-ink); background: var(--c-surface-2); cursor: default;
  box-shadow: inset 0 -2px 0 var(--c-accent);
}

/* -------------------------------------------------------------------------
 * Section header with ordinal. "03 Revenue".
 *
 * A SECTION HEADER IS A HEADING, decided 2026-10-16 after a measured trial on
 * index.html. It was 12px grey small caps, the same style as the card and
 * group labels inside it, so it ranked lowest on the page, and the gap above
 * a section matched the gaps within it, so sections ran together. Now: the
 * t2 size, sentence case, ink; the ordinal small and grey beside it, as a
 * reference rather than a title; and twice the space above a section as
 * within it, less on a phone where height is the cost. The labels inside a
 * section keep their small caps, so the order reads heading, group label,
 * card label.
 * ---------------------------------------------------------------------- */
.sec {
  display: flex; align-items: baseline; gap: var(--s8);
  margin: var(--s48) 0 var(--s16); padding-bottom: var(--s12);
  border-bottom: 1px solid var(--c-border);
  font-family: var(--font-display);
  font-size: var(--t2); font-weight: 600;
  text-transform: none; letter-spacing: -0.01em;
  color: var(--c-ink);
}
@media (max-width: 600px) { .sec { margin-top: var(--s32); } }
/* The ordinal is a token, because the two palettes answer this differently:
   neutral keeps magenta off text entirely, warm can afford its accent here. */
.sec .ord, .fold > summary .ord {
  font-size: var(--t4); letter-spacing: 0.06em;
  color: var(--c-ordinal); font-variant-numeric: tabular-nums;
}
.sec .spacer { flex: 1 1 auto; }
.sec .aside {
  font-family: var(--font-ui); font-size: var(--t5); font-weight: 500;
  text-transform: none; letter-spacing: 0; color: var(--c-ink-3);
}

/* -------------------------------------------------------------------------
 * Cards, including the stat card.
 * ---------------------------------------------------------------------- */
.card {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--s16);
  transition: transform var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.card:hover, .card.is-hover { transform: translateY(-1px); border-color: var(--c-border-strong); }

.cards {
  display: grid; gap: var(--s12);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
/* THREE TO A ROW, AND TWO ON A PHONE WITH THE THIRD FULL WIDTH. auto-fit is
   right for a row whose length is not known, and wrong for a set of exactly
   three figures that belong together: at 1040px it spreads three cards across
   five columns' worth of width, and at 700px it can drop one card onto a row
   of its own with no rule about which. Three fixed columns keep a week's
   Revenue, Orders and Accounts side by side, and the phone rule below pairs
   the first two and gives the third the whole width, so nothing sits in a
   half-empty row. minmax(0, 1fr) rather than 1fr, because a 32px figure has a
   min-content width and grid would otherwise let it push the column wider
   than its share. */
.cards--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* THE DESTINATIONS LINE UP ALONG THE BOTTOM. Three cards in a row stand at
   one height, so a card whose footnote is one line shorter than its
   neighbour's would otherwise hang its link halfway up the box and the row
   would read as three unrelated blocks. */
.cards--3 .card { display: flex; flex-direction: column; }
.cards--3 .card-link { margin-top: auto; padding-top: var(--s12); }
/* SIX FIGURES, THREE BY TWO ON A DESKTOP AND TWO UP ON A PHONE. The plain
   auto-fit grid puts six cards five to a row at 1040 and leaves the sixth on
   a row of its own, and `.cards--3` gives its third child the whole phone
   width, which for six cards leaves the sixth beside a hole. lost-lines.html's
   summary is the user: two headline figures, then what was set aside and why. */
.cards--3x2 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

.stat-label {
  font-size: var(--t5); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--c-ink-3); margin: 0 0 var(--s8);
}
.stat-value {
  font-family: var(--font-display);
  font-size: var(--t1); line-height: var(--lh-tight); font-weight: 600;
  letter-spacing: -0.02em; color: var(--c-ink); margin: 0;
}
.stat-foot { font-size: var(--t4); color: var(--c-ink-3); margin: var(--s4) 0 0; }
/* A STAT CARD WHOSE VALUE IS A NAME, AN ADDRESS OR A DATE, not a figure.
   prospect.html's detail cards hold a contact name and a street address, and
   at t1 a contact name is four lines in a 170px phone column. One step down
   the scale, same weight and ink, so it is still the card's value tier and
   not body text; long words break rather than push the grid wider. */
.stat-value--text {
  font-size: var(--t2); letter-spacing: -0.01em; overflow-wrap: anywhere;
}
/* THE CARD'S DESTINATION: the list the count on it came from. Not a button,
   because a landing page carries several of these and none of them is its one
   primary action, and not a count line, because it holds no count and
   `.count` is the shape a protected figure is written in. */
.card-link { margin: var(--s12) 0 0; font-size: var(--t3); }
/* A row of badges inside a card, which is a count cut several ways. Spaced on
   the scale rather than on the paragraph default, which is 14px and on no
   scale at all. */
.card > .row { margin: var(--s8) 0 0; }

/* -------------------------------------------------------------------------
 * Badges. One size, one radius, one padding.
 *
 * STATE badges carry a coloured dot and ink text, so state reads at a glance
 * without spending a colour on the type. FLAG badges tint, because a flag is a
 * warning about the record rather than a property of the account.
 * ---------------------------------------------------------------------- */
/* `.flag-custom` and `.flag-inactive` are the order form's catalog flags on a
   typed code ("Custom", "Not currently active", "Discontinued"). The page's
   script writes them as spans inside `.flag-cell`, so they are listed here and
   on the two flag tints below rather than renamed. */
.badge, .flag-custom, .flag-inactive {
  display: inline-flex; align-items: center; gap: var(--s4);
  height: var(--s24); padding: 0 var(--s8);
  border: 1px solid var(--c-border); border-radius: var(--radius);
  background: var(--c-surface-2); color: var(--c-ink-2);
  font-size: var(--t5); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em; white-space: nowrap;
}
.badge .dot {
  width: var(--s8); height: var(--s8); border-radius: 50%;
  background: var(--c-state-none); flex: none;
}
.badge--active   .dot { background: var(--c-state-active); }
.badge--slipping .dot { background: var(--c-state-slipping); }
.badge--dormant  .dot { background: var(--c-state-dormant); }
.badge--lost     .dot { background: var(--c-state-lost); }
.badge--none     .dot { background: var(--c-state-none); }

.badge--warn,
.badge.st-retrieval_requested, .flag-custom {
  background: var(--c-warn-bg); border-color: var(--c-warn-border); color: var(--c-warn-ink);
}
.badge--danger,
.badge.st-lost, .flag-inactive {
  background: var(--c-danger-bg); border-color: var(--c-danger-border); color: var(--c-danger-ink);
}

/* -------------------------------------------------------------------------
 * Banners.
 * ---------------------------------------------------------------------- */
/* `.cat-note.stale` is opening-order.html's catalog banner in its warning state.
   The page's script sets `className = 'cat-note stale'` (or `ok`) on #catNote,
   so the banner rules name that pair rather than the page renaming it; see the
   order form block below. */
.banner, .cat-note.stale {
  display: flex; gap: var(--s12);
  padding: var(--s12);
  border: 1px solid var(--c-border); border-left-width: 2px;
  border-radius: var(--radius);
  background: var(--c-surface-2);
  font-size: var(--t3); color: var(--c-ink-2);
}
.banner + .banner { margin-top: var(--s8); }
/* FLEX ONLY WHEN THERE IS AN ICON TO SIT BESIDE. Found 2026-10-01 by the
   first capture ever to reach prospect.html's linked delete banner: as a flex
   container, a banner whose content is bare text with a <strong> and a link
   in it laid every piece out as its own column, one word wide. That is the
   shape every banner the shared modules and the page helpers write
   (warnIfTruncated, read failures, refusals), so every such banner on every
   page was broken the moment it fired, and nothing photographed one firing. */
.banner:not(:has(> .icon)) { display: block; }
.banner strong { color: var(--c-ink); font-weight: 600; }
.banner .icon { flex: none; color: var(--c-ink-3); margin-top: 2px; }
.banner--info { border-left-color: var(--c-border-strong); }
.banner--warn, .cat-note.stale {
  background: var(--c-warn-bg); border-color: var(--c-warn-border);
  border-left-color: var(--c-warn-ink); color: var(--c-warn-ink);
}
.banner--warn strong { color: var(--c-warn-ink); }
.banner--warn .icon { color: var(--c-warn-ink); }
/* The third banner, for a read or a write that FAILED, as distinct from a
   caveat about a result that arrived. The shared modules already draw this
   distinction (rows.js, entries.js and assets.js all write `banner err` on a
   failure and nothing else does), so the sheet needs the third tint or every
   failure renders as an ordinary note. */
.banner--danger {
  background: var(--c-danger-bg); border-color: var(--c-danger-border);
  border-left-color: var(--c-danger-ink); color: var(--c-danger-ink);
}
.banner--danger strong { color: var(--c-danger-ink); }
.banner--danger .icon { color: var(--c-danger-ink); }

/* -------------------------------------------------------------------------
 * Count line. The protected "N in view, N off screen" shape.
 * ---------------------------------------------------------------------- */
/* THE COUNT LINE IS BODY, NOT A FOOTNOTE. It was --c-ink-3, which put the
   sentence a rep actually reads into the same tier as the caption under a
   stat card. The number stays 600 ink and the sentence is 400 ink; anything
   after it that states a basis rather than a fact takes .foot. */
.count { font-size: var(--t3); font-weight: 400; color: var(--c-ink); margin: 0; }
.count b { color: var(--c-ink); font-weight: 600; }
.count .foot { font-size: var(--t4); color: var(--c-ink-3); }
.count + .count { margin-top: var(--s4); }

/* SEVERAL COUNTS ON ONE LINE, for a view that has more protected counts than
   it has vertical room. Each count keeps its own element and its own id, so
   an assertion that names one still names one; what it gives up is a line
   break and a margin per count.

   THE SEPARATOR IS ON THE LATER SPAN AND USES `~`, NOT `+`, and that is the
   whole rule. A count that is legitimately empty (the map draws no
   no-coordinates sentence on a fully geocoded load) must not leave a middot
   hanging on either side of it, so a span takes a leading separator when SOME
   earlier sibling has content, rather than when the one immediately before it
   does. Written with `+` the middle empty case drops the separator entirely
   and two counts run together as one sentence. */
.count--multi > span:not(:empty) ~ span:not(:empty)::before {
  content: "\00b7"; color: var(--c-ink-3); margin-right: var(--s4);
}

/* ---------------------------------------------------------------------------
 * A COUNT THAT IS A WARNING, WITH THE WARNING IN THE ICON AND THE TEXT IN INK.
 *
 * The map's "97 accounts not shown, no coordinates" needs to read as a warning
 * and used to do it by turning the whole line orange, which broke the rule
 * that state colour is confined to badges and left a line of coloured body
 * text on a page whose every other count is ink.
 *
 * A banner is the wrong component for this: .banner--warn tints its whole box
 * and takes the text with it, which is right for "a result arrived with
 * something wrong in it" and too loud for one clause inside a count line that
 * is otherwise fine. The icon carries the alarm and the sentence stays in the
 * tier every other count is in, so the line still scans as one line.
 *
 * The icon is the same --c-warn-ink the warn banner and the warn badge use, so
 * there is one warning colour in the Hub rather than a second one that only
 * appears here.
 * ------------------------------------------------------------------------- */
/* THE SAME ICON IN A TABLE CELL, for a figure that has crossed a line the
   page states: prospects.html's Last touch at 21 days or more. It used to set
   the cell orange inline, which is state colour outside a badge. One warning
   colour, one icon, and the figure stays ink. */
.count .warn-icon, .tbl .warn-icon {
  color: var(--c-warn-ink);
  width: 14px; height: 14px; vertical-align: -2px;
  margin-right: var(--s4); flex: none;
}

/* ---------------------------------------------------------------------------
 * DISCONTINUED EXPOSURE, ONE TREATMENT ON EVERY PAGE THAT SHOWS IT.
 *
 * Three states, because two of them used to be one empty cell: a real
 * percentage, a real zero, and no exposure row at all. The number is plain
 * ink and takes NO colour band. The distribution is continuous and the rule
 * is that the figure is shown so a rep can judge and is never filtered or
 * ranked on, so an amber band above 40% and an orange one above 70% put two
 * thresholds on it and re-introduced the ranking the rule forbids.
 *
 * It lived on account.html and not on recovery.html, which is one number
 * under two rules on two pages. This is the single definition.
 * ------------------------------------------------------------------------- */
.exp-pct, .exp-zero { color: var(--c-ink); font-variant-numeric: tabular-nums; }
.exp-none { color: var(--c-ink-3); }

/* ---------------------------------------------------------------------------
 * THREE TEXT TIERS, BY WEIGHT AND COLOUR RATHER THAN BY SIZE ALONE.
 *
 * Size alone was carrying the whole hierarchy, and one step of the scale is
 * not enough to separate four kinds of text: a label, a value, a footnote and
 * a control all rendered at weight 400 in grey, a size apart, and a phone
 * screen read as one texture. These three are the whole vocabulary, and a
 * fourth treatment is a bug rather than an addition.
 *
 *   .t-label  600, muted, small caps. What a value IS.
 *   .t-body   400, ink. The value, and any sentence a rep reads for content.
 *   .t-foot   400, muted, one size smaller than body. Bases, empty states,
 *             and anything that qualifies a value rather than being one.
 *
 * Interactive text is none of these: it is ink at 500 or more with an
 * underline or a chevron, which is what keeps it out of all three tiers.
 * scripts/check-type-tuples.mjs fails a page where a control shares a
 * (size, weight, colour) with static text.
 * ------------------------------------------------------------------------- */
.t-label {
  font-size: var(--t5); font-weight: 600; color: var(--c-ink-3);
  text-transform: uppercase; letter-spacing: 0.08em;
}
.t-body { font-size: var(--t3); font-weight: 400; color: var(--c-ink); }
.t-foot, .cat-note.ok { font-size: var(--t4); font-weight: 400; color: var(--c-ink-3); }

/* A SUB-LABEL INSIDE A SECTION, one size below a section header and with the
   letter-spacing dropped, so UPCOMING under 02 ACTIVITY cannot be mistaken
   for a section of its own. Same weight and case as a header: it is the same
   KIND of thing, one rank down, and the two signals that separate the ranks
   are size and tracking rather than colour, which is already carrying the
   tier distinction. */
/* `.grp-h` IS THE SAME THING UNDER entries.js's NAME, so it shares this
   declaration rather than carrying a near copy of it. That module writes
   UPCOMING and HISTORY as `.grp-h` and is data code a design pass may not
   edit, so the alias is the only way the pair can take the sub-label rank.
   Its own spacing stays with the other module markup in the bridge below. */
.sublabel, .grp-h {
  font-family: var(--font-display);
  font-size: var(--t5); font-weight: 600; color: var(--c-ink-2);
  text-transform: uppercase; letter-spacing: 0;
  margin: var(--s16) 0 var(--s8);
}
.sublabel:first-child { margin-top: 0; }

/* -------------------------------------------------------------------------
 * Save indicator. Three states, one shape.
 *
 * TWO SPELLINGS OF THE SAME THREE STATES. The sheet writes `.save--saving`,
 * `.save--saved` and `.save--failed`. opening-order.html's autosave sets
 * `saving`, `saved` and `error` on #saveStatus from updateSaveStatus(), which
 * is data code, so those names are listed on the same rules rather than the
 * script being edited to match. The fourth state, no class at all, is "No
 * changes" or "Submitted": the neutral shape with no icon.
 *
 * ONE ICON PER STATE, ALL THREE IN THE MARKUP. The script changes a class and
 * a text node and nothing else, so it cannot swap an icon. The page carries
 * one of each (`.save-i--saving`, `--saved`, `--failed`) and the state class
 * shows one.
 * ---------------------------------------------------------------------- */
.save {
  display: inline-flex; align-items: center; gap: var(--s8);
  height: var(--s32); padding: 0 var(--s12);
  border: 1px solid var(--c-border); border-radius: var(--radius);
  background: var(--c-surface-2);
  font-size: var(--t4); color: var(--c-ink-2);
}
.save .icon { flex: none; }
.save--saving .icon, .save.saving .icon { color: var(--c-ink-3); animation: spin 900ms linear infinite; }
.save--saved  .icon, .save.saved .icon { color: var(--c-state-active); }
.save--failed, .save.error {
  background: var(--c-danger-bg); border-color: var(--c-danger-border); color: var(--c-danger-ink);
}
.save--failed .icon, .save.error .icon { color: var(--c-danger-ink); }
.save .save-i { display: none; }
.save.saving .save-i--saving,
.save.saved .save-i--saved,
.save.error .save-i--failed { display: inline-block; }
.save time { color: var(--c-ink-3); font-variant-numeric: tabular-nums; }
@keyframes spin { to { transform: rotate(360deg); } }

/* -------------------------------------------------------------------------
 * Table. 44px rows, small caps header, right aligned numbers, hover. Faint stripes on reference tables only (see READABLE ROWS).
 * ---------------------------------------------------------------------- */
.tbl-wrap {
  border: 1px solid var(--c-border); border-radius: var(--radius);
  background: var(--c-surface); box-shadow: var(--shadow); overflow: hidden;
}
.tbl { width: 100%; border-collapse: collapse; font-size: var(--t3); }
.tbl th {
  height: var(--row-h); padding: 0 var(--s12); text-align: left;
  font-size: var(--t5); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--c-ink-3); background: var(--c-surface-2);
  border-bottom: 1px solid var(--c-border);
}
.tbl td {
  height: var(--row-h); padding: 0 var(--s12);
  border-bottom: 1px solid var(--c-border); color: var(--c-ink);
}
.tbl tbody tr:last-child td { border-bottom: 0; }
/* No zebra on a card list or a short table: the hover highlight is the row
   treatment. Reference tables stripe, faintly; see READABLE ROWS below. */
.tbl tbody tr { transition: background var(--dur) var(--ease); }
.tbl tbody tr:hover, .tbl tbody tr.is-hover { background: var(--c-surface-2); }
.tbl .num { text-align: right; }
/* RIGHT ALIGNED IN THE TABLE, NORMAL FLOW ON THE CARD. For a column that holds
   a figure on most rows and a sentence on the rest: recovery.html's STILL
   SELLABLE is a count, or "nothing it bought is still in the catalogue", or
   "no item detail loaded". In the table the figure belongs in the run of right
   aligned numbers beside it, and the header takes the same class so the two
   agree rather than the value sitting a column width away from its own label.

   IT IS NOT `.num`, AND THE REASON IS THE CARD. Below 600px `.tbl td` is
   already a flex row with the label left and the value flush right, so
   text-align moves no figure at all there; the only thing it reaches is the
   sentence states, which it re-rags against the right margin across three or
   four wrapped lines. So the alignment is scoped to the width where it lines a
   column up. No tabular figures either, for the same reason: `.num` carries
   those and they would reach the card, and a right aligned integer in a table
   lines up on its right edge whether or not its digits are fixed width. */
.tbl .num-desk { text-align: right; }
.tbl .muted { color: var(--c-ink-3); }
/* A negative total is a credit, and it takes the same colour the chart gives a
   credit bar. One fact, one colour, wherever it appears.

   AND A WEIGHT, SO IT IS NOT COLOUR ALONE. reconcile.html also marks a
   non-zero No-line orders count with it, and a count has no minus sign to
   carry the meaning for a reader who cannot tell the red from the ink. 600 is
   the figure weight the count line and the totals row already use, so it adds
   no new weight to the scale. */
.tbl .neg { color: var(--c-state-lost); font-weight: 600; }

/* A LEDGER TABLE: every column but the first is a figure, and the job is to
   hold one row against another source digit by digit. `num-desk` deliberately
   carries no tabular figures (see above), which is right for a column that is
   sometimes a sentence and wrong for a column of currency, so the modifier
   puts them on the whole table. reconcile.html's three revenue tables. */
.tbl--ledger { font-variant-numeric: tabular-nums; }
/* One line per row. A ledger row is read across, and "Aug 29 to Sep 4, 2026"
   wrapping onto two lines at 1280 made every row read as two. The cells are
   figures and one short label, so nothing here needs to wrap; the headers
   still may. */
.tbl--ledger td { white-space: nowrap; }

/* THE TOTALS ROW. A <tfoot> under the body, one pixel of the strong border
   above it and the figure weight, and no fill: it is the same row height and
   the same ink as every row above it, and it ranks by weight alone. */
.tbl tfoot td {
  height: var(--row-h); padding: 0 var(--s12);
  font-weight: 600; color: var(--c-ink);
  border-top: 1px solid var(--c-border-strong); border-bottom: 0;
}

/* A LINK INSIDE A CELL, and this is the third place the same treatment has had
   to be written. `.idline a` and `a.cc-v` already say it: ink rather than
   accent, a rule underneath rather than a colour, the accent only on hover and
   focus. A header holding links must not spend the one accent per view, and a
   TABLE of 595 of them even less so.

   `text-decoration` rather than `border-bottom` because an account name wraps
   to three lines on a 390 wide card and a bottom border draws one rule under
   the whole box; the underline follows the text.

   `.link` IS THE SAME TREATMENT WITH A NAME, AND IT IS THE DEFINITION NOW.
   The gap this comment used to record was real: hub.css declared no rule for
   a bare `<a>` at all, so a link that was not inside a table rendered in the
   browser's default blue, and section 22 of the sheet drew its own
   "interactive" specimen that way. index.html needs links outside a table (a
   reminder's account, "Full recovery list", a search hit), so the treatment
   is a class and the table selector is one of its users. Both are listed here
   rather than one aliasing the other, so neither can drift.

   500 AND INK IS THE WHOLE POINT, not decoration on top of a colour. Weight
   is what keeps a link's (size, weight, colour) out of the body tier, which
   is what scripts/check-type-tuples.mjs asserts; the underline is the signal
   a reader sees and the weight is the one the check can measure. */
.tbl a, .link {
  color: var(--c-ink); font-weight: 500; text-decoration: underline;
  text-decoration-color: var(--c-border-strong); text-underline-offset: 3px;
  transition: text-decoration-color var(--dur) var(--ease);
}
.tbl a:hover, .tbl a.is-hover, .link:hover, .link.is-hover {
  text-decoration-color: var(--c-accent);
}
.tbl a:focus-visible, .tbl a.is-focus, .link:focus-visible, .link.is-focus {
  outline: 2px solid var(--c-accent); outline-offset: 2px; border-radius: 2px;
}

/* THE EMPTY STATE OF A TABLE IS A SENTENCE, NOT A ONE COLUMN ROW. A table
   whose only row says "no orders" still draws a header, a border and a card on
   the phone, so the emptiness arrives dressed as data. This strips the row back
   to the same treatment as `.entry-empty`, which is what an empty list looks
   like everywhere else in the Hub. The row is still a row, because the page
   that fills the table cannot always write something other than one.

   THE SIZE WENT WITH THE COLOUR, which it did not before: this said
   `.entry-empty` and then set the colour alone, so the sentence stayed at
   body size in muted grey, one of the "neither tier" pairs the three tiers
   above exist to remove. An empty state is a footnote about the data. */
.tbl tr.empty td, .tbl tr.empty {
  font-size: var(--t4); font-weight: 400; color: var(--c-ink-3);
}
.tbl tr.empty td { height: auto; padding: var(--s12) 0; border-bottom: 0; }
/* And the wrapper loses its card treatment with it, so the sentence is not
   framed as though it were a result. Degrades to a framed sentence where
   `:has()` is unsupported, which is the harmless direction. */
.tbl-wrap:has(tr.empty) { border: 0; background: none; box-shadow: none; }

/* Below 600px the table stops being a table and becomes one card per row. */
@media (max-width: 600px) {
  .tbl-wrap { border: 0; background: none; box-shadow: none; overflow: visible; }
  .tbl thead { display: none; }
  .tbl, .tbl tbody, .tbl tr, .tbl td { display: block; width: 100%; }
  /* A CELL WITH NOTHING TO SAY IS NOT A LINE ON THE CARD. At 1280 an empty
     cell keeps its column, because the column is the alignment; on a phone
     card it was a label with no value, "AT" and "DAYS" on every asset in the
     warehouse. The page marks such a cell `is-empty`, so the rule only ever
     removes what the page has said is empty, never a cell whose value
     happens to render as blank. */
  .tbl td.is-empty { display: none; }
  .tbl tr {
    background: var(--c-surface);
    border: 1px solid var(--c-border); border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: var(--s8) var(--s12); margin-bottom: var(--s8);
  }
  .tbl tbody tr:last-child { margin-bottom: 0; }
  /* An empty state is not a card either. Same rule as above, one width down,
     and its cell stays a block: the flex row below is what puts a label on the
     left and a value on the right, and a sentence has no label, so it would be
     pushed to the right margin on its own. */
  .tbl tr.empty {
    background: none; border: 0; box-shadow: none; padding: 0; margin-bottom: 0;
  }
  .tbl tr.empty td { display: block; padding: var(--s12) 0; }
  .tbl td {
    display: flex; align-items: center; justify-content: space-between; gap: var(--s16);
    height: auto; min-height: var(--s24); padding: var(--s4) 0; border-bottom: 0;
  }
  .tbl td::before {
    content: attr(data-label);
    font-size: var(--t5); font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.06em; color: var(--c-ink-3);
  }
  .tbl td.num { text-align: right; }
  /* THE TOTALS ROW IS A CARD TOO, AND IT COMES FIRST. At 390 a 100 row table
     is 100 cards, and a totals card after the last of them is thirty screens
     from where a reader starts. The table becomes a flex column so the footer
     can be ordered ahead of the body, and the card takes the strong border so
     it is not read as one more week. Its first cell is relabelled Total,
     because its column label names a week and its value is a count of them.
     Empty totals cells (Accounts, which is distinct per period and does not
     sum) take no line. Where `:has()` is unsupported the totals card lands
     after the rows, which is where the table puts it anyway. */
  .tbl:has(> tfoot) { display: flex; flex-direction: column; }
  .tbl > tfoot { display: block; order: -1; margin-bottom: var(--s8); }
  .tbl tfoot tr { border-color: var(--c-border-strong); }
  .tbl tfoot td { border-top: 0; height: auto; padding: var(--s4) 0; }
  .tbl tfoot td:empty { display: none; }
  .tbl tfoot td:first-child::before { content: "Total"; }
  /* See `.num-desk` above. The value is already flush right on a card, so the
     cell keeps normal flow for the rows where it is a sentence rather than a
     figure. Stated as `left` rather than left out, because the base rule is
     not inside a media query and would otherwise apply here too. */
  .tbl td.num-desk { text-align: left; }
  /* A CELL HOLDING SEVERAL CONTROLS keeps them in one wrapper, because the
     cell is a flex row here and each child would otherwise be its own item.
     The wrapper sits flush right like every other value on the card, and its
     buttons wrap within it. assets.html's Actions cell is the user. */
  .tbl td > .row { justify-content: flex-end; }
}

/* A REFERENCE TABLE KEEPS ITS COLUMNS AT EVERY WIDTH, AND SCROLLS SIDEWAYS.
   Card stacking is for lists a rep acts on in the field: one account per
   card, read top to bottom in a van. A reference table read at a desk is the
   other kind (reconcile.html's ledgers, held row against row with the
   Scorecard and Acumatica), and as cards its 100 weeks became 100 nine-line
   cards, 52 screens of them, with no column to read down. `.tbl--ref` undoes
   every phone rule above for one table: the header stays, rows stay rows,
   the totals stay last, and the wrapper scrolls inside itself so the page
   does not. The first column is pinned so a reader scrolled to the right
   still knows which week a figure belongs to. Decided 2026-10-01, and stated
   in CLAUDE.md's design section and designer.md. */
.tbl-wrap:has(> .tbl--ref) { overflow-x: auto; overscroll-behavior-x: contain; }
.tbl--ref th:first-child, .tbl--ref td:first-child {
  position: sticky; left: 0; z-index: 1; background: var(--c-surface);
}
.tbl--ref th:first-child { background: var(--c-surface-2); }
.tbl--ref tbody tr:hover td:first-child { background: var(--c-surface-2); }
@media (max-width: 600px) {
  .tbl-wrap:has(> .tbl--ref) {
    border: 1px solid var(--c-border); background: var(--c-surface); box-shadow: var(--shadow);
    overflow-x: auto;
  }
  .tbl.tbl--ref { display: table; width: max-content; min-width: 100%; }
  .tbl.tbl--ref:has(> tfoot) { display: table; }
  .tbl--ref thead { display: table-header-group; }
  .tbl--ref tbody { display: table-row-group; }
  .tbl--ref > tfoot { display: table-footer-group; order: 0; margin-bottom: 0; }
  .tbl--ref tr {
    display: table-row; width: auto; background: none;
    border: 0; border-radius: 0; box-shadow: none; padding: 0; margin: 0;
  }
  .tbl--ref td, .tbl--ref th {
    display: table-cell; width: auto; height: var(--row-h); min-height: 0;
    padding: 0 var(--s12); border-bottom: 1px solid var(--c-border); white-space: nowrap;
  }
  .tbl--ref tfoot td { border-top: 1px solid var(--c-border-strong); border-bottom: 0; }
  .tbl--ref td::before, .tbl--ref tfoot td:first-child::before { content: none; }
  .tbl--ref td.num-desk { text-align: right; }
  .tbl--ref td.is-empty, .tbl--ref tfoot td:empty { display: table-cell; }
}

/* -------------------------------------------------------------------------
 * READABLE ROWS. Decided 2026-10-16 after a trial on pricing.html, measured
 * before and after at 390 and 1280. A wrapped cell doubled a row's height,
 * and every cell in the same ink gave the eye nothing to hold, which is what
 * made long tables tiring. Five tools, each opt in by class except the first
 * two, which every reference table takes:
 *
^ *   one line per row   a reference table never wraps a cell; long text is cut
 *                      with an ellipsis (.trunc) and carries its full value in
 *                      a title
 *   faint stripes      on reference tables only, where a row is read across
 *                      many columns; never on a card list or a short table
 *   .lead / .sub       the identity and the answer at the figure weight, the
 *                      columns read only when needed in the secondary ink
 *   .same              a value repeated from the row above steps back, so a
 *                      run of one account or one vendor reads as a group
 *   .tbl-wrap--scroll  on a desktop the wrapper takes the height and the
 *                      header stays while the rows scroll; not on a phone,
 *                      where a scroll inside a scroll is worse
 *
 * On a card list (.tbl without --ref) .trunc and .nowrap act at desktop width
 * only: below 600px a cell is a labelled row of its card and its value wraps.
 * Order matters as much as any style: the identity first, then the answer,
 * then how it was reached, then the reference columns. pricing.html's two
 * tables are the worked example.
 * ---------------------------------------------------------------------- */
.tbl--ref td { white-space: nowrap; }
.tbl--ref tbody tr:nth-child(even) td { background: color-mix(in srgb, var(--c-surface-2) 55%, var(--c-surface)); }
.tbl--ref tbody tr:hover td { background: var(--c-surface-2); }
.tbl td.lead { font-weight: 600; }
.tbl td.sub { color: var(--c-ink-2); }
.tbl td.same, .tbl td.same a { color: var(--c-ink-3); }
.tbl td.same a { text-decoration-color: transparent; }
.tbl .src { margin-left: var(--s4); font-size: var(--t4); color: var(--c-ink-3); }
.tbl--ref td.trunc { max-width: 16rem; overflow: hidden; text-overflow: ellipsis; }
.tbl--ref td.trunc-s { max-width: 10rem; overflow: hidden; text-overflow: ellipsis; }
@media (min-width: 601px) {
  .tbl td.nowrap { white-space: nowrap; }
  .tbl td.trunc { max-width: 16rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .tbl-wrap--scroll { max-height: 72vh; overflow: auto; }
  .tbl-wrap--scroll .tbl thead th { position: sticky; top: 0; z-index: 2; }
  .tbl-wrap--scroll .tbl thead th:first-child { z-index: 3; }
}
@media (max-width: 600px) {
  /* On a phone a reference table's long text is cut shorter, so the identity
     and the answer are on the first screen and only the reference columns sit
     behind the sideways scroll. */
  .tbl--ref td.trunc { max-width: 8.5rem; }
  .tbl--ref td.trunc-s { max-width: 7rem; }
  /* .trunc--narrow: the cell that gives way so the answer fits, on a phone
     only (pricing.html's description, so the price is on the first screen). */
  .tbl--ref td.trunc.trunc--narrow { max-width: 5.5rem; }
  .tbl--ref td, .tbl--ref th { padding-left: var(--s8); padding-right: var(--s8); }
}
/* A CHANGE IS COLOURED BY ITS DIRECTION, decided 2026-10-16 after a trial
   on index.html: better in the active green, worse in the lost red, no change
   in grey, and always at the figure weight with its sign, so it never rests
   on colour alone. Better and worse are the reader's, not the arithmetic's:
   revenue up is better, money billed under the rules is worse. Colour on a
   change and on a state badge is the only state colour off the badges. */
.delta { font-weight: 600; font-variant-numeric: tabular-nums; }
.delta--up { color: var(--c-state-active); }
.delta--down { color: var(--c-state-lost); }
.delta--flat { color: var(--c-ink-3); }

/* .field sets display, which beats the hidden attribute, so say it outright.
   quote.html's Price level showed beside an existing account without it; the
   Tool Kit's Back in Stock records the same trap. */
.field[hidden] { display: none; }

/* Prose stops at a readable width on a wide screen. */
.count, .info-body { max-width: 78ch; }

/* -------------------------------------------------------------------------
 * Buttons. Same height and radius as the inputs, so a control row is level.
 * ---------------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s8);
  height: var(--control-h); padding: 0 var(--s16);
  border: 1px solid var(--c-border-strong); border-radius: var(--radius);
  background: var(--c-surface); color: var(--c-ink);
  font-family: inherit; font-size: var(--t3); font-weight: 500;
  cursor: pointer; white-space: nowrap;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}
.btn:hover, .btn.is-hover { background: var(--c-surface-2); border-color: var(--c-ink-3); }
.btn:focus-visible, .btn.is-focus {
  outline: none; border-color: var(--c-accent);
  box-shadow: 0 0 0 2px var(--c-focus);
}
.btn:active, .btn.is-active { transform: translateY(1px); background: var(--c-surface-2); }
/* A BUTTON THAT IS A TOGGLE, held down. `aria-pressed` is the state, and
   the style reads off it rather than off a class, so the thing a screen
   reader announces and the thing an eye sees cannot disagree. Ink rather
   than accent: the accent is reserved for the one primary action in a
   view, and a scope toggle is not it. */
.btn[aria-pressed="true"] {
  background: var(--c-ink); border-color: var(--c-ink); color: var(--c-surface);
}
.btn[aria-pressed="true"]:hover, .btn[aria-pressed="true"].is-hover {
  background: var(--c-ink-2); border-color: var(--c-ink-2);
}
.btn:disabled, .btn.is-disabled {
  opacity: 0.5; cursor: not-allowed; transform: none;
  background: var(--c-surface-2); border-color: var(--c-border); color: var(--c-ink-3);
}

/* The one primary action per view. */
.btn--primary {
  background: var(--c-accent-solid); border-color: var(--c-accent-solid); color: var(--c-on-accent);
}
.btn--primary:hover, .btn--primary.is-hover {
  background: var(--c-accent-hover); border-color: var(--c-accent-hover);
}
.btn--primary:active, .btn--primary.is-active { background: var(--c-accent-hover); }
.btn--primary:disabled, .btn--primary.is-disabled {
  background: var(--c-accent-solid); border-color: var(--c-accent-solid); color: var(--c-on-accent);
}

/* A DESTRUCTIVE ACTION: one with no undo. The danger tint the danger banner
   and badge use, so it reads as a warning and never as the primary action;
   the accent stays with the one thing a rep is meant to do. The focus ring is
   the accent like every other control, because focus is a property of the
   keyboard, not of the action. prospect.html's "Delete this prospect". */
.btn--danger {
  background: var(--c-danger-bg); border-color: var(--c-danger-border); color: var(--c-danger-ink);
}
.btn--danger:hover, .btn--danger.is-hover {
  background: var(--c-danger-bg); border-color: var(--c-danger-ink);
}
.btn--danger:active, .btn--danger.is-active { background: var(--c-danger-border); }
.btn--danger:disabled, .btn--danger.is-disabled {
  background: var(--c-danger-bg); border-color: var(--c-danger-border); color: var(--c-danger-ink);
}

/* -------------------------------------------------------------------------
 * Form controls. Nothing native is left unstyled.
 * ---------------------------------------------------------------------- */
.field { display: block; }
.field-label {
  display: block; margin-bottom: var(--s4);
  font-size: var(--t5); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em; color: var(--c-ink-3);
}
.field-hint { margin: var(--s4) 0 0; font-size: var(--t4); color: var(--c-ink-3); }

.input, .select {
  width: 100%; height: var(--control-h);
  padding: 0 var(--control-px);
  border: 1px solid var(--c-border-strong); border-radius: var(--radius);
  background: var(--c-input-bg); color: var(--c-ink);
  font-family: inherit; font-size: var(--t3); line-height: normal;
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.input::placeholder { color: var(--c-ink-3); opacity: 1; }
/* A TEXTAREA IS THE TEXT INPUT, TALLER. Same border, radius, padding, focus
   and disabled states; it gives up the fixed 40px for a two control minimum
   and grows by hand. opening-order.html's addresses and notes are the users. */
textarea.input {
  height: auto; min-height: calc(var(--control-h) * 2);
  padding: var(--s8) var(--control-px); line-height: var(--lh-body);
  resize: vertical;
}
.input:hover, .input.is-hover, .select:hover, .select.is-hover { border-color: var(--c-ink-3); }
.input:focus-visible, .input.is-focus, .select:focus-visible, .select.is-focus {
  outline: none; border-color: var(--c-accent);
  box-shadow: 0 0 0 2px var(--c-focus);
}
/* A REQUIRED FIELD LEFT EMPTY. opening-order.html's validate() adds `.invalid`
   to each empty [required] control and removes it on the next keystroke, so
   the state is spelled with the script's class. The danger ink, which is 3:1
   and more on white, so the edge still identifies the control. */
.input.invalid, .select.invalid { border-color: var(--c-danger-ink); }
.input:disabled, .input.is-disabled, .select:disabled, .select.is-disabled {
  background: var(--c-surface-2); color: var(--c-ink-3);
  border-color: var(--c-border); cursor: not-allowed;
}

/* The select loses its native chrome and gets the chevron back as a real icon,
   positioned by the wrapper, so it matches the input exactly at every state. */
.select {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  padding-right: var(--s32);
}
.select::-ms-expand { display: none; }
.select-wrap { position: relative; display: block; }
.select-wrap .chev {
  position: absolute; right: var(--s12); top: 50%; transform: translateY(-50%);
  color: var(--c-ink-3); pointer-events: none;
}

/* -------------------------------------------------------------------------
 * Scope pill. A select that reads as the answer it currently gives.
 *
 * WHY THIS IS NOT THE 40px SELECT ABOVE. A page-level scope control is not a
 * field a rep fills in, it is a statement about everything underneath it, and
 * as a labelled full width select it took a label row, a control row and a
 * sentence row of the first screen to say one thing. The pill says it in one
 * line beside the page title: what you are looking at, how many of them there
 * are, and a chevron that says it can be changed.
 *
 * THE LABEL IS THE PROTECTED CLAIM, NOT A CAPTION NEXT TO ONE. A narrowed
 * book and the whole company look identical, so the sentence saying which is
 * the only thing standing between the two, and recovery.html shipped one
 * reading "Showing all accounts" over a list of 309 of 595. Here the claim IS
 * the control's face, painted from the control's own selected option, so
 * there is no second string that can fall out of step with it.
 *
 * THE NATIVE SELECT IS STILL THE CONTROL. It is laid over the pill at zero
 * opacity, so the options, the keyboard, and the phone's own picker are the
 * browser's rather than a menu we would have to build and get wrong. Nothing
 * here changes what it does.
 * ---------------------------------------------------------------------- */
.pill {
  position: relative;
  display: inline-flex; align-items: center; gap: var(--s4);
  padding: var(--s4) var(--s8) var(--s4) var(--s12);
  border: 1px solid var(--c-border-strong); border-radius: var(--radius);
  background: var(--c-surface);
  transition: border-color var(--dur) var(--ease);
}
/* 600, NOT 500, and ink. The nav links beside it are 12px/500 ink, and the
   type check reads this label as static text because the control covering it
   is a sibling rather than an ancestor, so at 500 it would share a tuple with
   every link on the page. The weight is what separates the page's own claim
   about itself from the row of destinations above it. */
.pill-label { font-size: var(--t4); font-weight: 600; color: var(--c-ink); }
.pill > .chev { color: var(--c-ink-3); }
.pill:hover, .pill.is-hover { border-color: var(--c-ink-3); }
.pill > select {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; padding: 0; border: 0; opacity: 0;
  font: inherit; cursor: pointer;
}
.pill:has(> select:focus-visible), .pill.is-focus {
  outline: 2px solid var(--c-accent); outline-offset: 1px;
}
/* THE SAME CLAIM WITH NOTHING TO CHANGE. A rep whose row level security
   already answers the question has one scope and no control, and a pill that
   looks tappable and is not is worse than a plain statement. It keeps the
   shape so the claim sits in the same place on both pages, and loses the
   chevron, the control border and the pointer, which are the three things
   that said it could be opened. */
.pill--static {
  border-color: var(--c-border); background: var(--c-surface-2); cursor: default;
}
.pill--static:hover { border-color: var(--c-border); }

/* -------------------------------------------------------------------------
 * Info toggle. A collapsed explanatory paragraph behind an (i).
 *
 * THIS IS THE PATTERN THAT TAKES 527 WORDS OFF THE PHONE VIEW. The prose is not
 * deleted, which would take the reasoning with it. It is folded, so the number
 * is what the page leads with and the explanation is one tap away.
 * ---------------------------------------------------------------------- */
.info { margin: 0; }
.info > summary {
  display: inline-flex; align-items: center; gap: var(--s4);
  list-style: none; cursor: pointer; user-select: none;
  padding: var(--s4) var(--s8); margin-left: calc(var(--s8) * -1);
  border-radius: var(--radius);
  /* INK AND 500, NEVER MUTED. This was --c-ink-3 at weight 400, which is the
     footnote tier, so every info toggle on a page read as a caption that
     happened to be clickable. Interactive text carries ink and at least 500,
     which is what keeps its (size, weight, colour) out of the body tier's. */
  font-size: var(--t4); font-weight: 500; color: var(--c-ink);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
/* THE GLYPH TAKES THE ACCENT. The (i) is the affordance, so it is the one
   place the accent appears outside a primary action: at 14px it is a hint
   rather than a second call to action, and it is what a rep learns to scan
   for. The chevron on a collapsible section is the same signal. */
.info > summary > i, .info > summary > svg { color: var(--c-accent); }
.info > summary::-webkit-details-marker { display: none; }
.info > summary:hover, .info > summary.is-hover { background: var(--c-surface-2); color: var(--c-ink); }
.info > summary:focus-visible, .info > summary.is-focus {
  outline: 2px solid var(--c-accent); outline-offset: 1px;
}
.info > summary:active, .info > summary.is-active { background: var(--c-surface-2); }
/* The verb is the tappable half of the label and reads as the control, so it
   is underlined rather than left to look like more prose. */
.info > summary .info-verb {
  text-decoration: underline; text-decoration-color: var(--c-border-strong);
  text-underline-offset: 3px;
}
.info > summary:hover .info-verb { text-decoration-color: var(--c-accent); }
.info[open] > summary .info-verb::after { content: "Hide"; }
.info > summary .info-verb::after { content: "Show"; }
.info-body {
  margin: var(--s8) 0 0; padding: var(--s12);
  border: 1px solid var(--c-border); border-radius: var(--radius);
  background: var(--c-surface-2);
  font-size: var(--t4); line-height: var(--lh-body); color: var(--c-ink-2);
  max-width: 68ch;
}

/* -------------------------------------------------------------------------
 * Sheet furniture. Only used to lay the specimens out, never a component.
 * ---------------------------------------------------------------------- */
.group-title {
  font-family: var(--font-display);
  font-size: var(--t2); font-weight: 600; letter-spacing: -0.01em;
  margin: 0 0 var(--s4);
}
.spec { margin-bottom: var(--s24); }
.spec:last-child { margin-bottom: 0; }
.spec-note { margin: 0 0 var(--s12); font-size: var(--t4); color: var(--c-ink-3); max-width: 68ch; }
.row { display: flex; flex-wrap: wrap; gap: var(--s8); align-items: center; }
.row--16 { gap: var(--s16); }
/* A field that takes the leftover width of its row, for the one control a
   row is built around: a search box beside a toggle. 14rem is a floor, not
   a width, so the row still wraps to one control per line on a phone
   instead of squeezing a search box down to nothing. */
.field--grow { flex: 1 1 14rem; }
.stack { display: flex; flex-direction: column; gap: var(--s8); }
.grid-2 { display: grid; gap: var(--s16); grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.state-tag {
  font-size: var(--t5); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--c-ink-3);
  min-width: 76px; display: inline-block;
}

/* Foundation specimens. */
.scale-row { display: flex; align-items: center; gap: var(--s12); margin-bottom: var(--s4); }
/* Ink, not accent. The accent is reserved for the one primary action per view,
   and seven magenta bars in a spacing specimen spends it on decoration, which
   is the rule this sheet exists to demonstrate. */
.scale-bar { height: var(--s8); background: var(--c-ink-3); border-radius: 2px; }
.scale-name { font-size: var(--t5); color: var(--c-ink-3); min-width: 56px; font-variant-numeric: tabular-nums; }
.swatch {
  display: flex; align-items: center; gap: var(--s8);
  font-size: var(--t4); color: var(--c-ink-2);
}
.swatch .chip {
  width: var(--s24); height: var(--s24); border-radius: var(--radius);
  border: 1px solid var(--c-border); flex: none;
}
.type-row { display: flex; align-items: baseline; gap: var(--s16); margin-bottom: var(--s8); }
.type-row .tag { font-size: var(--t5); color: var(--c-ink-3); min-width: 96px; }
.radius-demo { display: flex; gap: var(--s8); align-items: center; }
.radius-box {
  width: var(--s48); height: var(--s32);
  border: 1px solid var(--c-border-strong); border-radius: var(--radius);
  background: var(--c-surface-2);
}
/* A stand-in for a basemap tile, so the map markers are specimened on
   something other than the card they would never be drawn on. --c-surface-2
   is not what OSM paints, and nothing here pretends it is: the point of the
   band is that the ground is light and unknown, which is the property every
   marker rule below has to survive. */
.marker-ground {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s16) var(--s24);
  padding: var(--s16);
  background: var(--c-surface-2);
  border: 1px solid var(--c-border); border-radius: var(--radius);
}
.marker-spec {
  display: inline-flex; align-items: center; gap: var(--s8);
  font-size: var(--t4); color: var(--c-ink-2);
}
/* The box Leaflet's divIcon gives a marker, at the size the page sets inline.
   .pk sizes itself at 100% of it, which is why the prospect needs a wrapper
   on the sheet and the circles do not. */
.marker-box { display: block; flex: none; }

/* -------------------------------------------------------------------------
 * Collapsible section. A section header that folds, carrying its headline
 * number in the summary.
 *
 * THE HEADLINE IS NOT DECORATION. Several of the protected elements in
 * .claude/agents/qa.md are counts, and that document says a protected element
 * may never be collapsed by default. Putting the count in the summary is how a
 * section folds without hiding one: the number stays on screen at all times and
 * only the working detail moves behind the disclosure.
 * ---------------------------------------------------------------------- */
.fold { border-bottom: 1px solid var(--c-border); margin-top: var(--s32); }
/* Folds in a row (account.html's 05 to 09) stay one compact list. */
.fold + .fold { margin-top: 0; }
@media (max-width: 600px) { .fold { margin-top: var(--s16); } }
.fold > summary {
  display: flex; align-items: baseline; gap: var(--s8);
  list-style: none; cursor: pointer; user-select: none;
  padding: var(--s16) 0;
  /* The same heading as .sec (see above): a section you can open is the same
     kind of thing as one that is open. Ink, as interactive text. */
  font-family: var(--font-display); font-size: var(--t2); font-weight: 600;
  text-transform: none; letter-spacing: -0.01em; color: var(--c-ink);
  transition: color var(--dur) var(--ease);
}
.fold > summary::-webkit-details-marker { display: none; }
.fold > summary:hover, .fold > summary.is-hover { color: var(--c-ink); }
.fold > summary:focus-visible, .fold > summary.is-focus {
  outline: 2px solid var(--c-accent); outline-offset: 2px; border-radius: var(--radius);
}
.fold > summary:active, .fold > summary.is-active { color: var(--c-ink); }
.fold .ord { color: var(--c-ordinal); font-variant-numeric: tabular-nums; }
/* A SECTION HEADER THAT DOES NOT FOLD MAY STILL CARRY ITS HEADLINE NUMBER,
   and it is the same element with the same treatment. The rank is a property
   of "this is the number the section is about", not of the disclosure, and a
   second spelling for the non-folding case is how the two would end up a size
   apart. index.html's 02 REMINDERS carries its open count this way: the
   section is a short list that never folds, and the count is protected. */
.fold .headline, .sec .headline {
  margin-left: auto; font-family: var(--font-ui); font-size: var(--t3); font-weight: 600;
  text-transform: none; letter-spacing: 0; color: var(--c-ink);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* THE CHEVRON IS SHARED WITH THE DISCLOSURE BUTTON BELOW, one declaration and
   two users. A disclosure whose summary says what it will DO ("+ Add a note")
   needs no open/shut indicator; one that says what it HOLDS ("Filters") does,
   because the same label is correct in both states and nothing else in the
   control says which state it is in. */
.fold .chev, .disclose > summary .chev {
  color: var(--c-accent); align-self: center; transition: transform var(--dur) var(--ease);
}
.fold[open] > summary .chev, .disclose[open] > summary .chev { transform: rotate(180deg); }
.fold-body { padding: 0 0 var(--s16); }

/* -------------------------------------------------------------------------
 * Disclosure button. A compose form folded behind "+ Add".
 *
 * The summary carries `class="btn"` rather than restating the button rules, so
 * a disclosure and a button cannot drift apart. Only the marker is removed.
 * ---------------------------------------------------------------------- */
summary.btn { list-style: none; }
summary.btn::-webkit-details-marker { display: none; }
.disclose > .disclose-body { margin-top: var(--s12); }

/* -------------------------------------------------------------------------
 * Bar chart. 24 months, one bar each.
 *
 * An empty month is a flat baseline rather than a missing bar, so a gap in
 * trading cannot be mistaken for the edge of the chart.
 * ---------------------------------------------------------------------- */
.chart { display: flex; align-items: flex-end; gap: var(--s4); height: var(--chart-h); }
.chart .bar {
  flex: 1 1 0; min-height: 2px; background: var(--c-ink-3); border-radius: 2px 2px 0 0;
}
.chart .bar.empty { height: 2px; background: var(--c-border); }
.chart .bar.credit { background: var(--c-state-lost); }
.axis {
  display: flex; justify-content: space-between; gap: var(--s8);
  margin-top: var(--s4); font-size: var(--t5); color: var(--c-ink-3);
}

/* -------------------------------------------------------------------------
 * Entry list. One note or one logged activity.
 * ---------------------------------------------------------------------- */
.entry { padding: var(--s12) 0; border-bottom: 1px solid var(--c-border); }
.entry:last-child { border-bottom: 0; }
.entry-meta {
  display: flex; flex-wrap: wrap; gap: var(--s8); align-items: baseline;
  margin-bottom: var(--s4);
  font-size: var(--t5); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--c-ink-3);
}
.entry-body { margin: 0; font-size: var(--t3); color: var(--c-ink); }
/* An empty state is a footnote about the data, not body copy: one size below
   body, muted, per the tier rule above. */
.entry-empty, .drafts-empty, .ac-no-results { margin: var(--s12) 0; font-size: var(--t4); font-weight: 400; color: var(--c-ink-3); }

/* -------------------------------------------------------------------------
 * Contact line. The compact header block, with the phone tappable.
 *
 * Phone and email are ink rather than accent, and carry a rule underneath
 * instead of a colour, so a header holding three links does not spend the one
 * accent this design allows per view.
 * ---------------------------------------------------------------------- */
.idline {
  display: flex; flex-wrap: wrap; gap: var(--s4) var(--s16);
  margin: var(--s8) 0 0; font-size: var(--t3);
}
.idline > span { display: inline-flex; align-items: baseline; gap: var(--s4); }
.idline .k {
  font-size: var(--t5); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--c-ink-3);
}
.idline a {
  color: var(--c-ink); font-weight: 500; text-decoration: none;
  border-bottom: 1px solid var(--c-border-strong);
  transition: border-color var(--dur) var(--ease);
}
.idline a:hover, .idline a.is-hover { border-bottom-color: var(--c-accent); }
.idline a:focus-visible, .idline a.is-focus {
  outline: 2px solid var(--c-accent); outline-offset: 2px; border-radius: 2px;
}
.idline a:active, .idline a.is-active { border-bottom-color: var(--c-accent); }

/* -------------------------------------------------------------------------
 * Top bar. The nav and the session control, ABOVE the page title.
 *
 * WHY IT IS A COMPONENT AND NOT TWO ELEMENTS SIDE BY SIDE. `nav.js` injects its
 * own `.hubnav` rules, including a bottom border and a bottom margin, and that
 * module is data code a design pass may not edit. Dropped next to a button the
 * border would run under the links and stop, which reads as a mistake. So the
 * bar owns the rule and the nav inside it gives its own up.
 *
 * BELOW 600px THE WHOLE BAR IS ONE ROW THAT SCROLLS SIDEWAYS, sign out
 * included. The rule used to be the opposite here (nav on its own full width
 * line, button pushed below it), and that cost three rows of identical
 * furniture standing above the one thing on a page that differs. It lived in
 * account.html's own <style> with a note saying it must move here the moment a
 * second page wanted it. recovery.html is that second page, so it has moved,
 * and the page copy is gone rather than duplicated. See the phone block at the
 * foot of this file for the rules themselves and for why the axis is left free.
 * ---------------------------------------------------------------------- */
.topbar {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s8) var(--s12);
  padding-bottom: var(--s8); margin-bottom: var(--s8);
  border-bottom: 1px solid var(--c-border);
}
.topbar > .nav, .topbar > .hubnav {
  flex: 1 1 200px; margin: 0; padding: 0; border-bottom: 0;
}
.topbar > .btn { flex: none; }
/* nav.js gives the back-link a -0.75rem top margin so it tucks under the nav's
   own bottom margin. The bar has no bottom margin to tuck into, so the pull is
   undone here rather than in the module. */
.topbar + .hubnav-back { margin-top: var(--s8); }

/* -------------------------------------------------------------------------
 * Contact card. Two groups of labelled rows: who to call, and what the record
 * says. Side by side where there is room, stacked below 600px.
 *
 * EVERY ROW IS 44px AND THAT IS THE WHOLE POINT. A phone number a rep cannot
 * reliably hit with a thumb is a phone number they read out and dial by hand,
 * which is the thing this card exists to remove. The dialable value fills its
 * row rather than being the height of its glyphs, so the target is the row.
 *
 * A ROW WITH NO VALUE IS NOT RENDERED. There is no empty state per row: the
 * page omits the row. What it does not omit is a recorded absence ("none on
 * file", "not geocoded"), which is a fact about the record rather than a gap
 * in the card.
 * ---------------------------------------------------------------------- */
.contact-card {
  display: grid; gap: var(--s12) var(--s32);
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  padding: var(--s12) var(--s16);
  background: var(--c-surface);
  border: 1px solid var(--c-border); border-radius: var(--radius);
  box-shadow: var(--shadow);
}
/* THE GROUP TITLE AND THE ROW LABEL WERE ONE TREATMENT, and at 390 the
   contact card read as a flat list of nine labels with no grouping in it.
   Both were 11px/600 muted small caps, which is the label tier, so a title
   naming a group and a label naming one row inside it had the same rank. The
   title is 12px ink; the labels underneath stay 11px muted. The tuple check
   cannot see a collision like this, because both sides are static, which is
   why it is written down here rather than left to the harness. */
.cc-title {
  margin: 0; padding: var(--s4) 0;
  font-size: var(--t4); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em; color: var(--c-ink);
}
/* contact.js writes `.c-block` and the account page keeps that class on the
   contact group, because scripts/screenshots.mjs matches the stored phone and
   contact name against `#meta .wide .c-block`. `.wide` and `.c-block` carry no
   box of their own here; the rows are laid out by `.cc-rows`. */
.contact-card .wide { display: contents; }
.cc-rows { display: flex; flex-direction: column; gap: 0; }
.cc-row {
  display: flex; align-items: center; gap: var(--s8);
  min-height: var(--row-h); margin: 0;
  font-size: var(--t3); color: var(--c-ink);
}
.cc-row + .cc-row { border-top: 1px solid var(--c-border); }
.cc-row > .icon { color: var(--c-ink-3); }
.cc-k {
  flex: none;
  font-size: var(--t5); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--c-ink-3);
}
.cc-v { min-width: 0; color: var(--c-ink); overflow-wrap: anywhere; }
/* A dialable or mappable value fills the row's full 44px, so the hit target is
   the row and not the text. The rule sits under the text rather than on the
   box's edge for the same reason: a border-bottom on a 44px inline-flex draws
   44px below the baseline and reads as a divider. */
a.cc-v {
  display: inline-flex; align-items: center; min-height: var(--row-h);
  color: var(--c-ink); font-weight: 500; text-decoration: underline;
  text-decoration-color: var(--c-border-strong); text-underline-offset: 3px;
  transition: text-decoration-color var(--dur) var(--ease);
}
a.cc-v:hover, a.cc-v.is-hover { text-decoration-color: var(--c-accent); }
a.cc-v:focus-visible, a.cc-v.is-focus {
  outline: 2px solid var(--c-accent); outline-offset: 2px; border-radius: 2px;
}
/* A value that is not a phone number is not a link. `phone_alt` style free text
   ("301-428-0444 / 9-5", "gate code 7815") still has to be readable, so it is
   rendered plainly rather than dropped or guessed at. */
.cc-why, .cc-none { font-size: var(--t5); color: var(--c-ink-3); }

/* THE PHONE FOLD. Seven rows is the right card on a desktop and too long on a
   phone, where it stood between the account name and the four KPI cards that
   decide whether to call: all four sat below 844px, so the first screen was
   the record rather than the numbers. Below 600px the card shows what a rep
   acts on (the store line, the named contact, the address) and folds the rest
   behind one More row.

   IT IS A CLASS AND NOT A <details>, WHICH IS THE WHOLE TRICK. A closed
   <details> is closed at every width, so at 1280, where there is room for all
   seven rows, the desktop card would open with a tap it does not need. Every
   row is rendered and in flow at both widths; only the phone hides four of
   them, and only until the control is pressed. The consequence to know is
   that the folded rows are IN THE DOM at 390: display:none, so no harness
   reading text can tell they are not on screen, which is why nothing
   protected is allowed in here.

   ONE CONTROL, NOT ONE PER GROUP. The fold spans both groups (email is a
   contact detail, county, terms and rep are record fields), and a control per
   group costs two 44px rows out of the four it saves, which is not enough
   height to bring the fourth KPI card onto the first screen. It sits last, so
   everything it opens is above it in one direction, and it opens the whole
   rest of the record as one decision rather than making a rep guess which
   group hid the email. */
.cc-more {
  display: none; width: 100%; margin: 0; padding: 0;
  border: 0; background: none; text-align: left; cursor: pointer;
  font-family: inherit;
  /* Ink and 500, like every other control here. A More that reads in the
     label's muted grey is a footnote a rep can tap, which is the collision
     scripts/check-type-tuples.mjs exists to fail. */
  font-size: var(--t3); font-weight: 500; color: var(--c-ink);
}
/* The chevron takes the accent, the same signal the info toggle and the
   collapsible section use, and it turns over rather than being redrawn. */
.cc-more > .icon { color: var(--c-accent); transition: transform var(--dur) var(--ease); }
.contact-card.cc-open .cc-more > .icon { transform: rotate(180deg); }
/* Underlined, because the row it sits in is otherwise a row of plain facts. */
.cc-more-t {
  text-decoration: underline; text-decoration-color: var(--c-border-strong);
  text-underline-offset: 3px;
}
.cc-more:hover .cc-more-t, .cc-more.is-hover .cc-more-t {
  text-decoration-color: var(--c-accent);
}
.cc-more:focus-visible, .cc-more.is-focus {
  outline: 2px solid var(--c-accent); outline-offset: -2px;
  border-radius: var(--radius);
}
/* BOTH WORDS ARE IN THE MARKUP AND ONE IS HIDDEN, rather than one word in a
   ::after that CSS swaps. A pseudo-element carries no text node, so a label
   written that way is invisible to the tuple check, and a control that the
   hierarchy check cannot see is a control with no hierarchy claim on it. */
.contact-card:not(.cc-open) .cc-more-t--open,
.contact-card.cc-open .cc-more-t--shut { display: none; }

@media (max-width: 600px) {
  .contact-card:not(.cc-open) .cc-row.is-folded { display: none; }
  .cc-more { display: flex; align-items: center; gap: var(--s8); min-height: var(--row-h); }
}

/* -------------------------------------------------------------------------
 * List lead. A list a rep writes into, with its Add control beside it.
 *
 * WHY THE BUTTON IS NOT A ROW OF ITS OWN. An empty section read as a full width
 * Add button followed by a one line "nothing here yet", which spends a whole
 * row on a control nobody has decided to use. On one line it costs nothing when
 * the list is empty and stays exactly where it was when the list is full.
 *
 * The compose form takes the full width when it opens, because a 180px column
 * is not somewhere to type.
 * ---------------------------------------------------------------------- */
.list-lead {
  display: flex; flex-wrap: wrap; align-items: flex-start;
  gap: var(--s8) var(--s12);
}
.list-lead > .list-body { flex: 1 1 180px; min-width: 0; }
.list-lead > .disclose { flex: 0 0 auto; }
.list-lead > .disclose[open] { flex: 1 1 100%; }
/* One empty-state sentence sits level with the button rather than at the top of
   a 40px line. A list holding entries keeps the button at the top, which is
   where it was. */
.list-lead > .list-body > .note:only-child,
.list-lead > .list-body > .entry-empty:only-child {
  display: flex; align-items: center; min-height: var(--control-h); margin: 0;
}
.list-lead > .list-body > .grp { margin-bottom: var(--s8); }
.list-lead > .list-body > .grp:last-child { margin-bottom: 0; }

/* -------------------------------------------------------------------------
 * Segmented control. One choice with two or three answers, in one row, with
 * the chosen one held down.
 *
 * WHY IT IS NOT N BUTTONS WITH aria-pressed. `.btn[aria-pressed="true"]`
 * above is the right answer for a toggle that stands alone. A segment group
 * is ONE question, so the answers share a border and only the chosen one
 * fills: that is what says they are alternatives rather than three separate
 * things a reader may press in any combination.
 *
 * THE STATE CLASS IS `.on`, WHICH map.html ALREADY WRITES. That page sets it
 * with classList.toggle in its render pass, so the sheet meets the markup
 * where it is instead of asking a page's render code to rename a class for
 * the sake of a stylesheet.
 *
 * INK AND 500 ON EVERY SEGMENT, CHOSEN OR NOT, and this is the defect the
 * component was written for. The segments rendered in the lighter muted grey
 * on map.html, so ten controls read as captions, which rule 1 of section 22
 * of the sheet forbids and which scripts/check-type-tuples.mjs failed that
 * page on. Held down is INK rather than the accent, for the same reason
 * .btn's pressed state is: the accent is the one primary action in a view,
 * and choosing how the map draws is not it.
 * ---------------------------------------------------------------------- */
.seg {
  display: inline-flex; overflow: hidden;
  border: 1px solid var(--c-border-strong); border-radius: var(--radius);
}
.seg > button {
  flex: 1 1 auto; height: var(--control-h); padding: 0 var(--s12);
  border: 0; background: var(--c-surface); color: var(--c-ink);
  font-family: inherit; font-size: var(--t3); font-weight: 500;
  cursor: pointer; white-space: nowrap;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.seg > button + button { border-left: 1px solid var(--c-border-strong); }
.seg > button:hover, .seg > button.is-hover { background: var(--c-surface-2); }
.seg > button:focus-visible, .seg > button.is-focus {
  outline: 2px solid var(--c-accent); outline-offset: -2px;
}
.seg > button.on { background: var(--c-ink); color: var(--c-surface); }
.seg > button.on:hover, .seg > button.on.is-hover { background: var(--c-ink-2); }
.seg > button:disabled, .seg > button.is-disabled {
  opacity: 0.5; cursor: not-allowed; background: var(--c-surface-2);
}

/* -------------------------------------------------------------------------
 * Tabs with counts. One question with several answers, each answer carrying
 * how many rows it holds, and the counts are the point.
 *
 * WHY THIS IS NOT THE SEGMENTED CONTROL. `.seg` is one bordered strip that
 * never wraps, which is right for two or three short answers and wrong for
 * lost-lines.html's four: "Known discontinuation 115" and three siblings are
 * about 700px of text, so at 390 the strip either clips a protected count off
 * the right edge or scrolls it out of sight. These are separate bordered
 * buttons that wrap to a second row instead, so every count is on screen at
 * every width with no interaction, which is the rule in qa.md.
 *
 * THE COUNT IS INK AT 500, THE SAME TUPLE AS THE LABEL, deliberately. At 600
 * it would share (14px, 600, ink) with `.count b`, the static figure in the
 * count line directly beneath, and a control would read as a number you can
 * only look at. It is separated from the label by the gap and by tabular
 * figures, not by weight.
 *
 * HELD DOWN IS INK, NOT THE ACCENT, for the reason `.seg > button.on` gives:
 * the accent is the one primary action in a view and picking a bucket is not
 * it. The state class is `.on`, which the page's render pass already writes.
 * ---------------------------------------------------------------------- */
.tabs { display: flex; flex-wrap: wrap; gap: var(--s8); }
.tab {
  display: inline-flex; align-items: center; gap: var(--s8);
  height: var(--control-h); padding: 0 var(--s12);
  border: 1px solid var(--c-border-strong); border-radius: var(--radius);
  background: var(--c-surface); color: var(--c-ink);
  font-family: inherit; font-size: var(--t3); font-weight: 500;
  cursor: pointer; white-space: nowrap;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              color var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.tab .c { font-variant-numeric: tabular-nums; }
.tab:hover, .tab.is-hover { background: var(--c-surface-2); border-color: var(--c-ink-3); }
.tab:focus-visible, .tab.is-focus {
  outline: none; border-color: var(--c-accent);
  box-shadow: 0 0 0 2px var(--c-focus);
}
.tab:active, .tab.is-active { transform: translateY(1px); }
.tab.on { background: var(--c-ink); border-color: var(--c-ink); color: var(--c-surface); }
.tab.on:hover, .tab.on.is-hover { background: var(--c-ink-2); border-color: var(--c-ink-2); }
.tab:disabled, .tab.is-disabled {
  opacity: 0.5; cursor: not-allowed; transform: none;
  background: var(--c-surface-2); border-color: var(--c-border); color: var(--c-ink-3);
}

/* -------------------------------------------------------------------------
 * Checkbox row, and the facet it stacks into.
 *
 * A CHECKBOX LABEL IS INK, NOT MUTED, for the reason the segments are: it is
 * a control. It is the one control the type-tuple check cannot see, because
 * the label wraps the input rather than being one, so `closest()` finds no
 * interactive ancestor from the text node and the row is measured as static.
 * The rule still applies; only the enforcement is missing, which is worth
 * knowing when reading a green run.
 *
 * A FACET IS A PARTITION AND ITS ZERO ROWS STAY. Dimmed, never dropped: the
 * property that makes every count on the map page checkable is that each
 * facet's values sum to the loaded total, and a facet that hides the values
 * a current filter cannot reach stops being a partition without saying so.
 * ---------------------------------------------------------------------- */
.chk {
  display: inline-flex; align-items: center; gap: var(--s8);
  min-height: var(--control-h);
  font-size: var(--t3); color: var(--c-ink); cursor: pointer;
}
.chk input { margin: 0; flex: none; accent-color: var(--c-accent-solid); }
.chk input:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }

.facet { margin-bottom: var(--s16); }
.facet-h { display: flex; align-items: center; gap: var(--s8); margin-bottom: var(--s4); }
.facet-h .t {
  font-size: var(--t5); font-weight: 600; color: var(--c-ink-3);
  text-transform: uppercase; letter-spacing: 0.08em;
}
.facet-h .clr { margin-left: auto; }
.facet-search { margin-bottom: var(--s4); }
.facet-vals { max-height: 12rem; overflow-y: auto; }
.fv {
  display: flex; align-items: center; gap: var(--s8);
  min-height: var(--s24); padding: var(--s4);
  border-radius: var(--radius); cursor: pointer;
  font-size: var(--t3); color: var(--c-ink);
}
.fv:hover, .fv.is-hover { background: var(--c-surface-2); }
.fv input { margin: 0; flex: none; accent-color: var(--c-accent-solid); }
.fv .sw { width: var(--s8); height: var(--s8); border-radius: 50%; flex: none; }
.fv .lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fv .n {
  margin-left: auto; flex: none;
  font-size: var(--t4); color: var(--c-ink-3); font-variant-numeric: tabular-nums;
}
.fv.zero { opacity: 0.5; }

/* The palette name. Content comes from a token, so naming the palette does not
   cost the two sheets their byte identity outside the :root block. */
.palette {
  display: inline-flex; align-items: center;
  height: var(--s24); padding: 0 var(--s8);
  border: 1px solid var(--c-border); border-radius: var(--radius);
  background: var(--c-surface-2); color: var(--c-ink-2);
  font-size: var(--t5); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em;
}
.palette::after { content: var(--palette-label); }

/* -------------------------------------------------------------------------
 * MAP MARKERS.
 *
 * The eight things the customer map can draw: the five account states, a
 * geocode that needs review, a prospect, and a cluster. They live here rather
 * than in map.html's own <style> for one reason: components.html links this
 * file and nothing else, so a treatment defined in a page's inline block is a
 * treatment no sheet renders, no screenshot photographs and no contrast check
 * can ever reach. Before this they were page-local, drawn against an INVERTED
 * basemap, and the only place any of them had ever been seen at both widths
 * was inside the map page's own legend.
 *
 * THESE ARE DRAWN AGAINST A LIGHT BASEMAP NOW, and that is the whole reason
 * every value below changed. Standard OSM raster is near white with grey
 * roads, pale green parks and pale blue water, so the ground under a marker
 * is light but NOT a known colour. A fill therefore cannot be relied on to
 * separate a marker from what is behind it. The ink ring does that, at one
 * width on every marker, and the fill is left to carry which state it is.
 * That is why every rule here puts its contrast in the border and not in the
 * background.
 *
 * STATE FILLS ARE THE SAME --c-state-* THE BADGES USE, so an account that
 * reads `slipping` in a list is the same colour on the map. map.html does NOT
 * yet honour this: its STATE_COLOR const still holds the old dark-palette
 * values (#4ea36a, #d9a520, #f48323, #ff8a8f, #6e6a78), it is JS rather than
 * CSS, and a design pass may not edit it. Until that one const moves, the
 * sheet is the definition and the page is one literal behind it.
 * ---------------------------------------------------------------------- */

/* An account. A circle in both draw modes, at every zoom, so shape alone
   separates it from a prospect. Size is set inline by the page: 11px in state
   mode, and by revenue in revenue mode. */
.mk {
  display: block;
  border-radius: 50%;
  border: 1.5px solid var(--c-ink);
  box-sizing: border-box;
}

/* A coordinate the geocoder is not confident about.

   IT WAS A WHITE DASHED RING AND THAT ONLY WORKED ON THE INVERTED BASEMAP,
   where an ordinary marker's dark ring vanished into the ground and this one
   was the only edge visible. On the light basemap every marker has a visible
   dark ring, so "dashed instead of solid" is no longer a difference: this was
   built as a 2px dashed ink border first and photographed, and at 11px the
   dashes closed up into what read as an ordinary marker with a slightly
   heavier edge. Eleven pixels is not enough circumference for a dash pattern
   to survive, and the marker size is set inline by the page.

   So the state is carried OUTSIDE the marker instead, as a second ring with a
   white gap, which reads as a target at any size and is 17px against 11px
   before the eye has resolved either ring. The dashed border stays because it
   still reads at legend size, where the swatch has no fill. */
.mk.review {
  border-style: dashed;
  border-width: 2px;
  box-shadow: 0 0 0 1px #fff, 0 0 0 3px var(--c-ink);
}

/* A prospect. A DIAMOND in both draw modes and at a constant size, because
   the failure mode is a rep driving to a lead believing it is a customer, and
   a diamond is still a diamond at every zoom and inside a cluster. Colour is
   the second cue and never the only one. */
.pk {
  display: block;
  width: 100%; height: 100%;
  background: var(--c-prospect);
  border: 1.5px solid var(--c-ink);
  box-sizing: border-box;
  transform: rotate(45deg);
  border-radius: 2px;
}

/* A cluster. Ink rather than the violet it used to be: that violet was a
   sixth colour on a page already spending five on states and one on
   prospects, and a count badge is structure rather than a fact about an
   account, so it takes the structure colour. The white text needs 4.5:1 and
   gets 8.8:1 here; the old translucent violet gave 5.79 and the old stack
   magenta gave 3.89, which failed.

   THE RING AND THE HALO BOTH FLIPPED. The ring was near white at 55% alpha,
   which measures 1.06:1 on a white basemap, so on a light ground it was not a
   ring at all. It is white and opaque now, to hold the disc off the markers
   underneath it, and the definition against the tiles moved into the first
   layer of the shadow. The halo that was a violet glow is a soft ink one. */
.cl {
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  font-size: var(--t4); font-weight: 600; font-variant-numeric: tabular-nums;
  color: #fff;
  background: var(--c-ink-2);
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px rgba(20, 22, 26, 0.35), 0 0 0 4px rgba(20, 22, 26, 0.10);
}

/* Every account in the cluster is at ONE coordinate, so zooming will never
   separate them and clicking opens a list instead. The accent carries it, as
   it did before, at the palette's solid value rather than the brand one: the
   brand #eb008b under white text is 3.89:1 and fails, #c2007a is 5.87:1. */
.cl.stack {
  background: var(--c-accent-solid);
  box-shadow: 0 0 0 1px rgba(20, 22, 26, 0.35), 0 0 0 4px rgba(194, 0, 122, 0.18);
}

/* -------------------------------------------------------------------------
 * SHARED-MODULE MARKUP BRIDGE.
 *
 * WHY THIS BLOCK EXISTS, and it is the same argument as the token aliases near
 * the top of this file. nav.js, contact.js, entries.js, assets.js and rows.js
 * write their own markup with their own class names, and those modules are
 * data code that a design pass may not edit. So a converted page holds two
 * vocabularies: the one on components.html, and the one the modules emit.
 * Unbridged, the second renders as browser defaults (an unstyled textarea, a
 * grey-on-grey banner, a bare span where a badge should be), which is a page
 * half converted and reads as a bug rather than as a missing rule.
 *
 * Every selector here is a class a module actually emits. Grep before adding
 * one, and delete one only when nothing in public/shared/ writes it any more.
 *
 * THE MODULE NAME IS THE ALIAS, THE COMPONENT ABOVE IS THE DEFINITION. Where a
 * module name maps onto a component this sheet already has, it points at the
 * same tokens rather than restating a second set of values, so the two cannot
 * drift into looking nearly the same.
 * ---------------------------------------------------------------------- */

/* An empty state or a loading line from a module: a sentence, not prose.
   entries.js, assets.js and interests.js all write `<p class="note">` for
   these, and every one of them is an empty state, a loading line or a
   sub-line annotation on an entry.

   IT IS THE FOOTNOTE TIER, SO IT IS SIZED AS ONE. This was t3, body size, in
   the muted colour, which is the exact "neither tier" shape the three tiers
   above exist to remove: body size says content and the muted colour says
   qualifier, so "No notes on this account yet." sat at the same rank as a
   note somebody had actually written. `.entry-empty` next to it was already
   t4 muted, so the two answers to one question disagreed inside this file. */
.note {
  margin: var(--s12) 0;
  font-size: var(--t4); font-weight: 400; color: var(--c-ink-3);
}
/* `.faint` is interests.js's name for the same thing (the picker's "Added."
   readback), so it shares the declaration. */
.dim, .faint { color: var(--c-ink-3); }

/* Banners. The modules spell the variants `err` and `warn`; this sheet spells
   them `banner--danger` and `banner--warn`. One component, two spellings. */
.banner.info { border-left-color: var(--c-border-strong); }
.banner.warn {
  background: var(--c-warn-bg); border-color: var(--c-warn-border);
  border-left-color: var(--c-warn-ink); color: var(--c-warn-ink);
}
.banner.warn strong { color: var(--c-warn-ink); }
.banner.err {
  background: var(--c-danger-bg); border-color: var(--c-danger-border);
  border-left-color: var(--c-danger-ink); color: var(--c-danger-ink);
}
.banner.err strong { color: var(--c-danger-ink); }

/* Badges. entries.js writes `badge sm none` and `badge sm slipping`; the `sm`
   modifier is the module's, and it is honoured rather than flattened because a
   badge sitting inside a table cell or an entry head genuinely wants to be
   smaller than one sitting on a card. assets.js writes `stage`, which is the
   same component under a different name. */
.badge.sm, .stage {
  height: 20px; padding: 0 var(--s4); letter-spacing: 0.05em;
}
.stage {
  display: inline-flex; align-items: center; gap: var(--s4);
  border: 1px solid var(--c-border); border-radius: var(--radius);
  background: var(--c-surface-2); color: var(--c-ink-2);
  font-size: var(--t5); font-weight: 600;
  text-transform: uppercase; white-space: nowrap;
}
/* Tinted, not dotted: these carry no dot element, so the tint is the only
   channel available and a flag tint is the right one for both. */
.badge.slipping, .stage.lost {
  background: var(--c-warn-bg); border-color: var(--c-warn-border); color: var(--c-warn-ink);
}
.stage.won {
  background: var(--c-surface-2); border-color: var(--c-border); color: var(--c-ink-2);
}

/* Buttons. `btn-inline` is the save action inside a compose form, so it is the
   primary; `btn-small` is every secondary control the modules draw (edit,
   delete, log outcome, request retrieval). */
.btn-small, .btn-inline, .swap-btn, .draft-delete, .remove-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s4);
  height: var(--s32); padding: 0 var(--s12);
  border: 1px solid var(--c-border-strong); border-radius: var(--radius);
  background: var(--c-surface); color: var(--c-ink);
  font-family: inherit; font-size: var(--t4); font-weight: 500;
  cursor: pointer; white-space: nowrap;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.btn-small:hover, .btn-small.is-hover,
.swap-btn:hover, .draft-delete:hover, .remove-btn:hover { background: var(--c-surface-2); border-color: var(--c-ink-3); }
.btn-inline {
  height: var(--control-h); font-size: var(--t3);
  background: var(--c-accent-solid); border-color: var(--c-accent-solid); color: var(--c-on-accent);
}
.btn-inline:hover, .btn-inline.is-hover {
  background: var(--c-accent-hover); border-color: var(--c-accent-hover);
}
.btn-small:focus-visible, .btn-inline:focus-visible,
.swap-btn:focus-visible, .draft-delete:focus-visible, .remove-btn:focus-visible {
  outline: none; border-color: var(--c-accent); box-shadow: 0 0 0 2px var(--c-focus);
}
.btn-small:disabled, .btn-inline:disabled,
.swap-btn:disabled, .draft-delete:disabled, .remove-btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* Compose forms. entries.js and assets.js write NATIVE controls with no class
   attribute at all, so the container carries the class and the sheet reaches
   its children. That is the one place descendant selectors on element names
   are right: the alternative is editing a data module to add a class. */
.modform label {
  display: block; margin: var(--s12) 0 var(--s4);
  font-size: var(--t5); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em; color: var(--c-ink-3);
}
.modform > label:first-child { margin-top: 0; }
.modform input, .modform select, .modform textarea {
  width: 100%; height: var(--control-h);
  padding: 0 var(--control-px);
  border: 1px solid var(--c-border-strong); border-radius: var(--radius);
  background: var(--c-input-bg); color: var(--c-ink);
  font-family: inherit; font-size: var(--t3); line-height: normal;
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.modform textarea { height: auto; padding: var(--s8) var(--control-px); resize: vertical; }
.modform input::placeholder, .modform textarea::placeholder { color: var(--c-ink-3); opacity: 1; }
.modform input:focus-visible, .modform select:focus-visible, .modform textarea:focus-visible {
  outline: none; border-color: var(--c-accent); box-shadow: 0 0 0 2px var(--c-focus);
}
/* A LISTBOX IS NOT A ONE LINE SELECT. interests.js writes its picker as
   <select size="8">, and the control height above would crush it to one
   row with a scrollbar. It keeps the border, radius and focus ring. */
.modform select[size] { height: auto; padding: var(--s4) 0; }
.modform input[type="checkbox"] {
  width: auto; height: auto; accent-color: var(--c-accent-solid);
}
.modform .form-grid {
  display: grid; gap: var(--s12);
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.modform .form-row {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s12); margin-top: var(--s12);
}

/* Entry rows as entries.js and assets.js write them. `.entry`, `.entry-body`
   and `.entry-empty` are components above; these are the parts of the row the
   sheet had no name for. */
.entry {
  border-left: 2px solid transparent; padding-left: var(--s8);
}
.entry.pending { opacity: 0.6; border-left-color: var(--c-state-slipping); }
.entry-head {
  display: flex; flex-wrap: wrap; gap: var(--s8); align-items: baseline;
  margin-bottom: var(--s4);
  font-size: var(--t5); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--c-ink-3);
}
.entry-who { color: var(--c-ink); }
.entry-when { font-weight: 500; }
.entry-body { white-space: pre-wrap; overflow-wrap: anywhere; }
.entry-outcome {
  margin-top: var(--s8); padding-top: var(--s8);
  border-top: 1px solid var(--c-border);
  font-size: var(--t4); color: var(--c-ink-2);
}
.entry-outcome .k {
  font-size: var(--t5); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--c-ink-3); margin-right: var(--s4);
}
.entry-acts { display: flex; flex-wrap: wrap; gap: var(--s8); margin-top: var(--s8); }
.grp { margin: 0 0 var(--s16); }
.grp:last-child { margin-bottom: 0; }
/* TWO COLUMNS WHERE THERE IS ROOM. An entry row is one short line and a name,
   so a group of four on a 1280px screen is four rows of 44px in a column of
   text 1,000px wide with nothing in the other 700. Two columns make four
   reminders two rows and cost nothing on a phone, where the group stays a
   single column and the rule below never applies.

   The sub-label spans both, because it names the whole group rather than the
   first column of it. The second rule is the ragged edge that falls out of
   pairing an even number of rows: `.entry:last-child` already drops the last
   border, and without this the row beside it keeps one, leaving a rule under
   half of the final row. */
@media (min-width: 601px) {
  .grp--2col { display: grid; grid-template-columns: 1fr 1fr; column-gap: var(--s24); }
  .grp--2col > .sublabel { grid-column: 1 / -1; }
  .grp--2col > .entry:nth-last-child(2):nth-child(2n) { border-bottom: 0; }
}
/* UPCOMING and HISTORY are SUB-LABELS, not labels, and the difference is the
   whole point of the rank: they used to render at exactly the tokens `.t-label`
   carries (t5, 600, ink-3, small caps, 0.08em tracking), so a group heading and
   the label on a field were the same treatment doing two different jobs. The
   type is declared once with `.sublabel` above; only the spacing is the
   module's own. */
.grp-h { margin: 0 0 var(--s4); }

/* The contact block, as contact.js renders it, laid out as the header line.
 *
 * THE MODULE IS NOT EDITED TO PRODUCE `.idline` MARKUP, and it must not be:
 * the route sheet renders the identical model through contactText(), and the
 * judgement about which of two phone numbers is a restatement of the other is
 * the whole reason that module exists. So the markup stays as it is and the
 * layout happens here. `.c-name` and `.c-k` both take the `.k` label
 * treatment, which is what makes "store" and "Jade Lee" read as two labels on
 * one line rather than as a heading and a value.
 */
.idline > .wide { display: contents; }
.idline .c-block {
  display: flex; flex-direction: row; flex-wrap: wrap;
  gap: var(--s4) var(--s16); align-items: baseline;
}
.idline .c-line { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: var(--s4); }
.idline .c-k, .idline .c-name {
  font-size: var(--t5); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--c-ink-3);
}
.idline .c-ph, .idline .c-em { color: var(--c-ink); }
.idline .c-em { overflow-wrap: anywhere; }
.idline .c-why, .idline .c-none { font-size: var(--t5); color: var(--c-ink-3); }
.idline .c-why { flex-basis: 100%; }

/* Interest chips, as interests.js renders them into a `.chips` row.
 *
 * THE MODULE INJECTS ITS OWN CSS (INTEREST_CSS, through the page's
 * #interestCss) and that CSS comes after this file in the cascade, so these
 * rules carry one more selector part than the module's to win. What they
 * change is the three things off this system: a 10px radius, sizes off the
 * type scale (0.68rem, and 0.56rem on the kind label), and padding off the
 * spacing scale, and the item chip's edge, which the module draws in
 * --magenta (the accent) and this file draws in light grey; see the rule
 * below. The category edge is the module's --purple, aliased to --c-ink-2.
 */
div.chips > .chip {
  gap: var(--s4); padding: 0 var(--s8); min-height: 20px; align-items: center;
  border-radius: var(--radius); border: 1px solid var(--c-border); border-left-width: 2px;
  background: var(--c-surface-2); color: var(--c-ink);
  font-size: var(--t4); line-height: var(--lh-body);
}
div.chips.compact > .chip { font-size: var(--t4); padding: 0 var(--s8); }
/* GREY, NOT THE ACCENT, decided 2026-09-30: the accent is for the one primary
   action in a view, and an item chip is not an action. Light grey for an item,
   dark grey for a category, and both carry their kind as a text label, so the
   edge is a second cue and not the only one. */
div.chips > .chip.item { border-left-color: var(--c-border-strong); }
div.chips > .chip.cat { border-left-color: var(--c-ink-2); }
div.chips > .chip .k {
  font-size: var(--t5); font-weight: 600; letter-spacing: 0.06em; color: var(--c-ink-3);
}
div.chips > .chip.more { color: var(--c-ink-2); }
/* THE REMOVE CONTROL ON A CHIP IS A BUTTON, SO IT IS INK. The module draws
   it in --text-tertiary, the muted grey, which is a control reading as a
   caption. It gains the focus ring every control carries. */
div.chips > .chip > button {
  margin: 0; padding: 0 var(--s4); background: none; border: 0; border-radius: 2px;
  color: var(--c-ink); font-family: inherit; font-size: var(--t3); font-weight: 500;
  line-height: 1; cursor: pointer;
}
div.chips > .chip > button:hover { color: var(--c-danger-ink); }
div.chips > .chip > button:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 1px; }
/* A chip's note, beneath the row, on the type scale. */
p.chip-note { font-size: var(--t4); color: var(--c-ink-2); margin: var(--s4) 0 0; }
div.chips { gap: var(--s4); }

/* The index Tools grid, as nav.js's mountToolCards() renders it: one link card
 * per Hub page, each carrying the blurb the nav row has no room for.
 *
 * WHY IT IS HERE AND NOT A COMPONENT ABOVE. The markup is the module's
 * (`<a class="tool"><div class="t">title</div><div class="d">blurb</div></a>`)
 * and that module is data code a design pass may not edit, so the sheet meets
 * it where it is. Unbridged it rendered as a browser-default blue link with
 * two unstyled divs under it, which is the half-converted page this block
 * exists to prevent.
 *
 * THE CARD IS THE COMPONENT ABOVE, WITH ONE CHANGE: the whole card is the
 * link, so it takes the card's border, radius, shadow and 1px hover lift and
 * adds a focus ring, because a card you can tab to has to show that it has
 * focus.
 *
 * NOTHING INSIDE IT IS MUTED, WHICH IS NOT AN AESTHETIC CHOICE. Every text
 * node in here is inside an `<a href>`, so by the rule in section 22 of the
 * sheet, and by what scripts/check-type-tuples.mjs measures, all of it is
 * interactive text: a blurb set in --c-ink-3 would be a control that reads as
 * a footnote. The blurb is separated from the title by SIZE alone for the
 * same reason, since both are already required to be ink.
 *
 * The grid is the page's, because the module writes no container. 260px is a
 * floor rather than a width: a 25 word blurb in a 170px column is eight lines
 * of text, so this wraps to one card per row on a phone instead of two. */
.tool-grid {
  display: grid; gap: var(--s12);
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.tool {
  display: block; padding: var(--s16);
  background: var(--c-surface);
  border: 1px solid var(--c-border); border-radius: var(--radius);
  box-shadow: var(--shadow);
  color: var(--c-ink); text-decoration: none;
  transition: transform var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.tool:hover, .tool.is-hover { transform: translateY(-1px); border-color: var(--c-border-strong); }
.tool:focus-visible, .tool.is-focus {
  outline: 2px solid var(--c-accent); outline-offset: 2px;
}
.tool .t { font-size: var(--t3); font-weight: 600; color: var(--c-ink); }
.tool .d { margin-top: var(--s4); font-size: var(--t4); color: var(--c-ink); }

/* -------------------------------------------------------------------------
 * PIPELINE STAGE BADGE. A prospect's stage, which is not an account state.
 *
 * ONE NEUTRAL BADGE FOR ALL SEVEN, AND A DOT ONLY ON THE THREE OUTCOMES. The
 * page this replaced gave every stage its own text colour, seven colours on a
 * list whose default shows four of them, which is the paint chart the colour
 * budget forbids. The four open stages (new, contacted, sampling, quoted) are
 * one kind of thing, a lead in progress, and read by the word. The three
 * terminal stages are an outcome and carry the state dot the account badges
 * use for the nearest meaning: won is a customer now (the active dot), lost is
 * lost, dormant is the no-state grey. The stage is read off `data-stage`, the
 * value the page already writes, so no page code has to map a stage to a
 * class name.
 *
 * `.stage` IS NOT THIS. That name is the bridge for assets.js, where `lost`
 * means "retrieval requested" and tints warn; reusing it would have a lost
 * prospect read as a freezer somebody wants back.
 * ---------------------------------------------------------------------- */
.badge--pipeline .dot { display: none; }
.badge--pipeline[data-stage="won"] .dot,
.badge--pipeline[data-stage="lost"] .dot,
.badge--pipeline[data-stage="dormant"] .dot { display: block; }
.badge--pipeline[data-stage="won"] .dot { background: var(--c-state-active); }
.badge--pipeline[data-stage="lost"] .dot { background: var(--c-state-lost); }
.badge--pipeline[data-stage="dormant"] .dot { background: var(--c-state-none); }

/* -------------------------------------------------------------------------
 * PIPELINE BOARD. One column per stage, one card per prospect.
 *
 * THE COLUMNS SCROLL SIDEWAYS AT EVERY WIDTH, AND SNAP BY COLUMN ON A PHONE.
 * Seven columns do not fit 1040px either, and stacking them on a phone would
 * put three collapsed terminal columns and every open one in one long strip
 * where pipeline order stops reading as order. So the board keeps its shape
 * and a phone gets one column at 80% of the screen with the next one showing
 * at the edge, which is the scroll cue.
 *
 * STALENESS IS THE CARD'S LEFT EDGE, AND IT DARKENS. The page's own sentence
 * says so ("A card's left edge darkens once nothing has touched it for 14
 * days, and again at 21"), so the mechanism is grey to mid grey to ink rather
 * than the gold and orange it was: state colour belongs on badges, and a
 * darkening edge is what the sentence promised. Both steps clear 3:1 against
 * the card, because the edge is the only place the board says it.
 * ---------------------------------------------------------------------- */
.board {
  display: flex; align-items: flex-start; gap: var(--s12);
  overflow-x: auto; padding-bottom: var(--s8);
  scroll-snap-type: x proximity;
}
.bcol {
  flex: 0 0 240px; scroll-snap-align: start;
  background: var(--c-surface-2);
  border: 1px solid var(--c-border); border-radius: var(--radius);
  padding: var(--s8);
}
.bcol-h {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s8);
  min-height: var(--s32); margin-bottom: var(--s8);
}
.bcol-h.terminal { cursor: pointer; }
/* THE COLUMN COUNT IS A FIGURE, so it is ink and 600 like `.count b`, one
   size down because it sits beside a badge rather than in a sentence. */
.bcol-n {
  font-size: var(--t4); font-weight: 600; color: var(--c-ink);
  font-variant-numeric: tabular-nums;
}
/* Hide and show are the control, so ink, 500, underlined: the info-verb
   treatment, never the muted grey. */
.bcol-toggle {
  /* A real button since 2026-09-30, for keyboard access; these lines undo
     the button chrome so it still reads as the info-verb treatment. */
  background: none; border: 0; padding: 0; font-family: inherit; cursor: pointer;
  margin-left: auto; font-size: var(--t4); font-weight: 500; color: var(--c-ink);
  text-decoration: underline; text-decoration-color: var(--c-border-strong);
  text-underline-offset: 3px;
}
.bcol-h.terminal:hover .bcol-toggle { text-decoration-color: var(--c-accent); }
.bcol-toggle:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; border-radius: 2px; }
.bcol-empty { font-size: var(--t4); color: var(--c-ink-3); padding: var(--s4); }
.pcard {
  background: var(--c-surface);
  border: 1px solid var(--c-border); border-left: 2px solid var(--c-border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: var(--s8) var(--s12); margin-bottom: var(--s8);
}
.pcard:last-child { margin-bottom: 0; }
.pcard.stale-warn { border-left-color: var(--c-border-strong); }
.pcard.stale-bad { border-left-color: var(--c-ink); }
.pcard-name { display: block; font-size: var(--t3); margin-bottom: var(--s4); }
.pcard-meta, .pcard-next {
  font-size: var(--t4); color: var(--c-ink-3); font-variant-numeric: tabular-nums;
}
.pcard-next { margin-top: var(--s4); }
.pcard .chips { margin: var(--s4) 0; }
.pcard-move-wrap { margin-top: var(--s8); }

@media (max-width: 600px) {
  .board { scroll-snap-type: x mandatory; }
  .bcol { flex-basis: 80%; }
}

/* -------------------------------------------------------------------------
 * PLACED EQUIPMENT. The status badge, and the Place dialog with its account
 * picker. assets.html is the user.
 *
 * THE STATUS BADGE IS THE SMALL BADGE, NEUTRAL, WITH TWO FLAGS. Five statuses
 * are derived from the placement history. Three of them (in warehouse,
 * placed, retired) are ordinary and read by the word, so they take the plain
 * neutral badge. Two are a flag about the unit: retrieval requested tints
 * warn, which is what the account page's equipment rows already show for the
 * same status through `.stage.lost`, and lost tints danger, because a unit
 * nobody can find is the one state that costs money. The page writes
 * `st-<status>`, the value it already had, so no page code maps a status to a
 * class name. The two flags are extra selectors on the flag badge rules
 * rather than a second copy of their values. It used to give every status its own text colour and a coloured
 * border, which is state colour as type.
 *
 * THE DIALOG IS A CARD THAT FLOATS. Surface, one border, one radius, the
 * card's shadow, and a backdrop in the ink at low alpha rather than black, so
 * the page behind reads as set aside rather than switched off. Its title is
 * the section header, with the word "Place" where the ordinal goes.
 *
 * A PICK ROW IS A CONTROL, SO ITS NAME IS INK AT 500. The account number and
 * city after it are the footnote tier, which qualifies the name rather than
 * competing with it. The rows are divs because the page's click handler looks
 * for div[data-acct]; see the sheet for what that costs a keyboard.
 * ---------------------------------------------------------------------- */
/* The two flagged statuses are listed on `.badge--warn` and `.badge--danger`
   themselves, under Badges above, so they cannot drift from the flag badge. */

.dlg {
  width: min(34rem, calc(100% - 2 * var(--s16))); max-width: none;
  padding: var(--s24); margin: auto;
  border: 1px solid var(--c-border); border-radius: var(--radius);
  background: var(--c-surface); color: var(--c-ink); box-shadow: var(--shadow);
}
.dlg::backdrop { background: rgb(20 22 26 / 0.4); }
.dlg .dlg-title { margin-top: 0; }
.dlg strong { font-weight: 600; color: var(--c-ink); }
.dlg .chk { margin-top: var(--s12); flex-wrap: wrap; }
.dlg-chosen { margin: var(--s12) 0 0; }
.dlg-acts { justify-content: flex-end; margin-top: var(--s16); }

.pick { max-height: 14rem; overflow-y: auto; margin-top: var(--s8); }
.pick:empty { display: none; }
/* .pick-row is a button (assets.html's account picker, for keyboard access),
   so these lines undo the button chrome; the other two are divs and ignore
   them. */
.pick-row { width: 100%; text-align: left; background: none; border: 0; font: inherit; color: inherit; }
.pick-row:focus-visible { outline: 2px solid var(--c-accent); outline-offset: -2px; }
.pick-row, .autocomplete-item, .draft-item {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s4) var(--s8);
  padding: var(--s8); border-radius: var(--radius); cursor: pointer;
  transition: background var(--dur) var(--ease);
}
.pick-row:hover, .pick-row.is-hover,
.autocomplete-item:hover, .autocomplete-item.active, .draft-item:hover { background: var(--c-surface-2); }
.pick-name, .ac-code, .draft-name { font-size: var(--t3); font-weight: 500; color: var(--c-ink); }
.pick > .entry-empty { margin: var(--s8) 0; }

/* -------------------------------------------------------------------------
 * ORDER FORM. The drafts bar and its menu, the item autocomplete, the line
 * items and the toast. opening-order.html is the user.
 *
 * NAMED AFTER THE PAGE'S CLASSES, AND THAT IS THE CONSTRAINT RATHER THAN A
 * PREFERENCE. That page's script builds these rows as template strings and
 * reads them back by class: `.line-row`, `.custom-item`, `.inactive-item`,
 * `.autocomplete-list.show`, `.autocomplete-item.active`, `.draft-item`,
 * `.drafts-menu.show`, `.toast.show`. Renaming one in a design pass would be
 * an edit to the autosave, the snapshot or the PDF builder, so the component
 * takes the names the script already uses, the way `.grp-h` and `.st-lost` do.
 * Where a piece IS an existing component (a pick row, a flag badge, a small
 * button, the footnote tier, the warn banner, the save indicator) the class is
 * listed on that component's own rule above, not restated here.
 * ---------------------------------------------------------------------- */

/* The strip under the catalog note: the drafts menu, the draft name and the
   save indicator. One row where there is room; the indicator keeps the right
   edge so the thing that says whether the work is safe sits in one place. */
.drafts-bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s8) var(--s12);
}
.drafts-left {
  position: relative; display: flex; align-items: center; gap: var(--s8);
  flex: 1 1 20rem; min-width: 0;
}
.drafts-left > .input { flex: 1 1 auto; min-width: 0; max-width: 22rem; }
.drafts-bar > .save { margin-left: auto; }
/* A BUTTON THAT OPENS A MENU carries the disclosure chevron, in the accent,
   the same signal as the collapsible section and the Filters drawer. */
.btn > .chev { color: var(--c-accent); }

/* A MENU FLOATS OVER THE FORM. The dialog's surface, radius and shadow, with
   the control border rather than the divider, because it sits on white inputs
   and a 1.3:1 edge would leave it with no edge at all. Shown only while the
   script has added `.show`. */
.drafts-menu, .autocomplete-list {
  display: none; position: absolute; z-index: 40;
  top: calc(100% + var(--s4)); left: 0;
  padding: var(--s4); overflow-y: auto;
  background: var(--c-surface); color: var(--c-ink);
  border: 1px solid var(--c-border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.drafts-menu.show, .autocomplete-list.show { display: block; }
.drafts-menu { width: min(22rem, calc(100vw - 2 * var(--s16))); max-height: 22rem; }
.drafts-menu > .btn { width: 100%; margin-bottom: var(--s4); }
.autocomplete-list { right: 0; max-height: 16rem; z-index: 50; }
/* The rows are pick rows (see Placed equipment). What differs: a draft row
   ends in its delete control, and an item row keeps its code in a column. */
.draft-item { flex-wrap: nowrap; align-items: center; justify-content: space-between; }
.draft-item.active { box-shadow: inset 2px 0 0 var(--c-ink); }
.draft-info { flex: 1 1 auto; min-width: 0; }
.draft-meta, .ac-desc { font-size: var(--t4); font-weight: 400; color: var(--c-ink-3); }
.draft-delete { flex: none; width: var(--s32); padding: 0; }
.ac-code { flex: 0 0 8rem; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
.ac-desc { flex: 1 1 auto; min-width: 0; }
.drafts-empty, .ac-no-results { margin: var(--s8); }
.drafts-storage-note {
  margin: var(--s4) 0 0; padding: var(--s8);
  border-top: 1px solid var(--c-border);
  font-size: var(--t4); color: var(--c-ink-3);
}

/* LINE ITEMS. A grid at desktop, one card per row below 600px, the same turn
   the table makes and for the same reason: five inputs across 358px is five
   inputs nobody can read. Column order: number, code, description, qty,
   remove. The flag line under a typed code spans the whole row.

   THE STATUS COLUMN IS HIDDEN, because nothing has written to it since the
   form moved into the Hub (ced45b1). Its `.custom-flag` span was styled to
   carry a one word verdict per line, gold for Custom and red for inactive;
   that commit moved the verdict into the full width `.flag-cell` under the
   row, as a badge plus a sentence, and left the span empty on every row.
   The span stays in the markup, so a harness or a PDF path that looks for it
   still finds it; it just takes no column. */
.line-header > :nth-child(5), .line-row > .custom-flag { display: none; }
.line-header, .line-row {
  display: grid; align-items: center; gap: var(--s8);
  grid-template-columns: var(--s24) minmax(0, 1.5fr) minmax(0, 2.3fr) 5rem var(--control-h);
}
.line-header {
  padding: 0 var(--s4) var(--s4);
  font-size: var(--t5); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em; color: var(--c-ink-3);
}
.line-row {
  position: relative; padding: var(--s4);
  border: 1px solid transparent; border-radius: var(--radius);
}
.line-row + .line-row { margin-top: var(--s4); }
.line-num {
  font-size: var(--t4); color: var(--c-ink-3);
  font-variant-numeric: tabular-nums; text-align: right;
}
.item-cell, .line-cell { position: relative; min-width: 0; }
/* A FLAGGED ROW TAKES THE FLAG'S OWN TINT, never a colour of its own: warn for
   a code the item master does not have, danger for one it has as inactive.
   The badge inside the flag line names which, so the tint is never alone. */
.line-row.custom-item   { background: var(--c-warn-bg);   border-color: var(--c-warn-border); }
.line-row.inactive-item { background: var(--c-danger-bg); border-color: var(--c-danger-border); }
.flag-cell {
  grid-column: 1 / -1;
  padding: 0 0 var(--s4) calc(var(--s24) + var(--s8));
  font-size: var(--t4); line-height: var(--lh-body);
}
.flag-cell:empty { display: none; }
.flag-cell > .flag-custom, .flag-cell > .flag-inactive { vertical-align: middle; margin-right: var(--s4); }
.flag-note { color: var(--c-ink-3); }
.flag-note strong { font-weight: 600; color: var(--c-ink); }
.swap-btn { margin-left: var(--s8); }
.remove-btn { width: var(--control-h); height: var(--control-h); padding: 0; font-size: var(--t3); }
.add-row-btn { margin-top: var(--s12); }
@media (max-width: 600px) {
  .line-header { display: none; }
  .line-row {
    grid-template-columns: var(--s24) minmax(0, 1fr) 5rem var(--control-h);
    grid-template-areas: "num code qty rm" ". desc desc desc" "flag flag flag flag";
    align-items: end;
    padding: var(--s8) var(--s12);
    background: var(--c-surface); border-color: var(--c-border); box-shadow: var(--shadow);
  }
  .line-row + .line-row { margin-top: var(--s8); }
  .line-row > .line-num { grid-area: num; line-height: var(--control-h); }
  .line-row > .item-cell:nth-child(2) { grid-area: code; }
  .line-row > .item-cell:nth-child(3) { grid-area: desc; }
  .line-row > .line-cell { grid-area: qty; }
  .line-row > .custom-flag { display: none; }
  .line-row > .remove-btn { grid-area: rm; }
  .line-row > .flag-cell { grid-area: flag; padding: 0; }
  /* EACH INPUT CARRIES ITS OWN LABEL ON THE CARD, which is why the cells carry
     `data-label`: the column header is gone at this width and a placeholder
     disappears the moment the field has a value in it. */
  .line-row [data-label]::before {
    content: attr(data-label); display: block; margin-bottom: var(--s4);
    font-size: var(--t5); font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.08em; color: var(--c-ink-3);
  }
}

/* THE TOAST. A card that rises from the bottom edge for three seconds, then
   goes. The failure variant takes the danger tint the failed save indicator
   and the danger banner use, so a failure reads the same in all three. */
.toast {
  position: fixed; left: 50%; bottom: var(--s24); z-index: 100;
  width: max-content; max-width: min(34rem, calc(100vw - 2 * var(--s16)));
  padding: var(--s12) var(--s16);
  background: var(--c-surface); color: var(--c-ink);
  border: 1px solid var(--c-border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow);
  font-size: var(--t3);
  opacity: 0; pointer-events: none;
  transform: translate(-50%, var(--s8));
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast.error { background: var(--c-danger-bg); border-color: var(--c-danger-border); color: var(--c-danger-ink); }

/* Icons. 16px in the UI, one stroke width, everywhere. */
.lucide, [data-lucide] { width: 16px; height: 16px; stroke-width: 1.5; flex: none; }

@media (max-width: 600px) {
  body { padding: var(--s24) var(--s16) var(--s48); }
  .page-head { flex-direction: column; gap: var(--s8); }
  /* The mark and the title share the first row on a phone too; anything else
     in the header (a scope menu, a button) takes its own full width row. */
  .page-head:has(> .brand-mark) { flex-direction: row; flex-wrap: wrap; align-items: center; gap: var(--s8) var(--s12); }
  .page-head > .brand-mark + div { flex: 1 1 0; min-width: 0; margin-right: 0; }
  .page-head > .brand-mark + div ~ * { flex: 1 0 100%; }
  .cards { grid-template-columns: 1fr 1fr; }
  /* Two up, and the third takes the whole width rather than sitting beside a
     hole. It is also the widest of the three figures more often than not:
     "Accounts ordering" is the longest label in the set and a dollar total is
     the longest value. */
  .cards--3 { grid-template-columns: 1fr 1fr; }
  .cards--3 > :nth-child(3) { grid-column: 1 / -1; }
  /* No wide child: six is even, so two up fills every row. minmax(0, 1fr) so
     a 32px dollar figure cannot push one column wider than its share. */
  .cards--3x2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* THE WIDE CARD READS ACROSS INSTEAD OF DOWN, and only here. A card holding
     one figure and given the whole width is a 326px box with a number in the
     top corner and 200px of nothing beside it. Same label, same value, same
     footnote, same order, turned ninety degrees for the width it was given.
     Measured on index.html, which has two of them: the phone page stood at
     2,203px with this rule off and 2,111px with it on, so the pair cost 92px
     of a rep's scroll to show nothing. A card carrying anything else (a
     breakdown, a destination) does not take this modifier, which is why it
     is a modifier and not part of the third-child rule above. */
  /* `.cards--3 .card--wide`, not `.card--wide`: the flex rule above it is a
     two-class selector and would win on specificity wherever this modifier is
     used inside that grid, which is the only place it is used. */
  .cards--3 .card--wide, .card--wide {
    display: grid; grid-template-columns: 1fr auto;
    align-items: center; column-gap: var(--s12);
  }
  .card--wide > .stat-label { grid-column: 1; align-self: end; margin-bottom: 0; }
  .card--wide > .stat-value { grid-column: 2; grid-row: 1 / span 2; }
  .card--wide > .stat-foot { grid-column: 1; align-self: start; }

  /* THE TOP BAR IS ONE ROW THAT SCROLLS SIDEWAYS, WITH SIGN OUT IN IT.
     Left to wrap, the eight links take two rows and the button a third, so
     three rows of furniture that is identical on every Hub page stand above
     the one thing on the page that is not. Here the bar stays on one line and
     is reached by scrolling instead of by stacking.

     overflow-x, never overflow:hidden. Hiding an axis would clip the 2px focus
     ring on the links, which is the one thing that has to survive a scrolling
     row, so the ring's 3px (2px outline plus 1px offset) is paid for in
     padding on all four sides and the axis is left free. Keyboard focus
     scrolls the row on its own, with the ring intact.

     THE INLINE PADDING IS NOT SYMMETRY, IT IS THE ENDS OF THE ROW. An outline
     is not part of a scroll container's scrollable overflow, so with the box
     flush to its content the FIRST and LAST items each lose 3px of ring off
     the outer edge, at every scroll position, which is the one place the ring
     matters most because those are the items a keyboard reaches first and
     last. 4px of inline padding gives it room. It is not given back with a
     negative margin: that would widen the bar's bottom border past the cards
     below it, and a 4px indent on links whose own text already sits 10px in
     is invisible where a wider rule is not. */
  /* THE SCROLLING ROW MOVED INTO nav.js AND IS NOT DUPLICATED HERE. This
     block used to make .topbar itself the scroll container, which covered the
     two converted pages and left the other eight wrapping to three rows. The
     module that every page's nav has in common is the only place the rule
     works everywhere, so .hubnav is the scroll container now and it also
     takes the page's Sign out button in as its last item. What is left here
     is the bar not adding a second scroll container around the first. */
  .topbar { display: block; padding: 0; overflow: visible; }
}
