/* `hidden` means hidden, whatever a later rule says.
   A component styled `display: flex` or `display: grid` silently outranks the
   browser's own `[hidden]` rule, and the result is a panel that is on screen
   permanently while every line of JavaScript around it looks correct. It has
   happened twice in this codebase; the guard is cheap. */
[hidden] { display: none !important; }

/* ===========================================================================
   Kanari Suite — application stylesheet.

   Hand-written and shipped compiled: shared hosts have no build step, so there
   is no Tailwind pipeline to run and no npm install to forget. Brand tokens
   are derived from the settings registry at render time (`Branding::palette()`),
   so changing a colour never needs a deploy (FR-CMS-04).

   ## The 0.18.0 rework

   The owner's words were that it "looks and feels rather stale", and the
   screenshots agreed. Four things were doing most of that work, and none of
   them was the colour:

   1. **Everything was outlined.** Every panel, every table, every stat tile
      carried a 1px border on a beige ground, so a screen was a stack of boxes
      of equal weight with nothing telling the eye what mattered. Depth now
      comes from surface and shadow, and a border is what separates things that
      are genuinely adjacent.

   2. **`--radius: 3px`.** Corners that sharp read as an administrative tool
      from 2010 whatever else is done to them.

   3. **Mono uppercase micro-type everywhere** — 9.5px letterspaced labels on
      nav groups, table headers and stat tiles alike. It is a strong voice and
      it was applied to everything, which flattens rather than emphasises. It
      is kept where it earns its place (a column header, a code) and dropped
      where it was decoration.

   4. **The typeface was never loaded.** `--sans` named *Archivo* and `--mono`
      named *IBM Plex Mono*; neither was ever fetched, no `@font-face` exists
      anywhere in the product, and `font-src 'self'` would have refused a CDN
      if one had been added. So the product has always rendered in whatever the
      system had, while the stylesheet claimed a designed typeface — the same
      shape as the settings that configured nothing.

      Not fixed by shipping font files. This product is sold into Cameroon on
      shared hosting: a webfont is 200 KB of package and a render-blocking
      fetch on a connection that is often a phone tethered in a shop. The stack
      below is what each platform already has, ordered so that Android — the
      hardware decision 21 chose for the counter — gets Roboto and Windows gets
      Segoe UI. It is honest about what will actually be drawn.

   ## The scales

   Sizes, spacing and radii come from three small scales rather than from
   whatever number looked right. The previous stylesheet used 11, 12, 13, 13.5,
   14, 15, 17, 22, 25 and 26px for type and nine different paddings, which is
   why nothing lined up with anything.
   ======================================================================== */

:root {
  /* --- brand. Overwritten per-request from the settings registry; these are
         the fallbacks for a page rendered before the database is reachable. */
  --brand:          #0d6156;
  --brand-ink:      #0a4c44;
  --brand-soft:     #e2eeec;
  --accent:         #b23a22;
  --accent-soft:    #f3eae8;
  /* What to write on top of them. Emitted per-request by `Branding::palette()`,
     which picks by luminance — see `.btn--primary`. */
  --on-brand:       #ffffff;
  --on-accent:      #ffffff;

  /* --- surfaces. A page is three layers, not a grid of outlined boxes:
         the ground it sits on, the panel that holds content, and a recessed
         tone for headers and inert areas. */
  --ground:         #f4f2ee;
  --panel:          #ffffff;
  --panel-2:        #f6f4f0;
  --rule:           #e2ddd3;
  --rule-soft:      #edeae3;

  --panel-3:        #efece5;

  --ink:            #1b211e;
  --ink-2:          #545c55;
  --ink-3:          #808880;

  /* The navigation rail. Every skin defines these; the sidebar's rules read
     them and never ask which skin is running, which is what lets a dark rail
     be a data change rather than twenty duplicated rules. See `Theme`. */
  --rail:           #f6f4f0;
  --rail-2:         #efece5;
  --rail-ink:       #1b211e;
  --rail-ink-2:     #646c65;
  --rail-rule:      #e2ddd3;

  --ok:             #3d6b2c;
  --ok-soft:        #e9f0e2;
  --warn:           #8a5c0d;
  --warn-soft:      #faf0da;
  --danger:         #a03028;
  --danger-soft:    #fae7e4;
  --info:           #2c5c78;
  --info-soft:      #e6eff5;

  /*
   * What the browser will actually draw. `system-ui` first so each platform
   * uses the face it was designed with, then the named fallbacks for the ones
   * whose `system-ui` is poor.
   */
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
          "Noto Sans", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
          "Liberation Mono", monospace;

  /* --- type scale, 1.20 ratio off a 13.5px body.
         Tightened half a step in 0.41.0. This is a working tool: a buyer
         reads a hundred-line purchase order and a cashier scans a shift
         report, and half a pixel of body type is four more rows on the
         screen. The headings keep their size, so the contrast between a
         heading and its rows went up rather than down. */
  --t-xs:   11px;
  --t-sm:   12px;
  --t-base: 13.5px;
  --t-md:   14.5px;
  --t-lg:   16px;
  --t-xl:   19px;
  --t-2xl:  23px;
  --t-3xl:  28px;

  /* --- spacing, a 4px grid with a hairline step at the bottom. */
  --s0: 2px;
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 20px;
  --s6: 28px;
  --s7: 40px;

  /* The height of one row of anything: a table row, a menu entry, a control.
     One number, so a table and the menu beside it line up instead of nearly
     lining up. */
  --row: 34px;

  /* --- radii. Tighter than 0.18.0's, which were drawn for a page of cards;
         at this density a 12px corner on a 34px row is a lozenge. */
  --r-sm:   4px;
  --r:      6px;
  --r-lg:   10px;
  --r-pill: 999px;

  /* Kept: a great deal of the product refers to it. */
  --radius: var(--r);

  /*
   * Two elevations and a focus ring. The old single shadow was
   * `0 1px 2px rgba(…, .07)` — close enough to nothing that every panel needed
   * a border to be visible at all, which is where the boxed-in look came from.
   */
  --shadow:    0 1px 2px rgba(27, 33, 30, .05), 0 1px 3px rgba(27, 33, 30, .06);
  --shadow-lg: 0 4px 6px rgba(27, 33, 30, .04), 0 12px 28px rgba(27, 33, 30, .10);
  --ring:      0 0 0 3px color-mix(in srgb, var(--brand) 26%, transparent);
}

/*
 * Dark mode.
 *
 * The brand values here are fallbacks only: `Branding::palette()` emits the
 * shop's own colour, lightened in HSL, at this same specificity. Until 0.18.0
 * this block hard-coded one palette and silently won over the colour the shop
 * had chosen — see `Branding::palette()` for what that cost.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --brand:       #50beb0;
    --brand-ink:   #92d9cf;
    --brand-soft:  #132f2b;
    --accent:      #db7a66;
    --accent-soft: #321e1a;
    --on-brand:    #10140f;
    --on-accent:   #10140f;

    --ground:      #121517;
    --panel:       #1a1e21;
    --panel-2:     #22272b;
    --panel-3:     #272d31;
    --rule:        #333a3f;
    --rule-soft:   #272d31;

    --ink:         #e7eae6;
    --ink-2:       #a9b1ab;
    --ink-3:       #7b837d;

    --rail:        #15191c;
    --rail-2:      #1d2225;
    --rail-ink:    #e7eae6;
    --rail-ink-2:  #7b837d;
    --rail-rule:   #2a3034;

    --ok:          #8fc172;
    --ok-soft:     #1d2a18;
    --warn:        #d9a44d;
    --warn-soft:   #2f2615;
    --danger:      #e08379;
    --danger-soft: #331e1b;
    --info:        #7fb2cf;
    --info-soft:   #19262e;

    --shadow:    0 1px 2px rgba(0, 0, 0, .4), 0 1px 3px rgba(0, 0, 0, .3);
    --shadow-lg: 0 4px 6px rgba(0, 0, 0, .35), 0 12px 28px rgba(0, 0, 0, .5);
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t-base);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a { color: var(--brand-ink); text-decoration: none; }
a:hover { text-decoration: underline; }

/* A visible ring rather than a hairline outline: on a beige ground the old
   2px outline disappeared against half the components it landed on. */
:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

h1, h2, h3, h4 { margin: 0; font-weight: 650; letter-spacing: -.018em; line-height: 1.22; }
h1 { font-size: var(--t-2xl); }
h2 { font-size: var(--t-lg); }
h3 { font-size: var(--t-md); }
p  { margin: 0 0 var(--s3); }

code, .mono { font-family: var(--mono); font-size: .9em; font-variant-ligatures: none; }
.tabular { font-variant-numeric: tabular-nums; }

/* Numbers line up wherever they are money or a count. Proportional digits in
   a column of totals is the single most common reason a figures table looks
   amateur. */
.stat .value, table.data td.num, table.data th.num, .kv td { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------- structure */

.shell { display: grid; grid-template-columns: 216px minmax(0, 1fr); min-height: 100vh; }

/*
 * The rail (0.41.0).
 *
 * Every colour here comes from a `--rail*` token, and that is the whole reason
 * a dark navigation is a change to `Theme::SKINS` rather than a second copy of
 * these twenty rules. A skin with a light rail points them at its own panel
 * tones; the rules below cannot tell the difference and do not ask.
 */
.sidebar {
  background: var(--rail);
  color: var(--rail-ink);
  border-right: 1px solid var(--rail-rule);
  padding: var(--s4) var(--s2) var(--s6);
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
  /* A quarter of the nav is off-screen on a laptop; a scrollbar that only
     appears on hover leaves people unsure there is more below. */
  scrollbar-width: thin;
}
.sidebar .brand {
  font-weight: 700; font-size: var(--t-sm); letter-spacing: .1em; text-transform: uppercase;
  color: var(--rail-ink); padding: 0 var(--s2) var(--s0);
}
.sidebar .brand em { color: var(--brand); font-style: normal; }
.sidebar .tenant {
  font-size: var(--t-xs); color: var(--rail-ink-2);
  padding: 0 var(--s2) var(--s4); word-break: break-word; line-height: 1.4;
}

/*
 * Group headings. Small caps rather than mono-uppercase-letterspaced: the old
 * treatment was the same one the table headers and the stat labels had, so
 * three different levels of the hierarchy shouted in the same voice.
 */
.nav-group {
  font-size: var(--t-xs); font-weight: 650; letter-spacing: .07em; text-transform: uppercase;
  color: var(--rail-ink-2); padding: var(--s3) var(--s2) var(--s0);
}
.nav-group:first-child { padding-top: var(--s1); }

/*
 * Folding groups.
 *
 * `<details>`, so the fold works with no script — including on the first paint,
 * before any script has run. Everything below is appearance; none of it is
 * behaviour, which is the reason for choosing the element.
 */
.nav-fold { margin: 0; }
.nav-fold > summary.nav-group {
  display: flex; align-items: center; gap: var(--s2);
  cursor: pointer; list-style: none; border-radius: var(--r-sm);
  user-select: none;
}
/* Safari draws its own triangle unless this is said in its own dialect. */
.nav-fold > summary::-webkit-details-marker { display: none; }

.nav-fold > summary.nav-group:hover { color: var(--rail-ink); background: color-mix(in srgb, var(--rail-ink) 8%, var(--rail)); }
.nav-fold > summary.nav-group:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

/*
 * The label takes the slack, so the badge and the chevron sit together at the
 * right whether or not there is a badge. Two competing `margin-left: auto`
 * would have split the free space between them and left the count stranded in
 * the middle of the heading.
 */
.nav-fold > summary.nav-group > span:first-child { flex: 1 1 auto; min-width: 0; }

/* The chevron. `currentColor` so it follows the heading into dark mode and
   into the hover state without a second rule for each. */
.nav-fold > summary.nav-group::after {
  content: ""; flex: none;
  width: 6px; height: 6px;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform .12s ease;
}
.nav-fold[open] > summary.nav-group::after { transform: rotate(45deg); }
@media (prefers-reduced-motion: reduce) {
  .nav-fold > summary.nav-group::after { transition: none; }
}

/* An open group is tied to its heading by a hairline rather than by
   indentation: indenting cost 12px of a 232px column and made the longer
   labels wrap. */
.nav-fold[open] > .nav-item { margin-left: 9px; border-left: 1px solid var(--rail-rule); border-radius: 0 var(--r-sm) var(--r-sm) 0; padding-left: 9px; }
.nav-fold[open] > .nav-item.is-active { border-left-color: transparent; }
.nav-fold[open] > .nav-item.is-active::before { left: -1px; }

.nav-item {
  display: flex; align-items: center; gap: var(--s2);
  min-height: 30px;
  padding: 5px var(--s2) 5px 9px;
  border-radius: var(--r-sm);
  /*
   * Brighter than the group heading above it.
   *
   * Both were `--rail-ink-2` in the first cut of this rail and the screenshot
   * showed why that is wrong: a heading and the six links under it in one
   * tone is a list with no hierarchy, so the eye has to read the words to
   * find the level. The heading stays muted and the links come forward.
   */
  /*
   * Mixed with the rail, not with transparency.
   *
   * `…84%, transparent` looks identical and is not the same thing: the text
   * is then translucent and composites over whatever is behind it, so its
   * real contrast is lower than the token says and **nothing can measure
   * it** — `getComputedStyle().color` answers with an alpha, and the audit in
   * `Theme` only ever sees the opaque token. The browser drive found it by
   * measuring what was actually painted. Mixing toward the rail gives the
   * same appearance as an opaque colour that both sides can check.
   */
  color: color-mix(in srgb, var(--rail-ink) 84%, var(--rail));
  font-size: var(--t-base);
  position: relative;
}
.nav-item:hover {
  background: color-mix(in srgb, var(--rail-ink) 9%, var(--rail));
  color: var(--rail-ink); text-decoration: none;
}

/*
 * The icon (0.41.0). 16px, inline SVG, `currentColor` — so it follows the
 * entry into hover, into the active state, into every skin and into dark mode
 * without a single rule of its own. An icon set that needed a colour per state
 * would be four sprites and a decision to get wrong.
 *
 * Slightly transparent when idle and fully opaque when the entry is current,
 * which is what makes a column of twenty-five icons read as a list rather than
 * as noise.
 */
.nav-item__icon { flex: none; width: 16px; height: 16px; opacity: .72; }
.nav-item:hover .nav-item__icon,
.nav-item.is-active .nav-item__icon { opacity: 1; }
.nav-group__icon { flex: none; width: 15px; height: 15px; opacity: .6; }

/*
 * The active item is filled rather than outlined on one edge. A 2px left
 * border was the only signal, which on a list of twenty-five items is very
 * little — and it moved the text by 2px, so the whole list shifted as you
 * navigated.
 */
/*
 * Mixed with the rail rather than set to `--brand-soft`.
 *
 * `--brand-soft` is a *light* tint of the shop's colour, which is right on a
 * light sidebar and a bright lozenge on a dark one — and the rail's darkness
 * is a property of the skin while the brand belongs to the shop, so the two
 * cannot be assumed to agree. Mixing the brand into whatever the rail is
 * gives one rule that is legible on both.
 */
.nav-item.is-active {
  background: color-mix(in srgb, var(--brand) 20%, var(--rail));
  color: var(--rail-ink); font-weight: 650;
}
.nav-item.is-active::before {
  content: ""; position: absolute; left: 0; top: 6px; bottom: 6px;
  width: 3px; border-radius: 0 2px 2px 0; background: var(--brand);
}
.nav-item__label { flex: 1 1 auto; min-width: 0; }
.nav-item .badge { flex: none; }

/* The till. The one entry somebody uses for eight hours, so it is allowed to
   be louder than the rest and is never folded away. */
.nav-item--strong { font-weight: 700; color: var(--rail-ink); }

/*
 * A badge whose query failed.
 *
 * Deliberately not red and not grey: red would claim something is wrong in the
 * shop, and grey would be ignorable. It says "this number could not be read",
 * which is a different statement from both "nothing is waiting" and "eleven
 * things are waiting" — and it is the statement the sidebar used to be unable
 * to make, because a failed count was rendered as zero.
 */
.badge--murky {
  background: var(--warn-soft); color: var(--warn); border-color: var(--warn);
  border-style: dashed; cursor: help; font-weight: 700;
}

.main { min-width: 0; display: flex; flex-direction: column; }

.topbar {
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
  min-height: 46px;
  padding: var(--s2) clamp(var(--s4), 3vw, var(--s5));
  border-bottom: 1px solid var(--rule); background: var(--panel);
  position: sticky; top: 0; z-index: 30;
}
.topbar .spacer { flex: 1; }
.topbar .who { font-size: var(--t-sm); color: var(--ink-3); }
.topbar strong { font-size: var(--t-md); font-weight: 650; letter-spacing: -.01em; }

/*
 * `min-width: 0`, because a flex child's default is `auto` — it refuses to shrink
 * below its content, so one wide table inside it can push the whole page sideways
 * however many scroll containers are nested in between.
 *
 * **Kept as hygiene and not claimed as a fix.** The sabotage harness removed this
 * line and the drive measured no difference across 88 screens at three widths:
 * with the wrappers in place and `.row > *` clamped, nothing in the product as it
 * stands depends on it. `mobile-test.php` therefore does not assert it, because
 * asserting the presence of a rule whose absence changes nothing is the shape of
 * a control that does not control.
 */
.content { padding: var(--s4) clamp(var(--s4), 3vw, var(--s5)) var(--s7); flex: 1; min-width: 0; }
.page-head { display: flex; align-items: flex-start; gap: var(--s4); flex-wrap: wrap; margin-bottom: var(--s4); }
.page-head .spacer { flex: 1; }
.page-head p { color: var(--ink-2); max-width: 72ch; margin: var(--s1) 0 0; font-size: var(--t-base); }

/* ----------------------------------------------------------------- cards */

.panel {
  background: var(--panel);
  /* A hairline, not a frame. The elevation does the separating now, so a page
     of six panels no longer reads as six boxes of equal importance. */
  border: 1px solid var(--rule-soft);
  border-radius: var(--r);
  box-shadow: var(--shadow);
}
.panel + .panel { margin-top: var(--s3); }
/*
 * The head sits on `--panel-3` from 0.41.0 rather than on the panel itself.
 *
 * At this density a head separated only by a hairline is a row of the table
 * below it wearing bold — which is exactly what a sticky table header must not
 * look like. A tone costs nothing and every skin defines one.
 */
.panel__head {
  padding: var(--s2) var(--s3);
  min-height: var(--row);
  background: var(--panel-3);
  border-bottom: 1px solid var(--rule);
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
  border-radius: var(--r) var(--r) 0 0;
}
.panel__head .spacer { flex: 1; }
.panel__head h2 { font-size: var(--t-base); font-weight: 650; letter-spacing: -.005em; }
.panel__body { padding: var(--s3); }
.panel__body--flush { padding: 0; }

/*
 * The classes views write that this file had never defined (0.89.0).
 *
 * Measured, by reading every `class="…"` literal in all 334 views against every
 * selector in this file: **332 distinct classes used, 54 of them styled
 * nowhere** — not here, not in any view's own inline `<style>`, and there is no
 * second stylesheet. The twenty-odd below are the ones with reach; the rest are
 * one-off and named as exemptions in `style-test.php`, which is the instrument
 * that makes this askable instead of invisible.
 *
 * The three worst are on documents somebody files:
 *
 *   - `.row--total` in `bilan.php`, `flux.php` and `resultat.php` — **the total
 *     line of a balance sheet, rendered exactly like every other line.**
 *   - `.table` and `.table--tight` in nine views, six of them accounting. The
 *     defined selector is `table.data`, so a view that wrote `class="table"`
 *     got a bare browser table inside a styled panel.
 *   - `.notice--warn` in `bilan.php`, `resultat.php` and `statement_lines.php`
 *     — the warning under a statement whose coverage is incomplete, which is
 *     0.88.0's own finding rendered in body text.
 *
 * This is the third release running in which a class an engineer wrote in good
 * faith had no rule behind it: `.alert--danger` at 0.88.0 (26 views), `.pill`
 * and `.alert--warn` here (34 and 14). Each was found by a person looking at
 * one screen. The pattern is the lesson, and the instrument is the answer.
 */
.panel__foot {
  padding: var(--s2) var(--s3);
  min-height: var(--row);
  background: var(--panel-3);
  border-top: 1px solid var(--rule);
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
  border-radius: 0 0 var(--r) var(--r);
}
.panel__foot .spacer { flex: 1; }
.panel--danger { border-color: var(--danger); }

/* A label and its control on one line, wrapping on a phone. */
.field-row { display: flex; gap: var(--s3); flex-wrap: wrap; align-items: flex-end; }
.field-row > .field { flex: 1 1 14rem; min-width: 0; }
.field--inline { display: flex; align-items: center; gap: var(--s2); }
.field--wide { flex-basis: 100%; }
.field-group { display: flex; flex-direction: column; gap: var(--s2); }

.form-grid { display: flex; gap: var(--s3); flex-wrap: wrap; align-items: flex-end; }
.form-grid > .field { flex: 1 1 14rem; min-width: 0; }
.form-grid__actions,
.form-actions { display: flex; gap: var(--s2); flex-wrap: wrap; align-items: center; }
.form-grid__actions { flex-basis: 100%; }
.page-head__actions { display: flex; gap: var(--s2); flex-wrap: wrap; align-items: center; }

.filters { display: flex; gap: var(--s2); flex-wrap: wrap; align-items: flex-end; margin-bottom: var(--s3); }

/*
 * `.table` is what nine views wrote for the selector defined as `table.data`.
 * Aliased rather than renamed in those views, for `.pill`'s reason: the name
 * that reads right is the one people write.
 */
table.table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); }
table.table th, table.table td {
  padding: var(--s2) var(--s3); border-bottom: 1px solid var(--rule-soft); text-align: left;
}
table.table thead th { background: var(--panel-3); font-weight: 650; font-size: var(--t-xs); }
table.table--tight th, table.table--tight td { padding: 3px var(--s2); }

/* The line an accountant's eye goes to. */
.row--total > * , tr.row--total > * {
  font-weight: 700; border-top: 2px solid var(--rule); background: var(--panel-2);
}
.row--credit > * { color: var(--ok); }
.row--wrap > * { white-space: normal; }

.inline { display: inline-flex; align-items: center; gap: var(--s2); }
.chip--sm { font-size: 10.5px; padding: 0 var(--s1); }
.badge--sm { font-size: 10.5px; padding: 0 var(--s1); }
.badge--warning { background: var(--warn-soft);   color: var(--warn);   border-color: var(--warn); }
.badge--success { background: var(--ok-soft);     color: var(--ok);     border-color: var(--ok); }

.notice--warn,
.notice--warning { border-color: var(--warn); color: var(--warn); }
.notice--ok { border-color: var(--ok); color: var(--ok); }

.stat-row { display: flex; gap: var(--s4); flex-wrap: wrap; }
.stat__label { display: block; font-size: var(--t-xs); color: var(--ink-3); }
.stat__value { display: block; font-size: var(--t-lg); font-weight: 700; }

/*
 * A panel that says a screen does not do what it looks like it does.
 *
 * Added in 0.16.0 for the honest-limits notice: where a control is configured
 * but not yet enforced, the product says so on the screen rather than letting
 * somebody rely on it. Styled to be read, not skimmed past — the whole failure
 * mode being addressed is a person trusting a setting.
 */
.panel--warn { border-color: var(--warn); background: var(--warn-soft); }
.panel--warn .panel__body { color: var(--ink); }
.panel--warn a { color: inherit; }

.grid { display: grid; gap: var(--s3); }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
.grid--4 { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }

/* Genuine halves for forms. auto-fit is right for stat tiles, which are happy
   at any width, and wrong for panels of fields: with six children it packs
   four across and truncates every label. */
.grid--halves { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 1100px) { .grid--halves { grid-template-columns: 1fr; } }

/*
 * The tile, tightened (0.41.0).
 *
 * Five of these across the top of the dashboard were 100px tall for one
 * number, which is a poster rather than an instrument. The label leads, the
 * figure follows immediately under it, and the whole tile is the height of
 * two table rows — so the row of figures and the table below it read as one
 * screen instead of two.
 */
.stat {
  background: var(--panel); border: 1px solid var(--rule-soft);
  border-radius: var(--r); padding: var(--s2) var(--s3) 10px;
  box-shadow: var(--shadow);
}
.stat .label {
  font-size: var(--t-xs); font-weight: 600; color: var(--ink-3);
  letter-spacing: .04em; text-transform: uppercase;
  display: block; margin-bottom: var(--s0);
}
/* The figure is the point of the tile, so it is set in the text face at a
   size that reads as a headline rather than in mono at 25px, which made every
   dashboard look like a server console. */
.stat .value {
  font-size: var(--t-2xl); font-weight: 680; letter-spacing: -.026em;
  line-height: 1.15; font-variant-numeric: tabular-nums;
}
.stat .note { font-size: var(--t-xs); color: var(--ink-3); }
.stat--warn   { border-color: var(--warn);   background: color-mix(in srgb, var(--warn-soft) 55%, var(--panel)); }
.stat--danger { border-color: var(--danger); background: color-mix(in srgb, var(--danger-soft) 55%, var(--panel)); }
.stat--danger .value { color: var(--danger); }

/*
 * `stat--bad`, `is-bad` and `is-bad-row` — the names the views actually use.
 *
 * Found by 0.44.0 while checking that a new screen's classes existed: they did
 * not exist for the old ones either. Eleven places across accounting, the
 * kitchen and purchasing have been asking for these since 0.15.0 — the trial
 * balance that does not balance, the posting queue that has fallen behind, the
 * ageing bucket over 120 days, a dish below its target margin, an invoice
 * carrying discrepancies — and every one of them rendered as an ordinary
 * figure.
 *
 * It failed in the direction nobody reports: the number was right, so a screen
 * that had been asked to shout it and did not looked like a screen with nothing
 * to shout about. Aliased rather than renamed in eleven views, because the
 * views are not wrong — the stylesheet was incomplete.
 */
.stat--bad    { border-color: var(--danger); background: color-mix(in srgb, var(--danger-soft) 55%, var(--panel)); }
.stat--bad .value { color: var(--danger); }
td.stat--bad, th.stat--bad { border-color: transparent; background: none; color: var(--danger); font-weight: 600; }
.is-bad       { color: var(--danger); font-weight: 600; }
.is-bad-row > td { background: color-mix(in srgb, var(--danger-soft) 30%, transparent); }

/* ---------------------------------------------------------------- tables */

.table-wrap { overflow-x: auto; }
table.data { width: 100%; border-collapse: collapse; font-size: var(--t-base); }
/*
 * The header is `--panel-3` and small caps from 0.41.0: it has to survive
 * being scrolled under, and a header that differs from its rows only in
 * weight stops looking like a header the moment the first row is bold.
 */
table.data th {
  text-align: left; font-size: var(--t-xs); font-weight: 650;
  letter-spacing: .045em; text-transform: uppercase; color: var(--ink-2);
  padding: var(--s2) var(--s3); border-bottom: 1px solid var(--rule); white-space: nowrap;
  background: var(--panel-3); position: sticky; top: 0; z-index: 1;
}
/*
 * Rows at `--row`, the one height everything in this product uses.
 *
 * 10px of vertical padding on a 14px line gave a 39px row; a buyer reading a
 * hundred-line purchase order saw twenty-two of them on a laptop. This gives
 * twenty-eight, which is a quarter more of the document per screen for no
 * loss of legibility — the line-height did not change, only the air.
 */
table.data td {
  padding: var(--s2) var(--s3);
  height: var(--row);
  border-bottom: 1px solid var(--rule-soft);
  vertical-align: middle;
}
table.data tr:last-child td { border-bottom: 0; }
table.data tbody tr:hover { background: color-mix(in srgb, var(--brand) 6%, transparent); }
/* Right-aligned and tabular, but in the text face: mono figures at body size
   are wider than the labels beside them and make a table of money look like a
   log file. */
table.data td.num, table.data th.num { text-align: right; font-variant-numeric: tabular-nums; }
table.data .sub { font-size: var(--t-xs); color: var(--ink-3); }

/* ---------------------------------------------------------------- badges */

.badge {
  display: inline-block; font-size: var(--t-xs); font-weight: 600;
  padding: 1px var(--s2); border-radius: var(--r-pill); white-space: nowrap;
  background: var(--panel-2); color: var(--ink-2); border: 1px solid var(--rule-soft);
  line-height: 1.5;
}

/*
 * A verdict in a word. Used where a number would be read as a measurement and
 * a colour alone would be read by nobody using a screen reader.
 */
.chip {
  display: inline-block; font-size: var(--t-xs); font-weight: 600;
  padding: 1px var(--s2); border-radius: var(--r-pill);
  background: var(--panel-2); color: var(--ink-2); border: 1px solid var(--rule-soft);
}
.chip--ok     { background: var(--ok-soft);     color: var(--ok);     border-color: color-mix(in srgb, var(--ok) 35%, transparent); }
.chip--warn   { background: var(--warn-soft);   color: var(--warn);   border-color: color-mix(in srgb, var(--warn) 35%, transparent); }
.chip--danger { background: var(--danger-soft); color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, transparent); }
.chip--info   { background: var(--info-soft);   color: var(--info);   border-color: color-mix(in srgb, var(--info) 35%, transparent); }

/*
 * `.pill` is the same thing under the name 34 views actually use, and it had no
 * rule at all until 0.89.0.
 *
 * Measured: `.pill` in 34 views with **zero** rules in this file — the only
 * match for it here was `.pill-row`, a flex container, which is why reading the
 * stylesheet for the word found something and reading it for the *selector*
 * found nothing. Its four modifiers in another 20 views between them, also with
 * nothing. Proved in a browser on the under-insurance screen, which is the one
 * `FR-AST-12` exists for: the shortfall figure computed to the body colour, a
 * transparent background, `0px` radius and `0px none` border, beside a
 * `.chip--danger` inserted for comparison that had all three.
 *
 * **This is 0.88.0's `.alert--danger` finding, five times larger, one release
 * later** — and the reason it is larger is the same reason: nobody could ask
 * the question. So the rules are added here and the question is made askable in
 * `core-test.php`, which now holds every class a view writes against this file.
 *
 * Defined as `.chip`'s aliases rather than as new art: a pill and a chip are
 * one idiom in this product and two names for it is the whole fault. A rename
 * of 50 call sites is the tidier answer and the riskier one; this is reversible
 * in an afternoon once something asserts which views use which.
 */
.pill {
  display: inline-block; font-size: var(--t-xs); font-weight: 600;
  padding: 1px var(--s2); border-radius: var(--r-pill);
  background: var(--panel-2); color: var(--ink-2); border: 1px solid var(--rule-soft);
}
.pill--ok      { background: var(--ok-soft);     color: var(--ok);     border-color: color-mix(in srgb, var(--ok) 35%, transparent); }
.pill--warn,
.pill--warning { background: var(--warn-soft);   color: var(--warn);   border-color: color-mix(in srgb, var(--warn) 35%, transparent); }
.pill--danger  { background: var(--danger-soft); color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, transparent); }
.pill--info    { background: var(--info-soft);   color: var(--info);   border-color: color-mix(in srgb, var(--info) 35%, transparent); }
.pill--muted   { background: var(--panel-2);     color: var(--ink-3);  border-color: var(--rule-soft); }
.badge--ok      { background: var(--ok-soft);     color: var(--ok);     border-color: var(--ok); }
.badge--warn    { background: var(--warn-soft);   color: var(--warn);   border-color: var(--warn); }
.badge--danger  { background: var(--danger-soft); color: var(--danger); border-color: var(--danger); }
.badge--info    { background: var(--info-soft);   color: var(--info);   border-color: var(--info); }
.badge--brand   { background: var(--brand-soft);  color: var(--brand-ink); border-color: var(--brand); }

/* ----------------------------------------------------------------- forms */

.field { margin-bottom: 14px; }
.field > label {
  display: block; font-size: 12.5px; font-weight: 600; margin-bottom: 4px; color: var(--ink);
}
.field .hint { font-size: 12px; color: var(--ink-3); margin-top: 4px; line-height: 1.45; }

input[type="text"], input[type="email"], input[type="password"], input[type="number"],
input[type="search"], input[type="url"], input[type="tel"], input[type="date"],
input[type="file"], select, textarea {
  width: 100%;
  /* 34px tall rather than 30px. Decision 21 put this product on a tablet at a
     counter, and the same fields are used with a thumb. */
  padding: var(--s2) 11px;
  font: inherit; font-size: 13.5px;
  color: var(--ink); background: var(--panel);
  border: 1px solid var(--rule); border-radius: var(--r-sm);
  transition: border-color .12s ease, box-shadow .12s ease;
}
@media (prefers-reduced-motion: reduce) {
  input, select, textarea { transition: none; }
}
input:hover:not([disabled]), select:hover:not([disabled]), textarea:hover { border-color: var(--ink-3); }
input:focus, select:focus, textarea:focus {
  border-color: var(--brand); outline: none; box-shadow: var(--ring);
}
input[type="color"] { width: 54px; height: 32px; padding: 2px; border: 1px solid var(--rule); background: var(--panel); border-radius: var(--radius); }
textarea { min-height: 74px; resize: vertical; }
input[disabled], select[disabled] { background: var(--panel-2); color: var(--ink-3); }

.check { display: flex; align-items: flex-start; gap: 8px; font-size: 13px; margin-bottom: 6px; }
.check input { margin-top: 3px; flex: none; }

.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.row--tight { gap: 6px; }
.row .spacer { flex: 1; }

/* --------------------------------------------------------------- buttons */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  font: inherit; font-size: 13.5px; font-weight: 600;
  padding: var(--s2) var(--s4); border-radius: var(--r-sm); cursor: pointer;
  border: 1px solid var(--rule); background: var(--panel); color: var(--ink);
  text-decoration: none; white-space: nowrap;
  box-shadow: var(--shadow);
  transition: background-color .12s ease, border-color .12s ease, box-shadow .12s ease;
}
@media (prefers-reduced-motion: reduce) { .btn { transition: none; } }
.btn:hover { background: var(--panel-2); text-decoration: none; }
.btn:active { box-shadow: none; }
/*
 * `var(--on-brand)`, not `#fff`.
 *
 * White is right on a dark green and wrong on a light one — and in dark mode
 * the brand is *always* light, because it has been lightened to be readable
 * against a near-black ground. So every primary button in dark mode was white
 * on pale teal, around 2:1. The branding screen measures contrast and reports
 * it in words; a product that then ignores it on its own buttons is doing the
 * thing decision 25 is about.
 */
.btn--primary { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
.btn--primary:hover { background: var(--brand-ink); border-color: var(--brand-ink); color: var(--on-brand); }
.btn--danger { background: var(--danger); border-color: var(--danger); color: #fff; }
.btn--danger:hover { filter: brightness(.93); }
.btn--ghost { background: transparent; border-color: transparent; box-shadow: none; }
.btn--ghost:hover { background: var(--panel-2); }
.btn--sm { padding: var(--s1) 10px; font-size: var(--t-sm); font-weight: 600; }
.btn[disabled] { opacity: .5; cursor: not-allowed; box-shadow: none; }

/* -------------------------------------------------------------- messages */

.alert {
  padding: var(--s3) var(--s4); border-radius: var(--r); margin-bottom: var(--s4);
  border: 1px solid var(--rule-soft);
  border-left: 3px solid var(--ink-3);
  background: var(--panel-2); font-size: 13.5px;
}
.alert--success { border-left-color: var(--ok);     background: var(--ok-soft);     color: var(--ok); }
.alert--error   { border-left-color: var(--danger); background: var(--danger-soft); color: var(--danger); }
.alert--warning { border-left-color: var(--warn);   background: var(--warn-soft);   color: var(--warn); }
.alert--info    { border-left-color: var(--info);   background: var(--info-soft);   color: var(--info); }

/*
 * `--danger` is the name twenty-six views actually write, and until 0.88.0 it
 * had no rule at all.
 *
 * The modifier that exists here is `--error`; `AGENT-CONVENTIONS.md` §6 has
 * been telling every worker to use `.alert--danger` since it was written, and
 * the CSS variable is `--danger` too, so the wrong one is the one that reads
 * right. The result was a banner saying *this shop's books disagree with its
 * takings* rendered in the same grey as a note — the house style's own naming
 * drift, costing colour on the twenty-six screens that most need it.
 *
 * Both names are kept rather than one renamed: a rename is twenty-six view
 * edits and a worker will write `--danger` again next release, because that is
 * what the convention document says. The convention document now says both.
 */
.alert--danger  { border-left-color: var(--danger); background: var(--danger-soft); color: var(--danger); }

/*
 * And two more of the same at 0.89.0, found by the package that measured every
 * class on the under-insurance screen: `.alert--warn` in **14** views with no
 * rule (the defined modifier is `--warning`), and `.alert--ok` in 2 (the
 * defined one is `--success`). The *everything is covered* banner on that
 * screen was `alert--ok` and computed grey — a reassurance rendered as a note.
 *
 * Both spellings kept rather than one renamed, for `--danger`'s reason: a
 * worker writes the word that reads right, and this product's own CSS variables
 * are `--ok`, `--warn` and `--danger`, so the short forms are the ones that
 * read right. The long forms stay because 30-odd views use them.
 */
.alert--warn    { border-left-color: var(--warn);   background: var(--warn-soft);   color: var(--warn); }
.alert--ok      { border-left-color: var(--ok);     background: var(--ok-soft);     color: var(--ok); }

/*
 * `.notice` is styled in `layout/print.php`'s inline block — for a receipt
 * roll, white on dark, hidden when the paper prints — and had no rule in this
 * file, so the nine screen views that use it rendered unstyled text where a
 * framed note was meant (0.88.0). Deliberately quieter than `.alert`: a notice
 * states a condition of the page, and an alert is addressed to the reader.
 */
.notice {
  padding: var(--s3) var(--s4); border-radius: var(--r); margin-bottom: var(--s4);
  border: 1px dashed var(--rule-soft);
  background: var(--panel-2); color: var(--ink-2); font-size: 13px;
}
.notice--error { border-color: var(--danger); color: var(--danger); }
.notice strong { font-weight: 700; color: inherit; }

.alert strong { font-weight: 700; }
.alert code { background: rgba(0,0,0,.05); padding: 1px 4px; border-radius: 2px; }

/* ------------------------------------------------------------- installer */

.installer { max-width: 760px; margin: 0 auto; padding: 40px 22px 80px; }
.installer__mark {
  font-weight: 800; font-size: 13px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--brand); margin-bottom: 6px;
}
.installer h1 { font-size: 26px; margin-bottom: 8px; }
.installer .lede { color: var(--ink-2); font-size: 15px; max-width: 62ch; }

.steps { display: flex; flex-wrap: wrap; gap: 0; margin: 26px 0 24px; border: 1px solid var(--rule); border-radius: var(--radius); overflow: hidden; }
.steps .step {
  flex: 1 1 90px; padding: 8px 10px; font-size: 11.5px; text-align: center;
  border-right: 1px solid var(--rule); background: var(--panel); color: var(--ink-3);
}
.steps .step:last-child { border-right: 0; }
.steps .step.is-done { background: var(--brand-soft); color: var(--brand-ink); }
.steps .step.is-current { background: var(--brand); color: var(--on-brand); font-weight: 700; }

.check-list { border: 1px solid var(--rule); border-radius: var(--radius); overflow: hidden; }
.check-list__row {
  display: grid; grid-template-columns: 24px 1fr auto; gap: 10px; align-items: start;
  padding: 9px 13px; border-bottom: 1px solid var(--rule-soft); background: var(--panel); font-size: 13px;
}
.check-list__row:last-child { border-bottom: 0; }
.check-list__row .mark { font-weight: 700; }
.check-list__row.is-ok   .mark { color: var(--ok); }
.check-list__row.is-fail .mark { color: var(--danger); }
.check-list__row .fix { font-size: 12px; color: var(--ink-3); margin-top: 3px; line-height: 1.45; }
.check-list__row .actual { font-family: var(--mono); font-size: 11.5px; color: var(--ink-3); white-space: nowrap; }

.progress { height: 6px; background: var(--panel-2); border-radius: 3px; overflow: hidden; border: 1px solid var(--rule); }
.progress > i { display: block; height: 100%; background: var(--brand); width: 0; transition: width .25s ease; }
@media (prefers-reduced-motion: reduce) { .progress > i { transition: none; } }

.log-box {
  font-family: var(--mono); font-size: 11.5px; line-height: 1.65; background: var(--panel);
  border: 1px solid var(--rule); border-radius: var(--radius);
  padding: 12px 14px; max-height: 260px; overflow: auto; white-space: pre-wrap; color: var(--ink-2);
}

/* ------------------------------------------------------------ public page */

.public { max-width: 620px; margin: 0 auto; padding: 44px 22px 80px; }
.verdict {
  border: 2px solid var(--rule); border-radius: var(--radius);
  padding: 18px 20px; background: var(--panel); margin-bottom: 18px;
}
.verdict__label { font-size: 19px; font-weight: 800; letter-spacing: -.01em; display: flex; align-items: center; gap: 9px; }
.verdict__note { color: var(--ink-2); margin: 7px 0 0; font-size: 13.5px; }
.verdict--ok      { border-color: var(--ok); }
.verdict--ok      .verdict__label { color: var(--ok); }
.verdict--danger  { border-color: var(--danger); }
.verdict--danger  .verdict__label { color: var(--danger); }
.verdict--warn    { border-color: var(--warn); }
.verdict--warn    .verdict__label { color: var(--warn); }
.verdict--info    { border-color: var(--info); }
.verdict--info    .verdict__label { color: var(--info); }

.kv { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.kv th { text-align: left; font-weight: 500; color: var(--ink-3); padding: 6px 0; width: 42%; vertical-align: top; }
.kv td { padding: 6px 0; font-weight: 600; }

.masked { background: var(--panel-2); border: 1px dashed var(--rule); border-radius: var(--radius); padding: 13px 15px; margin-top: 14px; }
.masked .lockline { font-size: 12.5px; color: var(--ink-3); margin-bottom: 9px; }
.masked .blur { color: var(--ink-3); letter-spacing: .16em; font-family: var(--mono); }

/* ----------------------------------------------------------------- misc */

.muted { color: var(--ink-3); }
.text-danger { color: var(--danger); }
.text-warn   { color: var(--warn); }
.text-ok     { color: var(--ok); }
.small { font-size: 12px; }
.strong { font-weight: 600; }
.right { text-align: right; }
.mt { margin-top: 14px; }
.mb { margin-bottom: 14px; }
.divider { height: 1px; background: var(--rule); margin: 14px 0; border: 0; }
.pill-row { display: flex; gap: 6px; flex-wrap: wrap; }

.matrix-wrap { overflow: auto; max-height: 70vh; }
table.matrix { border-collapse: collapse; font-size: 12.5px; width: 100%; }
table.matrix th, table.matrix td { border: 1px solid var(--rule-soft); padding: 5px 8px; }
table.matrix thead th { position: sticky; top: 0; background: var(--panel-2); z-index: 2; font-size: 11px; }
table.matrix tbody th {
  position: sticky; left: 0; background: var(--panel); text-align: left; font-weight: 500;
  font-family: var(--mono); font-size: 11px; white-space: nowrap; z-index: 1;
}
table.matrix td { text-align: center; }
table.matrix tr.module-head th { background: var(--panel-2); font-family: var(--sans); font-weight: 700; font-size: 11.5px; }

/* ===========================================================================
   The small screen (0.47.0).

   The product was measured at 390 px before any of this was written, and the
   answer was 97 tables laid out at their natural width inside a 390 px column:
   ten screens slid the whole page sideways, the worst by 398 px. `.table-wrap`
   has existed since 0.15.0 and had been applied to eighteen of a hundred and
   fifteen tables — the wrapper was the exception, not the rule, and a page
   overflows because of the one table nobody wrapped however many others are
   right. `mobile-test.php` is what keeps that true; these rules are the rest.
   =========================================================================== */

/*
 * The menu, folded away.
 *
 * `<details>` around the whole nav, with the summary hidden above the
 * breakpoint so a desktop can never collapse its own menu by accident, and
 * `nav.js` closing it on a narrow viewport. On a phone the sidebar sits above
 * the content, so an expanded menu is one to two screenfuls between somebody
 * and the page they asked for.
 */
.nav-shell__toggle { display: none; }

@media (max-width: 860px) {
  .shell { grid-template-columns: 1fr; }
  .sidebar { position: static; height: auto; border-right: 0; border-bottom: 1px solid var(--rule); }
  /*
   * The whole menu used to be laid out as a grid of links on a narrow screen.
   * With folding groups that is the wrong unit — a `<details>` would have
   * become one cell and its contents would have had to lay themselves out
   * inside it. Folding is doing the same job the grid was doing, and better:
   * it is the links, not the columns, that make the list long.
   */
  .sidebar .nav-list { display: block; }
  .nav-fold > summary.nav-group { padding-top: var(--s3); padding-bottom: var(--s2); }

  .nav-shell__toggle {
    display: flex; align-items: center; gap: var(--s3); cursor: pointer;
    /* 44 px tall, because it is the one control on a phone that everything else
       is reached through. */
    min-height: 44px; padding: 0 var(--s2);
    font-size: var(--t-md); font-weight: 650; color: var(--rail-ink);
    list-style: none;
  }
  .nav-shell__toggle::-webkit-details-marker { display: none; }

  /* Three bars, drawn rather than fetched — the icon set is inlined per page
     and this one mark does not need to be in it. */
  .nav-shell__bars {
    width: 18px; height: 2px; background: currentColor; border-radius: 2px;
    box-shadow: 0 -5px 0 currentColor, 0 5px 0 currentColor;
  }

  .nav-shell[open] > .nav-shell__toggle { margin-bottom: var(--s2); }
}

@media (max-width: 760px) {
  /*
   * Touch targets. 13.5 px type in 4 px of padding is a 26 px button, which is
   * fine with a mouse and a coin-toss with a thumb — and the buttons this
   * applies to are the ones in table rows, where they sit beside each other.
   *
   * Only the height is raised, and only here: a wider button would reflow every
   * page-head on a phone, and the desktop measurements the theme drive takes
   * must not move.
   */
  .btn { min-height: 38px; }
  .btn--sm { min-height: 34px; }

  /*
   * And a button's label may wrap.
   *
   * `.btn` is `white-space: nowrap` by design — a wrapped label in a page head
   * reads as a broken button. On a phone the opposite is true: *Réinitialiser la
   * démonstration* is 377 px of unbreakable text in a 358 px column, and it takes
   * the whole page sideways with it. The battery found this one on the
   * demonstration screen, at a width where the label happened to be the longest
   * thing on the page.
   */
  .btn { white-space: normal; }
  .content input, .content select, .content textarea { min-height: 40px; }

  /* A page head whose title and buttons fight for one line reads as a broken
     header; stacked, it reads as a heading with actions under it. */
  .page-head { gap: var(--s2); }
  .page-head .spacer { flex-basis: 100%; }

  /* Four-across stat grids become two: a figure squeezed into 90 px wraps in
     the middle of the number, which is the one thing a figure must not do. */
  .grid--4, .grid--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* The filter bars are built as `.row` with `flex-wrap`, and their fields
     carry inline `max-width` — which on a phone leaves a 240 px field beside a
     40 px gap. Full width, stacked. */
  .panel__body > form .field { max-width: none !important; }

  /*
   * `!important`, and it is the right tool here rather than a shortcut.
   *
   * Forty views carry `style="flex-wrap:nowrap"` on a search row — a **view**
   * asserting a layout decision that happens to be correct at 1400 px and wrong
   * at 360, where a 240 px field plus a button is 368 px in a 334 px column.
   * Responsive decisions belong in the stylesheet, and an inline style can only
   * be answered from here with `!important`. The alternative was editing forty
   * views to move a decision none of them should have been making.
   */
  .content .row { flex-wrap: wrap !important; }

  /*
   * And `min-width: 0` on what is inside one, because a flex item's default is
   * `auto` — it refuses to shrink below its own content however narrow the row
   * gets. A file-upload form nested in an alert's `.row` was 295 px inside a
   * 260 px row at 320 px wide, which is the whole product scrolling sideways for
   * one field nobody uses twice a year.
   */
  .content .row > * { min-width: 0; }

  /*
   * And a field in a row takes the whole line rather than a share of it.
   *
   * `min-width: 0` alone let three fields squash to 59 px each, which stops the
   * page overflowing and produces a form nobody can read — labels overflowing
   * their own boxes, inputs too narrow for the value in them. At 320 px that was
   * still one pixel of page overflow from a label that could not break; at 360 it
   * was simply unusable. Stacked, a field is as wide as the column.
   *
   * `!important` for the reason the wrap rule above gives: the views carry
   * `style="flex:1"` on these fields, which is a view deciding a layout that is
   * right at 1400 px and wrong at 360.
   */
  .content .row > .field,
  .content form .row > label { flex: 1 1 100% !important; }
  /*
   * Every input, and the selector says so the long way round: an `input` with no
   * `type` attribute at all is a text field and `input[type="text"]` does not
   * match it. The first version of this rule missed the backups filename field
   * for exactly that reason, and the measurement at 320 px is what said so.
   */
  .content input:not([type="checkbox"]):not([type="radio"]):not([type="color"]) {
    max-width: 100%;
  }

  /*
   * A file input needs `width` and not `max-width`: its intrinsic size is the
   * browser's own button plus the filename, and as a flex item with the default
   * `min-width: auto` it declines to go below that — 295 px inside a 294 px
   * column, which is a six-pixel sideways scroll on every screen in the product
   * for one upload field.
   */
  .content input[type="file"] { width: 100%; min-width: 0; }

  /* A backup filename, a checksum, a key id — long, unbreakable, and the one
     kind of string that pushes a page sideways with no layout involved. */
  .content code { overflow-wrap: anywhere; }

  /*
   * And ordinary prose, because a long word is the same defect with no code in it.
   *
   * Found on the **upgraded** install and not the development one: a customer
   * screen and the site editor each carried a sentence whose longest word did not
   * fit a 116 px box, and the box let it stick out. Inherited from `.content`
   * rather than listed per element, because the next one will be in an element
   * nobody listed. `break-word` and not `anywhere`: it breaks a word only when the
   * word cannot fit, so normal text still wraps between words.
   */
  .content { overflow-wrap: break-word; }

  /*
   * A badge is `white-space: nowrap`, which is right for *en service* and wrong for
   * *n'apparaîtra pas : aucune succursale n'a d'horaires* — a sentence that the
   * site editor puts in one, 307 px of it in a 268 px column. On a phone a badge
   * carrying a sentence wraps; the short ones are unaffected because they fit.
   */
  .content .badge { white-space: normal; }

  .content { padding-left: var(--s3); padding-right: var(--s3); }
}

@media (max-width: 420px) {
  /* One column, because two 170 px figures side by side is where the wrapping
     starts again. */
  .grid--4, .grid--3, .grid--halves { grid-template-columns: 1fr; }
}

@media print {
  .sidebar, .topbar, .btn { display: none !important; }
  body { background: #fff; }
}

/* ===========================================================================
   The item picker.

   Used wherever an item is chosen — purchase requisitions, purchase orders,
   promotions, price lists. The list is positioned absolutely inside a
   `position: relative` wrapper so it can overflow a table cell: these pickers
   live in multi-line tables and a list clipped by its row would show two
   results out of twelve.
   ======================================================================== */

.picker        { position: static; }
.picker__field { position: relative; }

.picker__input { width: 100%; }

.picker.is-chosen .picker__input {
  /* A chosen item reads as settled rather than as text somebody is still
     typing — the difference matters on a six-line order form where the eye
     needs to find the one line that is not done yet. */
  border-color: var(--brand);
  background: var(--brand-soft);
  font-weight: 600;
}

.picker__list {
  position: absolute;
  z-index: 40;
  top: calc(100% + 2px);
  left: 0;
  right: 0;
  min-width: 30rem;
  max-height: 21rem;
  overflow-y: auto;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: 0 6px 24px rgba(28, 35, 32, .16);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .picker__list {
    box-shadow: 0 6px 24px rgba(0, 0, 0, .55);
  }
}

.picker__opt {
  display: flex;
  justify-content: space-between;
  /* `flex-start`, not `baseline`: a product name long enough to wrap pushed
     the price and the stock figure down to sit beside its second line, which
     read as though they belonged to a different row. */
  align-items: flex-start;
  gap: 14px;
  padding: 7px 9px;
  border-radius: var(--radius);
  cursor: pointer;
}

/* Hover and keyboard selection are the same state deliberately: two different
   highlights on one list is how somebody presses Enter and gets the other one. */
.picker__opt:hover,
.picker__opt.is-active {
  background: var(--brand-soft);
}

.picker__opt-main   { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1 1 auto; }
.picker__opt-main strong { font-weight: 600; }
.picker__dci        { font-size: 12px; color: var(--ink-3); }

.picker__opt-meta {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--ink-2);
  white-space: nowrap;
}

.picker__sku    { font-family: var(--mono); font-size: 11.5px; color: var(--ink-3); background: none; }
.picker__price  { font-variant-numeric: tabular-nums; }
.picker__stock  { font-variant-numeric: tabular-nums; color: var(--ok); }

/* A bare red `0` beside a price reads as an error, not as a fact about the
   shelf. Out of stock is said in a word. */
.picker__stock.is-out { color: var(--danger); font-variant-numeric: normal; }

.picker__note {
  margin: 4px 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--ink-3);
}

.picker__note.is-error { color: var(--danger); }

/* ---------------------------------------------------- active ingredients */

/*
 * A product with more than one active ingredient, marked on its row.
 *
 * Amber rather than red: a combination is not an error, it is a different
 * medicine — and a cashier searching for amoxicilline may well want the
 * co-amoxiclav. What they must not do is take it for the other thing.
 */
.picker__combined {
  /* `align-self`, because the option's main column is a flex column and a
     plain span stretches to its full width — which drew the mark as a bar
     across the row rather than as a label on it. */
  align-self: flex-start;
  margin-top: 2px;
  padding: 0 5px;
  border-radius: 3px;
  font-size: 11px;
  font-weight: 600;
  color: var(--warn);
  background: color-mix(in srgb, var(--warn-soft) 70%, transparent);
}

/* Sits inside the list, under the options — see `Picker.prototype.agents`. */
.picker__agents {
  margin: 0;
  padding: 6px 9px;
  border-top: 1px solid var(--rule);
  font-size: 12px;
  line-height: 1.45;
  color: var(--ink-3);
}

.picker__agents-lede { margin-right: 6px; }
.picker__agent       { margin-right: 8px; white-space: nowrap; }

/* ===========================================================================
   The shop's own marks.

   `brand.logo_path` was a setting nothing read from 0.1.0 to 0.17.0 — a field
   on a screen that implied the product supported a logo when nothing in it
   did. These are the places it now appears.
   ======================================================================== */

.brand--logo { padding: 0; }
.brand--logo img {
  display: block;
  max-width: 100%;
  max-height: 44px;
  /* `contain` rather than `cover`: a logo cropped to fill its box is a logo
     with somebody's name cut off. */
  object-fit: contain;
  object-position: left center;
}

.signin-mark { text-align: center; margin-bottom: 22px; }
.signin-mark img { display: block; max-width: 190px; max-height: 78px; margin: 0 auto 10px; object-fit: contain; }

.public__logo { max-height: 34px; max-width: 140px; object-fit: contain; }

.receipt-logo { display: block; max-width: 58%; max-height: 70px; margin: 0 auto 6px; object-fit: contain; }

/* --- the branding screen ------------------------------------------------ */

.colour-row { display: flex; align-items: center; gap: 10px; }
.colour-row input[type="color"] {
  width: 56px; height: 38px; padding: 2px;
  border: 1px solid var(--rule); border-radius: var(--radius);
  background: var(--panel); cursor: pointer;
}
.colour-row input[type="text"] { max-width: 10rem; }

.brand-preview__logo-box {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 14px 18px; margin-bottom: 10px;
  /* A chequerboard, so a transparent logo is visibly transparent rather than
     looking as though it has a white background it does not have. */
  background-color: var(--panel-2);
  background-image:
    linear-gradient(45deg, var(--rule-soft) 25%, transparent 25%, transparent 75%, var(--rule-soft) 75%),
    linear-gradient(45deg, var(--rule-soft) 25%, transparent 25%, transparent 75%, var(--rule-soft) 75%);
  background-size: 16px 16px;
  background-position: 0 0, 8px 8px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

.brand-logo-sample { max-width: 220px; max-height: 90px; object-fit: contain; display: block; }

.brand-preview {
  display: flex;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
  min-height: 118px;
}

.brand-preview__nav {
  flex: 0 0 190px;
  padding: 12px;
  background: var(--brand);
  color: var(--on-brand);
  display: flex; flex-direction: column; gap: 5px;
}

.brand-preview__mark { margin-bottom: 8px; min-height: 30px; }
.brand-preview__mark img { max-width: 100%; max-height: 30px; object-fit: contain; object-position: left; }
.brand-preview__initials { font-size: 21px; font-weight: 700; letter-spacing: .06em; }

.brand-preview__navitem { font-size: 13px; opacity: .74; }
.brand-preview__navitem.is-on {
  opacity: 1; font-weight: 600;
  background: rgba(255, 255, 255, .16);
  border-radius: var(--radius);
  padding: 3px 7px; margin: -3px -7px;
}

.brand-preview__body {
  flex: 1 1 auto;
  padding: 16px;
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  background: var(--panel);
}

.brand-preview__link { color: var(--brand); text-decoration: underline; font-size: 14px; }
.brand-preview .btn--primary { background: var(--brand); border-color: var(--brand); opacity: 1; }
.brand-preview .badge { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }

.brand-actions { display: flex; align-items: center; gap: 12px; }

/* ===========================================================================
   Density corrections found by looking at real screens.

   The restyle enlarged buttons and inputs for the tablet at the counter
   (decision 21), which is right on a form and wrong in a list: a full-size
   *Modifier* button repeated forty times down a table takes a fifth of the
   width and pulls the eye away from the data, which is what somebody came to
   the screen to read.
   ======================================================================== */

table.data .btn {
  padding: var(--s1) 10px;
  font-size: var(--t-sm);
  box-shadow: none;
}
table.data td:last-child:has(.btn) { width: 1%; white-space: nowrap; text-align: right; }

/* A row of actions in a table cell should not wrap onto two lines. */
table.data .row { flex-wrap: nowrap; justify-content: flex-end; gap: var(--s1); }

/* Filter bars: the submit button should sit on the same baseline as the
   fields beside it rather than being pushed down by their labels. */
.panel__body > .grid--4 .btn,
.panel__body > .grid--3 .btn { align-self: end; }

/* ===========================================================================
   The storefront (FR-SHOP, 0.40.0).

   In this file rather than a stylesheet of its own, and that is a decision
   rather than laziness. A separate storefront stylesheet would have had to
   restate the palette — `--brand`, `--panel`, `--ink`, the dark-mode
   redefinitions — which means two answers to what this shop's green is, and
   the one that is not `Branding::palette()` would be wrong the first time
   somebody changed it.

   The cost is that a shopper downloads the whole file. It is 37 KB, served
   once and cached for every page after the first, which leaves the rest of
   `FR-SHOP-08`'s budget for the page itself — and the requirement's real
   demand, that text paints before pictures on a slow connection, is met by
   the storefront having no script at all and no web font.
   ======================================================================== */

.shop-body {
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 var(--s4) var(--s7);
  background: var(--ground);
}

.shop-head { padding: var(--s4) 0; border-bottom: 1px solid var(--rule); }

.shop-head__bar { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }

.shop-head__mark {
  display: inline-flex; align-items: center; gap: var(--s2);
  font-weight: 650; font-size: var(--t-lg); color: var(--ink); text-decoration: none;
}

.shop-head__cart {
  display: inline-flex; align-items: center; gap: var(--s2);
  padding: var(--s2) var(--s3);
  border: 1px solid var(--rule); border-radius: var(--r);
  color: var(--ink); text-decoration: none; background: var(--panel);
}

.shop-head__tools {
  display: flex; align-items: flex-end; gap: var(--s4);
  flex-wrap: wrap; margin-top: var(--s3);
}

.shop-search { display: flex; gap: var(--s2); flex: 1 1 320px; }
.shop-search input { flex: 1 1 auto; }

.shop-picker { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.shop-picker label { font-size: var(--t-sm); color: var(--ink-3); }
.shop-picker__one { margin: 0; }

.shop-main { padding-top: var(--s5); }
.shop-foot { margin-top: var(--s7); padding-top: var(--s4); border-top: 1px solid var(--rule); }

/* ===========================================================================
   The public site (0.45.0).

   Shares the storefront's chrome, its palette tokens and its stylesheet — a
   second sheet would have had to restate what this shop's green is, and the
   copy that was not `Branding::palette()` would be wrong the first time
   somebody changed it.

   No grid framework and no breakpoint soup: one `auto-fit` grid does the
   cards, which reflows from three columns to one without a media query. The
   requirement behind that is `FR-SHOP-08` — a text-priority rendering path on
   Cameroonian mobile data — and the cheapest layout is the one the browser
   already knows how to do.
   =========================================================================== */

.site-block { margin: 0 0 var(--s7); }
.site-block > h2 { margin: 0 0 var(--s3); }

/* -------------------------------------------------------------- the hero */

.site-hero {
  padding: var(--s7) var(--s5);
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--brand-soft) 60%, var(--panel));
}

/*
   A photograph behind the words, with a scrim over it.

   The scrim is not decoration: a hero image is chosen by a shop owner, not by
   a designer, and white text over an unknown photograph is unreadable about
   half the time. The gradient guarantees the contrast whatever they upload.
*/
.site-hero--image {
  background-size: cover;
  background-position: center;
  position: relative;
  color: #fff;
}
.site-hero--image::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(180deg, rgba(0,0,0,.30), rgba(0,0,0,.65));
}
.site-hero__inner { position: relative; max-width: 46rem; }
.site-hero h1 { margin: 0 0 var(--s2); font-size: clamp(1.7rem, 5vw, 2.6rem); line-height: 1.12; }
.site-hero__lede { margin: 0 0 var(--s3); font-size: var(--t-lg); }
.site-hero__body { margin: 0 0 var(--s4); }
.site-hero--image .site-hero__lede,
.site-hero--image .site-hero__body { color: rgba(255,255,255,.92); }

/* ------------------------------------------------------------- the cards */

.site-cards {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}

.site-card {
  padding: var(--s4);
  border: 1px solid var(--rule);
  border-radius: var(--r);
  background: var(--panel);
}
.site-card h3 { margin: 0 0 var(--s2); font-size: var(--t-base); }
.site-card__line { margin: 0 0 var(--s1); }
.site-card--product { display: block; text-decoration: none; color: inherit; }
.site-card--product:hover { border-color: var(--brand); }

.site-prose { max-width: 42rem; }

/* ------------------------------------------------------------- the hours */

.site-hours { width: 100%; border-collapse: collapse; font-size: var(--t-sm); }
.site-hours th { text-align: left; font-weight: 500; color: var(--ink-2); padding: 3px 0; }
.site-hours td { text-align: right; font-variant-numeric: tabular-nums; padding: 3px 0; }
/* Today, marked — the row somebody is actually looking for. */
.site-hours .is-today th,
.site-hours .is-today td { font-weight: 700; color: var(--ink); }
/* A note is a second line, not a continuation of the times — "08:00–14:00
   Samedi : matinée seulement" read as one string in the first screenshot. */
.site-hours .sub { display: block; font-weight: 400; }

/* ----------------------------------------------------------- the gallery */

.site-gallery {
  display: grid;
  gap: var(--s3);
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}
.site-gallery img {
  width: 100%;
  height: 12rem;
  object-fit: cover;
  border-radius: var(--r);
  display: block;
}

/* ------------------------------------------------------ the call to action */

.site-cta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4);
  align-items: center;
  justify-content: space-between;
  padding: var(--s5);
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--accent-soft) 60%, var(--panel));
}
.site-cta h2 { margin: 0 0 var(--s1); }
.site-cta p  { margin: 0; }

.shop-head__shop { font-weight: 600; }

/* ------------------------------------------------------------ the editor */

.site-admin__block + .site-admin__block { border-top: 1px solid var(--rule); }
.site-admin__edit summary { cursor: pointer; color: var(--brand); }
.site-admin__form { margin-top: var(--s3); }
.site-admin__times { display: inline-flex; gap: 4px; white-space: nowrap; }
.site-admin__times input { width: 7.5rem; }
.site-admin__thumbs { display: flex; flex-wrap: wrap; gap: 8px; margin-top: var(--s3); }
.site-admin__thumbs img { object-fit: cover; border-radius: var(--r-sm); }

/* A hero button wants to be bigger than a toolbar's. */
.btn--lg { padding: 10px 18px; font-size: var(--t-base); }

/* The two-column shelf. One column under 760 px, with the filters below the
   goods rather than above them: on a phone, a screenful of checkboxes before
   the first product is a shop that looks empty. */
.shop-layout { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: var(--s5); }
.shop-layout--wide { grid-template-columns: minmax(0, 1fr); }

@media (max-width: 760px) {
  .shop-layout { grid-template-columns: 1fr; }
  .shop-side { order: 2; }
  .shop-results { order: 1; }
}

.shop-side__title {
  font-size: var(--t-sm); text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-3); margin: 0 0 var(--s2);
}

.shop-cats ul, .shop-facet ul, ul.shop-cats { list-style: none; margin: 0 0 var(--s5); padding: 0; }
.shop-cats a, .shop-facet a {
  display: block; padding: var(--s1) 0; color: var(--ink-2); text-decoration: none;
  font-size: var(--t-sm);
}
.shop-cats a:hover, .shop-facet a:hover { color: var(--brand); }
.shop-cats a.is-on, .shop-facet a.is-on { color: var(--brand); font-weight: 650; }

/* The departments a shop leads with (0.78.0). Three across on a desktop, two
   on a tablet, one on a telephone — and the tiles are shorter than they are
   wide, because a square tile with a word in it is a card pretending to be a
   product. */
.shop-aisles {
  display: grid; gap: var(--s4); margin: 0 0 var(--s6);
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
}
.shop-aisle {
  display: flex; flex-direction: column;
  color: var(--ink); text-decoration: none;
  background: var(--panel); border: 1px solid var(--rule); border-radius: var(--r-lg);
  overflow: hidden;
  transition: box-shadow .12s ease, transform .12s ease, border-color .12s ease;
}
.shop-aisle:hover, .shop-aisle:focus-visible {
  box-shadow: var(--shadow-lg); border-color: var(--brand); transform: translateY(-2px);
}
.shop-aisle__img {
  display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover;
}
/* The lettered stand-in. Centred, oversized and quiet: it has to read as a
   deliberate tile rather than as an image that failed to load. */
.shop-aisle__img--none {
  display: flex; align-items: center; justify-content: center;
  background: var(--panel-2); color: var(--ink-3);
  font-size: var(--t-3xl); font-weight: 700; letter-spacing: .04em;
}
.shop-aisle__body { display: flex; flex-direction: column; gap: 2px; padding: var(--s3) var(--s4); }
.shop-aisle__name { font-weight: 650; font-size: var(--t-base); }
.shop-aisle__tag { font-size: var(--t-sm); color: var(--ink-2); }

/* Two across on a telephone, shorter, with the tagline on one line.
   Full-width tiles put six screenfuls of departments above the first product,
   which is the same mistake the filter panel makes and the same answer: on a
   phone, the goods come first. */
@media (max-width: 560px) {
  .shop-aisles { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s3); }
  .shop-aisle__img { aspect-ratio: 4 / 3; }
  .shop-aisle__img--none { font-size: var(--t-2xl); }
  .shop-aisle__body { padding: var(--s2) var(--s3); }
  .shop-aisle__name { font-size: var(--t-sm); }
  .shop-aisle__tag { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}

/* The goods. `auto-fill` rather than `auto-fit` so a single result does not
   stretch to the width of the page and stop looking like a product.

   Wider columns since 0.78.0 (150px to 170px): the cards carry photographs
   now, and a 150px column on a phone put two thumbnails side by side at a
   size nobody could see what was in. */
.shop-grid {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--s4);
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
}

/* A card is one object, so the whole of it lifts rather than the picture
   inside it. `flex: 1` on the link pushes the price row to the bottom, which
   is what makes a row of cards with names of different lengths line their
   prices up — the thing that reads as a catalogue rather than as a list. */
.shop-card {
  display: flex; flex-direction: column;
  padding: var(--s3);
  background: var(--panel); border: 1px solid var(--rule); border-radius: var(--r-lg);
  transition: box-shadow .12s ease, transform .12s ease, border-color .12s ease;
}
.shop-card:hover, .shop-card:focus-within {
  box-shadow: var(--shadow-lg); border-color: var(--brand); transform: translateY(-2px);
}
.shop-card a { color: var(--ink); text-decoration: none; }
.shop-card__link { display: flex; flex-direction: column; flex: 1; gap: var(--s2); }
/* The frame keeps the square even when the file is not, so a shelf of
   photographs taken on four different telephones still lines up. */
.shop-card__frame {
  display: block; position: relative; width: 100%; aspect-ratio: 1 / 1;
  background: var(--panel-2); border-radius: var(--r); overflow: hidden;
}
.shop-card__img {
  display: block; width: 100%; height: 100%;
  object-fit: cover; border-radius: var(--r);
}
.shop-card__img--none {
  display: flex; align-items: center; justify-content: center;
  background: var(--panel-2); color: var(--ink-3);
  font-size: var(--t-2xl); font-weight: 700;
}
/* Two lines, then an ellipsis. A name that wrapped to five pushed the price
   out of line with its neighbours, which is the one thing the row has to get
   right. */
.shop-card__name {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: var(--t-base); line-height: 1.3;
}
.shop-card__foot {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s2); margin-top: var(--s3); flex-wrap: wrap;
}
.shop-card__price {
  font-weight: 700; font-size: var(--t-lg); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.shop-pages { display: flex; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s5); }

/* What the shelf can promise. Four states, four colours, and the quantity is
   never one of them. */
.shop-stock {
  display: inline-block; font-size: var(--t-sm); font-weight: 600;
  padding: 2px 8px; border-radius: 999px;
}
.shop-stock--in      { background: var(--ok-soft);     color: var(--ok); }
.shop-stock--low     { background: var(--warn-soft);   color: var(--warn); }
.shop-stock--order   { background: var(--accent-soft); color: var(--ink-2); }
.shop-stock--out     { background: var(--danger-soft); color: var(--danger); }
.shop-stock--service { background: var(--panel-2);     color: var(--ink-3); }

/* `align-items: start` so the facts sit beside the top of the picture rather
   than stretching down it, and a margin so the related shelf below is not
   touching the gallery. */
.shop-product {
  display: grid; grid-template-columns: minmax(0, 420px) minmax(0, 1fr);
  gap: var(--s5); align-items: start; margin-bottom: var(--s6);
}
@media (max-width: 760px) { .shop-product { grid-template-columns: 1fr; } }

.shop-product__image img { width: 100%; height: auto; border-radius: var(--r); }

/* The one being looked at, in a square frame for the reason the cards are:
   the shop's photographs are whatever shape its telephone took them. */
.shop-product__shot {
  display: block; width: 100%; aspect-ratio: 1 / 1;
  object-fit: cover; border-radius: var(--r-lg);
  background: var(--panel-2);
}
.shop-product__shot--none {
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-3); font-size: 64px; font-weight: 700;
  border: 1px dashed var(--rule);
}

/* The thumbnails. Links, so the state is in the address — `is-on` is the one
   being shown, and it is marked with a border rather than with opacity so it
   is still legible to somebody who cannot tell the two greens apart. */
.shop-shots {
  list-style: none; margin: var(--s3) 0 0; padding: 0;
  display: flex; gap: var(--s2); flex-wrap: wrap;
}
.shop-shots__one {
  display: block; border: 2px solid transparent; border-radius: var(--r);
  overflow: hidden; line-height: 0;
}
.shop-shots__one img { width: 72px; height: 72px; object-fit: cover; display: block; }
.shop-shots__one.is-on { border-color: var(--brand); }
.shop-shots__one:hover { border-color: var(--rule); }

/* Where the shopper is. Small, quiet, and above everything else. */
.shop-crumbs {
  display: flex; gap: var(--s2); flex-wrap: wrap; align-items: center;
  font-size: var(--t-sm); color: var(--ink-3); margin-bottom: var(--s4);
}
.shop-crumbs a { color: var(--ink-2); text-decoration: none; }
.shop-crumbs a:hover { color: var(--brand); }

.shop-product__price { font-size: var(--t-3xl); font-weight: 700; margin: 0 0 var(--s1); }
.shop-product__spec { display: grid; grid-template-columns: auto 1fr; gap: var(--s1) var(--s3); margin: var(--s4) 0; }
.shop-product__spec dt { color: var(--ink-3); font-size: var(--t-sm); }
.shop-product__spec dd { margin: 0; }
.shop-product__text { margin: var(--s4) 0; line-height: 1.6; }

.shop-add { display: flex; gap: var(--s2); align-items: center; margin: var(--s4) 0; }
.shop-add__qty { width: 5.5rem; }
.shop-qty { display: flex; gap: var(--s1); align-items: center; }
.shop-qty input { width: 4.5rem; }

.shop-choice {
  display: flex; gap: var(--s3); align-items: flex-start;
  padding: var(--s3); border: 1px solid var(--rule); border-radius: var(--r);
  margin-bottom: var(--s2); cursor: pointer;
}
.shop-choice span { display: flex; flex-direction: column; gap: 2px; }

.shop-checkout .panel { margin-bottom: var(--s4); }

/* Visible to a screen reader, not to the eye. The quantity fields on the
   listing have a visible number beside them and a label that would repeat it;
   leaving the label off entirely is the version that is wrong. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ===========================================================================
   The skin picker (0.41.0).

   The swatches are drawn from the skin's own tokens by the template, so what
   somebody is looking at is the palette itself rather than a picture of it —
   a screenshot would be a second copy, taken once and wrong the day a value
   changed.
   ======================================================================== */

.theme-grid {
  display: grid; gap: var(--s3);
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
}

.theme-card {
  display: flex; flex-direction: column; gap: var(--s1);
  padding: var(--s3);
  border: 1px solid var(--rule); border-radius: var(--r);
  background: var(--panel); cursor: pointer;
  position: relative;
}
.theme-card:hover { border-color: var(--brand); }
.theme-card.is-on { border-color: var(--brand); box-shadow: var(--ring); }
.theme-card input { position: absolute; top: var(--s2); right: var(--s2); }

.theme-card__name { font-weight: 650; }
.theme-card__note { font-size: var(--t-sm); color: var(--ink-2); line-height: 1.4; }
.theme-card__meta { font-size: var(--t-xs); color: var(--ink-3); margin-top: auto; padding-top: var(--s1); }

/* Six bands, no gaps: a palette reads as a palette when the colours touch. */
.theme-swatch {
  display: flex; height: 26px; border-radius: var(--r-sm);
  overflow: hidden; border: 1px solid var(--rule);
}
.theme-swatch i { flex: 1 1 0; display: block; }

/* ------------------------------------------- the item screen's photographs */

/* Not the storefront. This is the panel the person managing the catalogue
   works in, so the pictures are small and the controls are visible rather
   than the other way round. */
.photo-admin {
  display: grid; gap: var(--s4);
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}
.photo-admin__cell {
  margin: 0; padding: var(--s3);
  border: 1px solid var(--rule); border-radius: var(--r);
  background: var(--panel-2);
}
.photo-admin__cell.is-primary { border-color: var(--brand); }
.photo-admin__cell img {
  display: block; width: 100%; aspect-ratio: 1 / 1; object-fit: cover;
  border-radius: var(--r-sm); background: var(--panel);
}
.photo-admin__cell figcaption {
  display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap;
  margin: var(--s2) 0;
}
.photo-admin__row { display: flex; gap: var(--s1); flex-wrap: wrap; }
.photo-admin__alt { display: grid; gap: var(--s1); margin-top: var(--s2); }
.photo-admin__alt input { width: 100%; }

/* --------------------------------------------------------- the floor plan */

/* Not a drag-and-drop canvas. A restaurant's floor plan on a screen is a list
   of tables with their covers on them, and the thing a manager needs to see at
   a glance is which ones are held — which a grid of small cards says and a
   stacked list does not. */
.floor-area + .floor-area { border-top: 1px solid var(--rule); padding-top: var(--s4); margin-top: var(--s4); }
.floor-area h3 { margin: 0; font-size: var(--t-base); }

.floor-tables {
  display: grid; gap: var(--s3); margin: var(--s3) 0;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
}
.floor-table {
  display: flex; flex-direction: column; gap: var(--s1);
  padding: var(--s3);
  background: var(--panel-2); border: 1px solid var(--rule); border-radius: var(--r);
}
/* A table switched off is still on the plan — it is furniture the restaurant
   owns and is not using this season — so it is dimmed rather than hidden. */
.floor-table.is-off { opacity: .55; border-style: dashed; }
.floor-table__code { font-weight: 700; font-size: var(--t-lg); }
.floor-table__seats { font-size: var(--t-sm); color: var(--ink-2); }
.floor-table__edit summary { cursor: pointer; color: var(--brand); font-size: var(--t-sm); }
.floor-area__add summary { cursor: pointer; color: var(--brand); font-size: var(--t-sm); }

/* The book. A cancelled party stays on the page — a host asked *did they ring
   back* needs to see it — and is struck through rather than removed. */
.data tr.is-cancelled td, .data tr.is-no_show td { opacity: .55; }
.data tr.is-cancelled .mono { text-decoration: line-through; }
.data tr.is-seated td { background: var(--ok-soft); }

/* ------------------------------------------- booking a table from the street */

.book-public { max-width: 44rem; }
.book-public__step { margin: var(--s6) 0; }
.book-public__step h2 { font-size: var(--t-lg); margin: 0 0 var(--s3); }

/* The times are links, not a select, so the choice is in the address and the
   page works with nothing running. Big enough to hit with a thumb. */
.book-times { display: flex; flex-wrap: wrap; gap: var(--s2); }
.book-time {
  display: inline-block; padding: 10px 16px;
  border: 1px solid var(--rule); border-radius: var(--r);
  background: var(--panel); color: var(--ink); text-decoration: none;
  font-variant-numeric: tabular-nums; font-weight: 600;
}
.book-time:hover { border-color: var(--brand); color: var(--brand); }
.book-time.is-on { background: var(--brand); border-color: var(--brand); color: #fff; }

/* A hint that is not advice but a condition: the thing above it will not work
   until somebody does what it says. */
.hint--warn { color: var(--warn); font-weight: 600; }
