/* ============================================================================
   Cajella — application stylesheet

   Organised as: tokens → base → layout → navigation → typography → buttons →
   forms → feedback → badges → tables → empty states → cards & detail → tabs →
   modal → wizard → auth → responsive.

   The locked visual system is the carbon-fibre tactical HUD described in
   `skills/cajella-hud/SKILL.md`: dark carbon ground, olive drab and muted gold,
   sharp angular panels with thin 1px edges, critical red reserved for real
   urgency. Implementation contract: `docs/plans/2026-08-21-cajella-hud-v1.md`.

   Three rules govern edits here (Decision Log: "Cajella shared visual system",
   and 2026-08-21 "Cajella wears the tactical HUD"):

   1. Colour, spacing, radius and shadow come from the `--cj-*` tokens below.
      A raw hex in a rule is a bug unless it is a one-off translucent overlay.
   2. A wrong rule is corrected where it is declared. Overrides appended to the
      bottom of the file to out-specify an earlier rule are what produced the
      two competing button systems this file replaces.
   3. No selector is deleted. `CssClassCoverageTest` requires every class a
      rendered page emits to have a rule, and `PrimitiveRenderTest` pins
      thirty-four primitive class names whether or not a template still uses
      them. Retheming edits a rule in place; it never removes one.
   ========================================================================== */

:root {
    /* Accents. Olive drab is the system colour — nav, primary action, ready.
       Muted gold carries money, counts and links. Nothing else is a brand
       colour; the HUD skill admits no third hue. */
    --cj-accent: #6B7A3A;
    --cj-accent-hover: #8A9A4A;
    --cj-accent-dark: #C4A35A;
    --cj-gold-dim: #8A7340;

    /* Surfaces. Three steps of near-black: page, panel, inset. The rail is a
       fourth, one step darker than the page, so the chrome sits *under* the
       content rather than floating over it. */
    --cj-bg: #0B0C0A;
    --cj-surface: #121410;
    --cj-hover: #171910;

    /* The panel hierarchy, as four named steps rather than three colours used
       from memory. `--cj-panel-face` is what a panel fills with;
       `--cj-panel-inset-face` is a region nested inside one; and the selected
       tint is olive at 12% over whichever of those it lands on, so a chosen row
       reads as the same surface lit rather than as a different colour. */
    --cj-panel-face: var(--cj-surface);
    --cj-panel-inset-face: var(--cj-hover);
    --cj-panel-selected-face: #6B7A3A1f;

    /* The carbon weave. A diagonal 1px thread at 5% over the page ground —
       enough to read as woven, not enough to read as texture. Applied once, on
       `body::before`, so no panel pays for it twice. */
    --cj-carbon: repeating-linear-gradient(
        45deg,
        #ffffff0d 0 1px,
        transparent 1px 5px
    );

    /* Sidebar */
    --cj-sidebar: #0E0F0C;
    --cj-sidebar-hover: #1B1D14;
    --cj-sidebar-border: #2A2C24;

    /* Text */
    --cj-text: #E6E2D4;
    --cj-muted: #9A9686;
    --cj-on-dark: #E6E2D4;
    --cj-on-dark-muted: #9A9686;

    /* Ink on an olive or gold fill. `--cj-surface` used to serve this purpose
       as well as being the card background: on paper those were the same
       colour, and on carbon they are opposites. Splitting them is what stops a
       primary button from rendering pale-on-olive at 3:1. */
    --cj-on-accent: #0B0C0A;

    /* Borders. A HUD panel is a 1px olive-gold edge, not a shadow. */
    --cj-border: #2A2C24;
    --cj-border-subtle: #1E2018;
    --cj-border-control: #34372B;
    --cj-border-hover: var(--cj-gold-dim);
    /* The resting edge of somewhere you type or choose: text fields, selects,
       textareas, the searchable-select input and choice cards. Control-only
       (UI clarity spec R-10, UC-D1). `--cj-border-control` above stays as it
       is because badges, chips, swatches, attention rows and step numbers draw
       with it too, and those are not controls. #68665A is --cj-muted at 55%
       over --cj-border: 3.21:1 on the surface, 3.40:1 on the page, 3.07:1 on
       the inset face and 3.33:1 on the sidebar, where #34372B was 1.5:1. */
    --cj-control-edge: #68665A;
    /* Empty and disabled text controls (UI clarity spec R-4, R-5). The
       placeholder is --cj-muted: 6.25:1 on the surface and 5.99:1 on the inset
       face, so it stays legible, and 2.29:1 below entered text, so it never
       reads as a value. Disabled text is the same muted tone — still readable,
       as a locked base unit has to be — and what says "disabled" rather than
       "empty" is the dashed edge and the field losing its face. */
    --cj-placeholder: var(--cj-muted);
    --cj-disabled-text: var(--cj-muted);
    --cj-disabled-edge: var(--cj-control-edge);
    --cj-disabled-face: transparent;
    /* A checked choice card (UI clarity spec R-3, amended §11.1). The edge is
       olive-bright: 6.00:1 on the surface, 6.35:1 on the page, 5.75:1 on the
       inset face and 6.23:1 on the sidebar. It is not meant to stand out from
       the resting edge (1.87:1); the native radio dot or checkbox tick is the
       mark that says "checked", and the face and weight reinforce it. */
    --cj-selected-edge: var(--cj-accent-hover);
    --cj-selected-face: #6B7A3A2e;
    /* A highlighted combobox option (UI clarity spec R-7). The face alone was
       1.05:1, so the cue is an olive-bright left bar, the sidebar's pattern:
       6.00:1 against the list face and 5.75:1 against the highlight face. The
       current selection is a different thing and says so with a check mark. */
    --cj-highlight-edge: var(--cj-accent-hover);
    --cj-highlight-face: var(--cj-hover);

    /* The panel edge, and the four states that are allowed to change it.
       `--cj-panel-edge` is one step brighter than `--cj-border` because it is
       the only line a panel has: with the chamfer the edge *is* the panel's
       silhouette, and at #2A2C24 against the page ground a 1px silhouette read
       as a smudge rather than as a drawn line.

       Everything else here is state. A panel wears the neutral edge unless it
       has something to say — the HUD skill reserves gold for meaning, and a
       screen where every panel is accented has told the owner nothing. The
       superseded treatment was a gold top hairline on every panel; it is now the
       emphasis variant, applied deliberately. */
    --cj-panel-edge: #33372A;
    --cj-panel-edge-inset: #262A20;
    --cj-panel-edge-emphasis: var(--cj-gold-dim);
    --cj-panel-edge-warning: var(--cj-warning-border);
    --cj-panel-edge-critical: var(--cj-danger-border);
    --cj-panel-edge-selected: var(--cj-accent-hover);

    /* Focus. Gold reads against every carbon surface in the application, so
       one ring serves both the page and the rail. */
    --cj-focus: var(--cj-accent-dark);
    --cj-focus-on-dark: var(--cj-accent-dark);

    /* Status. Olive is ready, gold is caution, red is genuine failure. The
       `-bg` tints are the same hue at low alpha over carbon rather than the
       pastel washes a light theme uses. */
    --cj-success: #7E9C4A;
    --cj-success-bg: #5D7A3A26;
    --cj-success-border: #5D7A3A;
    --cj-warning: #C4A35A;
    --cj-warning-bg: #C4A35A1f;
    --cj-warning-border: var(--cj-gold-dim);
    --cj-danger: #D2664B;
    --cj-danger-hover: #B54A32;
    --cj-danger-bg: #B54A3226;
    --cj-danger-border: #B54A32;
    --cj-info: #9A9686;
    --cj-info-bg: #E6E2D414;
    --cj-info-border: #34372B;
    --cj-neutral: #9A9686;
    --cj-neutral-bg: #E6E2D40f;

    /* Elevation. Flat HUD cards with 1px edges — the skill rejects heavy
       glassmorphism, so these are depth cues, not drop shadows. */
    --cj-shadow: 0 1px 0 #00000059;
    --cj-shadow-lg: 0 6px 20px #00000073;
    --cj-shadow-modal: 0 24px 70px #000000b3;

    /* Radii. Sharp angular panels: effectively square, with 1–2px to stop the
       corners looking accidental. Panels no longer use a radius at all — they
       are cut rather than rounded (see the chamfer below) — and these remain the
       corner treatment for controls, chips and small chrome. */
    --cj-radius-xs: 0;
    --cj-radius-sm: 1px;
    --cj-radius-lg: 2px;
    --cj-radius-pill: 2px;
    /* Approved 2026-09: the shared button shape (below) moved from a cut
       corner to a conventional rounded one — the chamfer was reading as a
       clipping bug on a filled, bordered control, not as Cajella's angular
       identity, which is untouched everywhere else (panels, cards, badges,
       chips, form fields all keep their cut). `.pagination-page` and
       `.row-overflow-trigger` move with it, because both are declared
       "the same family as the buttons" where the cut used to live — see the
       comment above `.form-control` below. This is deliberately a new,
       larger radius rather than a reuse of `--cj-radius-sm`/`-lg`: those are
       barely-there roundings tuned to still read as square, and "modestly
       rounded" was the whole point of the correction. */
    --cj-radius-control: 6px;

    /* The chamfer. This is the shape of Cajella: a panel's corners are cut at
       45°, not rounded, and the 1px edge follows the cut. One size for panels,
       one for controls and chips, one for the small framed icon container — a
       9px cut on a 22px badge would eat the badge.

       Sizes are deliberately small. The cut is meant to read as machined, and a
       cut large enough to notice on its own is a decoration. */
    --cj-chamfer: 9px;
    --cj-chamfer-sm: 5px;
    --cj-chamfer-xs: 3px;
    /* The one genuinely round thing in Cajella: an identity chip. A person's
       initials in a square read as a label; in a circle they read as a person,
       which is the whole job of the control in the header. Nothing else may use
       this — the system is angular, and `--cj-radius-pill` above is 2px on
       purpose. */
    --cj-radius-round: 50%;

    /* Display face — condensed military sans for headers, nav group labels and
       widget labels, uppercase and tracked. Deliberately a system stack: an
       ERP that a shop runs on a bench machine must not depend on fetching a
       font. Table bodies and form controls never use it; the HUD skill forbids
       stencil in a table body. */
    --cj-font-display: 'Oswald', 'Roboto Condensed', 'Arial Narrow',
        'Helvetica Neue Condensed', 'Liberation Sans Narrow', ui-sans-serif, sans-serif;

    /* Icons. One family, one delivery mechanism.
       The geometry is Lucide's — outlined, 24px box, 2px stroke, ISC-licensed —
       inlined as SVG masks. Inlined rather than fetched for the same reason the
       HUD mark beside the wordmark is drawn in CSS: the rail must render
       identically on a bench machine with no network. Masks rather than an
       `<img>` because a mask takes its colour from `background-color`, so a
       glyph is tinted by the token layer like everything else and an active row
       can accent its icon without shipping a second file. Masks rather than an
       inline `<svg>` in Blade because a pseudo-element is decorative to
       assistive technology without anyone having to remember `aria-hidden` — the
       label beside it already says where the row goes.
       Add a glyph by adding a token here and a `[data-icon]` line beside
       `.nav-item::before`; `ApplicationShellTest` fails on a destination whose
       icon name has no token. */
    --cj-icon-layout-dashboard: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Crect width='7' height='9' x='3' y='3' rx='1' /%3E%3Crect width='7' height='5' x='14' y='3' rx='1' /%3E%3Crect width='7' height='9' x='14' y='12' rx='1' /%3E%3Crect width='7' height='5' x='3' y='16' rx='1' /%3E%3C/svg%3E");
    --cj-icon-layers: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z' /%3E%3Cpath d='M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12' /%3E%3Cpath d='M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17' /%3E%3C/svg%3E");
    --cj-icon-blocks: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M10 22V7a1 1 0 0 0-1-1H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5a1 1 0 0 0-1-1H2' /%3E%3Crect x='14' y='2' width='8' height='8' rx='1' /%3E%3C/svg%3E");
    --cj-icon-package: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z' /%3E%3Cpath d='M12 22V12' /%3E%3Cpolyline points='3.29 7 12 12 20.71 7' /%3E%3Cpath d='m7.5 4.27 9 5.15' /%3E%3C/svg%3E");
    --cj-icon-box: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z' /%3E%3Cpath d='m3.3 7 8.7 5 8.7-5' /%3E%3Cpath d='M12 22V12' /%3E%3C/svg%3E");
    --cj-icon-wrench: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z' /%3E%3C/svg%3E");
    --cj-icon-factory: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M12 16h.01' /%3E%3Cpath d='M16 16h.01' /%3E%3Cpath d='M3 19a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8.5a.5.5 0 0 0-.769-.422l-4.462 2.844A.5.5 0 0 1 15 10.5v-2a.5.5 0 0 0-.769-.422L9.77 10.922A.5.5 0 0 1 9 10.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z' /%3E%3Cpath d='M8 16h.01' /%3E%3C/svg%3E");
    --cj-icon-trash-2: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M10 11v6' /%3E%3Cpath d='M14 11v6' /%3E%3Cpath d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6' /%3E%3Cpath d='M3 6h18' /%3E%3Cpath d='M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2' /%3E%3C/svg%3E");
    --cj-icon-settings: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915' /%3E%3Ccircle cx='12' cy='12' r='3' /%3E%3C/svg%3E");
    --cj-icon-circle-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Ccircle cx='12' cy='12' r='10' /%3E%3Cpath d='M8 12h8' /%3E%3Cpath d='M12 8v8' /%3E%3C/svg%3E");
    --cj-icon-bell: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M10.268 21a2 2 0 0 0 3.464 0' /%3E%3Cpath d='M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326' /%3E%3C/svg%3E");
    --cj-icon-chevron-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='m6 9 6 6 6-6' /%3E%3C/svg%3E");

    /* Added by the panel pass. The severity, trend, time and money glyphs the
       shared panel, metric, badge and attention primitives need — same family,
       same 24px box, same 2px stroke, same delivery. A glyph is added here and
       bound below beside `.nav-item::before`; nothing draws an icon any other
       way. */
    --cj-icon-chevron-right: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='m9 18 6-6-6-6' /%3E%3C/svg%3E");
    --cj-icon-triangle-alert: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3' /%3E%3Cpath d='M12 9v4' /%3E%3Cpath d='M12 17h.01' /%3E%3C/svg%3E");
    --cj-icon-circle-alert: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Ccircle cx='12' cy='12' r='10' /%3E%3Cline x1='12' x2='12' y1='8' y2='12' /%3E%3Cline x1='12' x2='12.01' y1='16' y2='16' /%3E%3C/svg%3E");
    --cj-icon-circle-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Ccircle cx='12' cy='12' r='10' /%3E%3Cpath d='m9 12 2 2 4-4' /%3E%3C/svg%3E");
    --cj-icon-clock: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Ccircle cx='12' cy='12' r='10' /%3E%3Cpath d='M12 6v6l4 2' /%3E%3C/svg%3E");
    --cj-icon-trending-up: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M16 7h6v6' /%3E%3Cpath d='m22 7-8.5 8.5-5-5L2 17' /%3E%3C/svg%3E");
    --cj-icon-trending-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M16 17h6v-6' /%3E%3Cpath d='m22 17-8.5-8.5-5 5L2 7' /%3E%3C/svg%3E");
    --cj-icon-coins: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M13.744 17.736a6 6 0 1 1-7.48-7.48' /%3E%3Cpath d='M15 6h1v4' /%3E%3Cpath d='m6.134 14.768.866-.5 2 3.464' /%3E%3Ccircle cx='16' cy='8' r='6' /%3E%3C/svg%3E");
    --cj-icon-clipboard-list: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1' /%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2' /%3E%3Cpath d='M12 11h4' /%3E%3Cpath d='M12 16h4' /%3E%3Cpath d='M8 11h.01' /%3E%3Cpath d='M8 16h.01' /%3E%3C/svg%3E");
    --cj-icon-boxes: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z' /%3E%3Cpath d='m7 16.5-4.74-2.85' /%3E%3Cpath d='m7 16.5 5-3' /%3E%3Cpath d='M7 16.5v5.17' /%3E%3Cpath d='M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z' /%3E%3Cpath d='m17 16.5-5-3' /%3E%3Cpath d='m17 16.5 4.74-2.85' /%3E%3Cpath d='M17 16.5v5.17' /%3E%3Cpath d='M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z' /%3E%3Cpath d='M12 8 7.26 5.15' /%3E%3Cpath d='m12 8 4.74-2.85' /%3E%3Cpath d='M12 13.5V8' /%3E%3C/svg%3E");
    --cj-icon-info: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Ccircle cx='12' cy='12' r='10' /%3E%3Cpath d='M12 16v-4' /%3E%3Cpath d='M12 8h.01' /%3E%3C/svg%3E");
    /* Business Settings names two things the set had no glyph for: the company
       itself, and a percentage. Same family, same 24px Lucide grid, same
       two-pixel round stroke as the twenty-three above. */
    --cj-icon-store: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7' /%3E%3Cpath d='M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8' /%3E%3Cpath d='M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4' /%3E%3Cpath d='M2 7h20' /%3E%3Cpath d='M22 7v3a2 2 0 0 1-2 2a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7' /%3E%3C/svg%3E");
    --cj-icon-calendar: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Crect width='18' height='18' x='3' y='4' rx='2' ry='2' /%3E%3Cline x1='16' x2='16' y1='2' y2='6' /%3E%3Cline x1='8' x2='8' y1='2' y2='6' /%3E%3Cline x1='3' x2='21' y1='10' y2='10' /%3E%3C/svg%3E");
    --cj-icon-percent: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cline x1='19' x2='5' y1='5' y2='19' /%3E%3Ccircle cx='6.5' cy='6.5' r='2.5' /%3E%3Ccircle cx='17.5' cy='17.5' r='2.5' /%3E%3C/svg%3E");
    --cj-icon-users: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2' /%3E%3Ccircle cx='9' cy='7' r='4' /%3E%3Cpath d='M22 21v-2a4 4 0 0 0-3-3.87' /%3E%3Cpath d='M16 3.13a4 4 0 0 1 0 7.75' /%3E%3C/svg%3E");
    /* The shell's own controls (mobile application experience SH-1, SH-3, SH-7):
       the drawer trigger, the drawer close and the back-to-parent link. Same
       family, grid, stroke and delivery as the glyphs above. */
    --cj-icon-menu: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M4 6h16' /%3E%3Cpath d='M4 12h16' /%3E%3Cpath d='M4 18h16' /%3E%3C/svg%3E");
    --cj-icon-x: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M18 6 6 18' /%3E%3Cpath d='m6 6 12 12' /%3E%3C/svg%3E");
    --cj-icon-chevron-left: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='m15 18-6-6 6-6' /%3E%3C/svg%3E");
    /* Approved 2026-09: the canonical Cajella mark (`public/images/cajella-mark.svg`),
       the same artwork the public marketing site renders, as a mask. Only the
       two fill paths are carried over — the cream vein stroke is redundant to
       an alpha mask, since it sits entirely inside the already-opaque leaf
       fill. One icon delivery mechanism, no exception for the brand mark: no
       network fetch, no rasterizing, no second image dropped into the shell. */
    --cj-brand-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cpath fill='%23000' d='M31 15 H101 L90 35 H48 L35 48 V76 L48 89 H91 L102 109 H31 L12 90 V34 Z'/%3E%3Cpath fill='%23000' d='M52 78 C55 60 68 49 87 46 C85 64 74 77 56 82 C54 82 53 80 52 78 Z'/%3E%3C/svg%3E");

    /* The application header's height, and the sidebar brand block's. One token
       because the two must match: their bottom hairlines are meant to read as a
       single line across the shell, and two independent figures is how that line
       ends up with a step in it. */
    --cj-topbar-h: 76px;

    /* Spacing */
    --cj-space-1: 4px;
    --cj-space-2: 8px;
    --cj-space-3: 12px;
    --cj-space-4: 16px;
    --cj-space-5: 24px;
    --cj-space-6: 32px;

    /* Density. The reference consoles are information-dense without being
       cramped, and the previous panel padding — a flat 24px on every side of
       every card — was the single biggest reason Cajella read as roomier and
       vaguer than them. These are the numbers a panel, its header, a metric tile
       and a grid actually use, named once so that "panel padding" is a decision
       and not a habit.

       They are also what the full-bleed panel headers negate: a header band
       reaches the panel edge by cancelling `--cj-panel-pad-*`, so the two must
       come from the same place or the band stops short of the edge by however
       much they disagree. */
    --cj-panel-pad-y: 18px;
    --cj-panel-pad-x: 20px;
    --cj-panel-head-pad-y: 9px;
    --cj-metric-pad-y: 14px;
    --cj-metric-pad-x: 16px;
    --cj-grid-gap: 12px;
    --cj-icon-gap: 9px;
    --cj-control-height: 34px;
    --cj-row-height: 40px;

    /* Type scale. The gap between the page title and a section title used to be
       2.6rem → 1.3rem, which left nothing to carry the middle of a page. */
    --cj-text-xs: .72rem;
    --cj-text-sm: .84rem;
    --cj-text-base: .95rem;
    --cj-text-lg: 1.15rem;
    --cj-text-xl: 1.35rem;
    --cj-text-2xl: 1.9rem;

    /* The seven levels the interface actually distinguishes, from the page's
       command header down to the smallest piece of metadata. Sizes above carry
       most of it; these three are the ones that were being guessed at each site
       of use — a metric's figure, a metric's figure in a dense grid, and the
       tracked caps of a panel label.

       Tracking is a token for the same reason a colour is: a panel label at
       .18em beside a badge at .1em beside a tile label at .2em is three
       systems, and the difference is invisible until they sit in one row. */
    --cj-text-metric: 2.15rem;
    --cj-text-metric-sm: 1.55rem;
    --cj-text-micro: .66rem;
    --cj-track-label: .18em;
}

/* ---------------------------------------------------------------- base ---- */

* { box-sizing: border-box; }

/* `color-scheme: dark` is what tells the browser to draw its *own* furniture
   dark: a date picker's calendar glyph, a select's arrow, a textarea's resize
   handle and the scrollbars. Without it those are drawn for a light page and are
   the only light things on the screen. */
html {
    color-scheme: dark;
    font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
    color: var(--cj-text);
    background: var(--cj-bg);
}

/* A radio or a checkbox is olive, wherever it is. `.form-choice input` said so
   already; the choices inside a fieldset did not, and rendered in the browser's
   own blue. */
input[type='radio'],
input[type='checkbox'] { accent-color: var(--cj-accent); }

body { margin: 0; line-height: 1.5; }

/* The weave. Fixed and non-interactive, so it does not scroll away from a long
   page and cannot intercept a click. `z-index: 0` with the shell above it keeps
   it behind content without giving every panel a stacking context. */
body::before {
    content: '';
    position: fixed;
    z-index: 0;
    inset: 0;
    background-image: var(--cj-carbon);
    pointer-events: none;
}
body > * { position: relative; z-index: 1; }

a { color: var(--cj-accent-dark); font-weight: 650; text-decoration: none; }
a:hover { color: var(--cj-warning); text-decoration: underline; }

:focus-visible { outline: 3px solid var(--cj-focus); outline-offset: 2px; }

/* A keyboard user should be able to reach the page without walking the whole
   sidebar first. Visible only while focused. */
.skip-link {
    position: absolute;
    z-index: 20;
    top: var(--cj-space-2);
    left: var(--cj-space-2);
    padding: 10px 15px;
    border: 1px solid var(--cj-accent-dark);
    border-radius: var(--cj-radius-sm);
    background: var(--cj-surface);
    color: var(--cj-text);
    font-weight: 750;
    transform: translateY(-200%);
    transition: transform .15s;
}
.skip-link:focus { transform: translateY(0); }

/* -------------------------------------------------------------- layout ---- */

.app-shell { display: flex; min-height: 100vh; min-height: 100dvh; }
.app-main { min-width: 0; flex: 1; }

/* The application header: the page's title on the left, controls on the right.
   It was a 58px strip carrying a .96rem caps line, which read as a breadcrumb
   above the page rather than as the page's own header — and because every module
   index also printed its name in the body, an owner read COMPONENTS twice. The
   band is the title now (`.topbar h1`), at the reference console's height and
   type scale, and it is the only title on the screen.

   `--cj-topbar-h` is shared with `.sidebar-brand`, which is the whole point of
   having it: the brand block and this band are the same height, so the hairline
   under each is one line running the full width of the shell. */
.topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--cj-space-5);
    min-height: var(--cj-topbar-h);
    padding: 14px 4vw;
    border-bottom: 1px solid var(--cj-border);
    background: var(--cj-sidebar);
    box-shadow: inset 0 -1px 0 #8A734033;
}
.topbar .eyebrow { margin: 0; }

/* Which module the page belongs to, and — when the page is a record inside that
   module rather than the module itself — what that record is. The identity
   truncates rather than wraps: the header is one row at every width, and the
   controls to its right must stay reachable before the identity stays complete.
   No flanking rules and no centring: the title starts at the content column's
   left edge, which is where a title of unpredictable length can start safely. */
.topbar-identity {
    display: flex;
    min-width: 0;
    overflow: hidden;
}
.topbar h1 {
    display: flex;
    align-items: baseline;
    min-width: 0;
    gap: var(--cj-space-3);
    margin: 0;
    font-family: var(--cj-font-display);
    font-size: clamp(1.15rem, 1.9vw, 1.6rem);
    font-weight: 600;
    letter-spacing: .12em;
    line-height: 1.1;
    text-transform: uppercase;
}
.topbar-module,
.topbar-detail {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* Gold for the module, olive for the record — the reference's two-tone title.
   The record is set smaller than the module rather than at the same size: the
   reference's second segment is a fixed two-word qualifier, and Cajella's is a
   Product or Material name that has no length limit. */
.topbar-module { flex: 0 1 auto; color: var(--cj-accent-dark); }
.topbar-detail {
    flex: 1 1 auto;
    color: var(--cj-accent-hover);
    font-size: .74em;
    letter-spacing: .1em;
}
.topbar-separator { flex: 0 0 auto; color: var(--cj-gold-dim); font-size: .6em; }

/* The controls, behind a hairline of their own so they read as chrome rather
   than as the end of the title. The hairline runs the height of the band. */
.topbar-actions {
    display: flex;
    align-items: center;
    align-self: stretch;
    flex: 0 0 auto;
    gap: var(--cj-space-1);
    padding-left: var(--cj-space-5);
    border-left: 1px solid var(--cj-border);
}

/* The superseded status strip. The shell used to state "Signed in as …" and a
   system lamp here; the person is now the user control to the right, and "the
   application is running" was never news to anyone reading the page it was on.
   The rule stays because the stylesheet corrects in place and deletes nothing
   (see this file's header), and `.topbar-lamp` is still the rail's lamp. */
.topbar-status { display: flex; align-items: center; gap: var(--cj-space-2); }
.topbar-lamp {
    display: inline-block;
    width: 7px;
    height: 7px;
    border-radius: var(--cj-radius-pill);
    background: var(--cj-success);
    box-shadow: 0 0 6px #7E9C4A99;
}

/* ------------------------------------------------ header controls ---- */

/* The notification bell and the user menu. Both are the same shape: a real
   <button> that owns `aria-expanded`, and a panel positioned against the group
   around them. Compact, square, outlined — a header control is chrome, so it is
   quiet until it is hovered, focused or open. */
.header-control-group { position: relative; display: flex; }

.header-control {
    display: inline-flex;
    align-items: center;
    min-height: 40px;
    gap: var(--cj-space-2);
    padding: 6px 10px;
    border: 1px solid transparent;
    border-radius: var(--cj-radius-sm);
    background: transparent;
    color: var(--cj-muted);
    font: inherit;
    line-height: 1;
    cursor: pointer;
}
.header-control:hover {
    border-color: var(--cj-border-control);
    background: var(--cj-sidebar-hover);
    color: var(--cj-text);
}
/* Open is a state of the control, not only of the panel, so the button still
   reads as pressed when the pointer leaves it. */
.header-control[aria-expanded='true'] {
    border-color: var(--cj-gold-dim);
    background: var(--cj-sidebar-hover);
    color: var(--cj-text);
}
/* A control whose whole content is a glyph is square and carries its meaning in
   `aria-label` instead. */
.header-control-icon-only {
    position: relative;
    justify-content: center;
    min-width: 40px;
    padding: 6px;
}

/* Unread notifications, when there are any. Cajella has no notification system
   at this commit, so `x-notification-bell` is passed no count and this never
   renders — it exists so that connecting one later is a controller change and
   not a design decision. */
.notification-badge {
    position: absolute;
    top: 4px;
    right: 4px;
    min-width: 16px;
    padding: 0 4px;
    border-radius: var(--cj-radius-sm);
    background: var(--cj-danger-hover);
    color: var(--cj-text);
    font-family: var(--cj-font-display);
    font-size: .6rem;
    font-weight: 700;
    line-height: 16px;
    text-align: center;
}

/* The signed-in person. A hairline separates them from the bell, the name
   truncates rather than pushing the control off the row, and the initials chip
   is the one round thing in the application (see `--cj-radius-round`). */
.user-menu { padding-left: var(--cj-space-1); border-left: 1px solid var(--cj-border-subtle); }
.user-control { max-width: 15rem; }
/* Tracked caps, stated rather than inherited. The shared `button` rule further
   down gives every bare <button> the display treatment, so this used to be caps
   by accident; the header wears caps on purpose and the declaration should say
   so. Tracking is the rail's .06em rather than the button rule's .1em, because a
   person's name is longer than a button's verb. */
.user-control-name {
    min-width: 0;
    overflow: hidden;
    color: var(--cj-text);
    font-family: var(--cj-font-display);
    font-size: .86rem;
    font-weight: 650;
    letter-spacing: .06em;
    text-overflow: ellipsis;
    text-transform: uppercase;
    white-space: nowrap;
}
.user-control-chevron { flex: 0 0 14px; width: 14px; height: 14px; mask-size: 14px 14px; -webkit-mask-size: 14px 14px; }
.user-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 28px;
    width: 28px;
    height: 28px;
    border: 1px solid var(--cj-gold-dim);
    border-radius: var(--cj-radius-round);
    background: var(--cj-bg);
    color: var(--cj-accent-dark);
    font-family: var(--cj-font-display);
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .04em;
}

/* The panel. Closed panels carry the `hidden` attribute rather than a class, so
   they are out of the tab order and the accessibility tree whether or not
   `menu.js` has run — see that file. */
/* An overlay panel: the shared shape at the smaller cut, and one of the two
   places besides the modal that keeps a shadow, because it genuinely is floating
   over the page it belongs to. */
.header-menu {
    --cj-panel-cut: var(--cj-chamfer-sm);
    --cj-panel-edge-color: var(--cj-panel-edge-emphasis);
    position: absolute;
    z-index: 8;
    top: calc(100% + 6px);
    right: 0;
    min-width: 14rem;
    padding: var(--cj-space-2);
    box-shadow: var(--cj-shadow-lg);
}
.header-menu-wide { min-width: 17rem; }
.header-menu-title {
    margin: 0 0 var(--cj-space-2);
    padding: 2px 8px 8px;
    border-bottom: 1px solid var(--cj-border-subtle);
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: .64rem;
    font-weight: 600;
    letter-spacing: .22em;
    text-transform: uppercase;
}
.header-menu-empty { margin: 0; padding: 2px 8px 6px; color: var(--cj-muted); font-size: var(--cj-text-sm); }
.header-menu-identity { padding: 2px 8px 10px; border-bottom: 1px solid var(--cj-border-subtle); }
.header-menu-identity strong { display: block; color: var(--cj-text); font-size: var(--cj-text-sm); }
.header-menu-identity small {
    display: block;
    overflow: hidden;
    color: var(--cj-muted);
    font-size: var(--cj-text-xs);
    text-overflow: ellipsis;
    white-space: nowrap;
}
.header-menu-form { margin: 0; }
.header-menu-item {
    display: block;
    width: 100%;
    margin-top: 2px;
    padding: 9px 8px;
    border: 0;
    border-left: 2px solid transparent;
    border-radius: var(--cj-radius-sm);
    background: transparent;
    color: var(--cj-on-dark);
    font: inherit;
    font-size: var(--cj-text-sm);
    font-weight: 650;
    letter-spacing: normal;
    text-align: left;
    /* Sign out is a <button> and Business settings is an <a>, so without this
       the shared `button` rule's display treatment made one of the two items in
       the same menu shout and the other not. Both are destinations; neither
       shouts. */
    text-transform: none;
    cursor: pointer;
}
.header-menu-item:hover {
    border-left-color: var(--cj-gold-dim);
    background: var(--cj-hover);
    color: var(--cj-text);
    text-decoration: none;
}
/* Signing out is the destructive-ish one of the two, and it is still not the
   loudest thing in the menu. */
.header-menu-item-danger { color: var(--cj-danger); }
.header-menu-item-danger:hover { border-left-color: var(--cj-danger-border); background: var(--cj-danger-bg); color: var(--cj-danger); }

/* Tighter again (was 42px, then 32px). The page's title is in the header band
   now, so what sits at the top of this box is the lede or the first panel, and
   the reference console starts its first panel close under the band. */
.page-content { max-width: 1240px; margin: 0 auto; padding: 24px 4vw 56px; }

/* A page whose content is a wide table rather than prose opts out of the
   reading measure with `@section('page-class','page-wide')`. The Materials list
   is the case that asked for it: eight columns plus an Actions cell of five
   controls did not fit inside 1240px less 4vw of gutter on a 1440px screen, and
   the table scrolled sideways while a third of the window sat empty beside it.
   Confined to the desktop range above the shell's own 900px breakpoint, so the
   narrow-viewport padding further down still applies untouched — this widens
   one page, not Cajella. */
@media (min-width: 901px) {
    .page-content.page-wide { max-width: none; padding-right: 24px; padding-left: 24px; }

    /* Billing (BUX): the family's main column and its rail (spec §5.8). */
    .billing-layout { grid-template-columns: minmax(0, 2fr) minmax(260px, 1fr); }
}

/* ---------------------------------------------------------- navigation ---- */

/* The rail. Around 240px per the HUD skill's implementation note — 256px since
   the rows gained an icon column. Sixteen pixels, not the fifty a conventional
   ERP rail would take, and measured rather than guessed: at 248px the longest
   label in the map, PACKAGING MATERIALS, wrapped to a second line and that one
   row stood 15px taller than every other. There is a gold hairline on the
   inside edge so the chrome is separated from the page by a line rather than by
   a change of brightness alone. */
.app-sidebar {
    display: flex;
    flex-direction: column;
    width: 256px;
    flex: 0 0 256px;
    /* No top padding: the brand block below sets its own height from
       `--cj-topbar-h` so that it finishes level with the application header. */
    padding: 0 14px 16px;
    border-right: 1px solid var(--cj-border);
    background: var(--cj-sidebar);
    color: var(--cj-text);
    box-shadow: inset -1px 0 0 #8A734026;
}
/* The focus ring must be visible against the dark rail, not against paper. */
.app-sidebar :focus-visible { outline-color: var(--cj-focus-on-dark); }

/* Mark plus wordmark, top-left. The mark is drawn from the border tokens
   rather than shipped as an asset: two olive bars and a gold corner, which is
   the HUD's own vocabulary and costs no request. */
/* A grid, not a flex row. The wordmark and its sub-label are two lines, and as
   a flex row they sat side by side and pushed "Operations" off the edge of the
   rail. Two columns put the mark beside the wordmark; the sub-label is placed
   into the second column of the second row so it aligns under the wordmark
   rather than under the mark. */
/* It is `--cj-topbar-h` tall and full-bleed to the rail's edges, with the same
   border and the same gold inset the application header carries. The two
   hairlines then meet at the rail's right edge and read as one line across the
   shell, which is what the reference console does and what this rail did not:
   the brand block finished at 76px and the header at 58px, so the chrome had a
   step in it eighteen pixels wide. */
.sidebar-brand {
    display: grid;
    box-sizing: border-box;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    align-content: center;
    min-height: var(--cj-topbar-h);
    margin: 0 -14px;
    column-gap: 10px;
    padding: 0 24px;
    border-bottom: 1px solid var(--cj-border);
    box-shadow: inset 0 -1px 0 #8A734033;
    color: var(--cj-text);
    font-family: var(--cj-font-display);
    letter-spacing: .16em;
    font-weight: 700;
    font-size: 1.12rem;
    text-transform: uppercase;
}
/* Approved 2026-09: the canonical Cajella mark (`--cj-brand-mark`) replaces
   the CSS-drawn rotated-square placeholder this pseudo-element used to draw.
   Auto-placed into the same first grid column the placeholder occupied,
   sized to what the placeholder was; `currentColor` so the mark recolors with
   the wordmark on hover instead of carrying its own hardcoded color. Same
   mask mechanism every other glyph in the shell uses, so no `<svg>` or
   `<img>` is added to the rendered page. */
.sidebar-brand::before {
    content: '';
    display: block;
    width: 14px;
    height: 14px;
    background-color: currentColor;
    -webkit-mask: var(--cj-brand-mark) center / contain no-repeat;
    mask: var(--cj-brand-mark) center / contain no-repeat;
}
.sidebar-brand small {
    grid-column: 2;
    display: block;
    color: var(--cj-muted);
    font-size: .6rem;
    font-weight: 500;
    letter-spacing: .28em;
}
.sidebar-brand:hover { color: var(--cj-accent-dark); text-decoration: none; }

.nav-group-label {
    margin: 18px 12px 6px;
    padding-bottom: 5px;
    border-bottom: 1px solid var(--cj-border-subtle);
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: .64rem;
    font-weight: 600;
    letter-spacing: .22em;
}

/* A nav row is `[icon column] [label]` on a flat strip, with room on the left
   for the active bar so nothing shifts sideways when the bar appears.
   The label is tracked caps in the display face, which is what the rest of the
   HUD's headers wear; it is *rendered* in caps rather than written that way, so
   the navigation map keeps one sentence-case label per destination and the rail
   never renames a screen. */
.nav-item {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 9px 12px;
    border-left: 3px solid transparent;
    border-radius: var(--cj-radius-sm);
    color: var(--cj-on-dark-muted);
    font-family: var(--cj-font-display);
    font-size: .8rem;
    font-weight: 600;
    letter-spacing: .06em;
    line-height: 1.3;
    text-transform: uppercase;
}

/* The icon column, and the one place a glyph is drawn.
   `flex: 0 0 18px` is what guarantees the alignment: every label starts exactly
   18px + one gap from the row's text edge whatever glyph sits there, so no icon
   can nudge the row below it out of line. The mask is chosen by the row's own
   `data-icon`, one line each, below. */
.nav-item::before,
.header-icon {
    display: block;
    flex: 0 0 18px;
    width: 18px;
    height: 18px;
    background-color: currentColor;
    -webkit-mask: var(--cj-glyph, none) center / 18px 18px no-repeat;
    mask: var(--cj-glyph, none) center / 18px 18px no-repeat;
}
.nav-item::before { content: ''; }

[data-icon='layout-dashboard'] { --cj-glyph: var(--cj-icon-layout-dashboard); }
[data-icon='layers'] { --cj-glyph: var(--cj-icon-layers); }
[data-icon='blocks'] { --cj-glyph: var(--cj-icon-blocks); }
[data-icon='package'] { --cj-glyph: var(--cj-icon-package); }
[data-icon='box'] { --cj-glyph: var(--cj-icon-box); }
[data-icon='wrench'] { --cj-glyph: var(--cj-icon-wrench); }
[data-icon='factory'] { --cj-glyph: var(--cj-icon-factory); }
[data-icon='trash-2'] { --cj-glyph: var(--cj-icon-trash-2); }
[data-icon='settings'] { --cj-glyph: var(--cj-icon-settings); }
[data-icon='circle-plus'] { --cj-glyph: var(--cj-icon-circle-plus); }
[data-icon='bell'] { --cj-glyph: var(--cj-icon-bell); }
[data-icon='chevron-down'] { --cj-glyph: var(--cj-icon-chevron-down); }
[data-icon='chevron-right'] { --cj-glyph: var(--cj-icon-chevron-right); }
[data-icon='triangle-alert'] { --cj-glyph: var(--cj-icon-triangle-alert); }
[data-icon='circle-alert'] { --cj-glyph: var(--cj-icon-circle-alert); }
[data-icon='circle-check'] { --cj-glyph: var(--cj-icon-circle-check); }
[data-icon='clock'] { --cj-glyph: var(--cj-icon-clock); }
[data-icon='trending-up'] { --cj-glyph: var(--cj-icon-trending-up); }
[data-icon='trending-down'] { --cj-glyph: var(--cj-icon-trending-down); }
[data-icon='coins'] { --cj-glyph: var(--cj-icon-coins); }
[data-icon='clipboard-list'] { --cj-glyph: var(--cj-icon-clipboard-list); }
[data-icon='boxes'] { --cj-glyph: var(--cj-icon-boxes); }
[data-icon='info'] { --cj-glyph: var(--cj-icon-info); }
[data-icon='store'] { --cj-glyph: var(--cj-icon-store); }
[data-icon='calendar'] { --cj-glyph: var(--cj-icon-calendar); }
[data-icon='percent'] { --cj-glyph: var(--cj-icon-percent); }
[data-icon='users'] { --cj-glyph: var(--cj-icon-users); }
[data-icon='menu'] { --cj-glyph: var(--cj-icon-menu); }
[data-icon='x'] { --cj-glyph: var(--cj-icon-x); }
[data-icon='chevron-left'] { --cj-glyph: var(--cj-icon-chevron-left); }

.nav-item:hover {
    border-left-color: var(--cj-gold-dim);
    background: var(--cj-sidebar-hover);
    color: var(--cj-text);
    text-decoration: none;
}
/* Pressed: the hover's left bar at full gold, so a tap shows before the next
   page has loaded. It lasts only while pressed, so nothing is left looking
   selected (spec T-4). */
.nav-item:active { border-left-color: var(--cj-accent-dark); background: var(--cj-sidebar-hover); color: var(--cj-text); }
/* Active is the olive left bar *and* an olive fill *and* a gold icon — the skill
   offers either of the first two; using all three means the current module is
   legible at a glance, legible to someone who cannot separate olive from carbon
   by hue alone, and legible in the icon column as well as the label. The 1px
   inset gold edge is the panel treatment the rest of the HUD uses, at the one
   scale where a real border would move the row. */
.nav-item.active {
    border-left-color: var(--cj-accent-hover);
    background: #6B7A3A3d;
    box-shadow: inset 0 0 0 1px #8A734033;
    color: var(--cj-text);
    font-weight: 700;
    text-decoration: none;
}
.nav-item.active::before { background-color: var(--cj-accent-dark); }

/* The brand block above is now full-bleed with a rule of its own, so whichever
   of these two follows it supplies the air the rail's old top padding used to. */
.business-identity {
    margin: 18px 0 16px;
    padding: 10px 12px;
    border: 1px solid var(--cj-sidebar-border);
    border-left: 2px solid var(--cj-gold-dim);
    border-radius: var(--cj-radius-sm);
    background: var(--cj-bg);
    color: var(--cj-text);
}
.business-identity small,
.business-switcher label {
    display: block;
    margin-bottom: 3px;
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: .62rem;
    font-weight: 600;
    letter-spacing: .2em;
    text-transform: uppercase;
}

.business-switcher { margin: 18px 0 16px; padding: 0 2px; }
.business-switcher label { margin-bottom: 6px; font-size: .62rem; }
.business-switcher select {
    width: 100%;
    padding: 8px;
    border: 1px solid var(--cj-control-edge);
    border-radius: var(--cj-radius-sm);
    background: var(--cj-bg);
    color: var(--cj-text);
    font: inherit;
}
/* The selected business's full name, wrapped, above a select whose closed state
   cannot wrap. Shown only in the drawer (responsive section), where the rail is
   narrow enough to cut a long name off; the desktop rail is unchanged. */
.business-switcher-current { display: none; }
/* "Switch business", laid over the closed select in the drawer only, where the
   line above already names the business (responsive section). */
.business-switcher .business-switcher-action { display: none; }

/* The superseded rail status line. It read "System ready" beside a green lamp,
   which claimed Cajella was monitoring a system it has no health check, worker
   or queue for; an indicator that reports invented state is worse than no
   indicator, so the rail now ends after the navigation and nothing replaced it.

   The rule stays because this stylesheet corrects in place and deletes no
   selector (see the file header), and because a rail-level status line is a
   plausible thing to want back on the day there is something true to put in it.
   `margin-top: auto` is what pinned it to the foot of the rail without a fixed
   height. */
.sidebar-status {
    display: flex;
    align-items: center;
    gap: var(--cj-space-2);
    margin: auto 12px 0;
    padding-top: 14px;
    border-top: 1px solid var(--cj-border-subtle);
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: .62rem;
    letter-spacing: .18em;
    text-transform: uppercase;
}

/* Sign out is a routine action, not the loudest control in the rail — which is
   also why it is no longer *in* the rail: it is the last item of the user menu
   in the header, beside the person it signs out. These three rules are kept
   because this stylesheet corrects in place and deletes nothing (see the file
   header), and because a rail-level sign-out is a plausible thing to want back. */
.sidebar-signout { margin: 16px 12px 0; }
.sidebar-signout .button {
    width: 100%;
    border-color: var(--cj-border-control);
    background: transparent;
    color: var(--cj-muted);
}
.sidebar-signout .button:hover {
    border-color: var(--cj-gold-dim);
    background: var(--cj-sidebar-hover);
    color: var(--cj-text);
}

/* The drawer toggle and its backdrop. Both are hidden until the drawer
   breakpoint; see the responsive section. */
.sidebar-toggle-button { display: none; }
.sidebar-backdrop { display: none; }
.sidebar-close { display: none; }

/* ---------------------------------------------------------- typography ---- */

.eyebrow {
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: var(--cj-text-xs);
    font-weight: 600;
    letter-spacing: .2em;
    text-transform: uppercase;
}

/* The page's lede and its actions — no longer its heading.

   This was a command header: a 1.9rem uppercase `<h1>` on a gold rule. The
   heading moved into the application header band (`.topbar h1`), because the
   page name was being rendered in both places and an owner opening Components
   read COMPONENTS in the band and COMPONENTS again right beneath it. What is
   left is one line of prose and the page's controls, so the rule goes with the
   heading — the band above already draws one, and two rules twenty pixels apart
   is a box around nothing. `.page-header h1` is gone with it: the component
   emits no heading, so a rule for one could only be reached by reintroducing
   the defect. */
.page-header {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--cj-space-5);
    margin-bottom: var(--cj-space-5);
}
.page-subtitle { margin: 0; color: var(--cj-muted); }

/* Back to parent (spec SH-7, approved I-3): one quiet link above the page
   header at every width. Muted and small so it never competes with the page's
   heading or its actions; a real link, so it keeps the link hover. On a touch
   tier it is 44px tall (TI-1, 900 block) and pulled up into the content's top
   padding so it costs little height. */
.back-link {
    display: inline-flex;
    align-items: center;
    gap: var(--cj-space-1);
    min-height: 32px;
    margin-bottom: var(--cj-space-2);
    color: var(--cj-muted);
    font-size: .86rem;
    font-weight: 600;
}
.back-link:hover { color: var(--cj-text); }
.back-link .icon { width: 16px; height: 16px; }

/* Section titles inside panels share the display face, one step down. Applied
   by element rather than by class so a card written before the HUD landed still
   gets the treatment. */
.card > h2,
.detail-section-header h2,
.form-section > h2,
.modal-header h2 {
    font-family: var(--cj-font-display);
    font-weight: 600;
    letter-spacing: .09em;
    text-transform: uppercase;
}

/* `x-page-header` emits exactly `<div class="page-actions">`, so the spacing
   between actions has to come from here. It previously existed only inside the
   mobile media query, which left desktop with no `gap` at all: the templates
   are minified, so there was not even inter-tag whitespace between two buttons
   and they rendered touching. */
.page-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--cj-space-2);
}
/* Why a disabled action is disabled, said where it can be read rather than in a
   hover-only tooltip (spec P-2, mobile plan MX-17): the same words as its
   `title`, on a line of their own under the actions. */
.page-action-note {
    flex-basis: 100%;
    color: var(--cj-muted);
    font-size: var(--cj-text-sm);
    text-align: right;
}

/* -------------------------------------------------------------- panels ---- */

/* One panel shape, for every panel in Cajella.
 *
 * A Cajella panel is square-sided with a small 45° cut at each corner, a 1px
 * edge that follows the cut, a flat dark face, and no shadow. That shape is the
 * thing a person recognises before they read a word on the screen, so it is
 * declared once, here, over the list of classes that are panels — not per page,
 * and not per module.
 *
 * Why two pseudo-elements rather than `border` + `clip-path`:
 *
 *   A `border` cannot follow a clipped corner. Clip a bordered box and the
 *   straight runs of border survive while the diagonal comes out unlined — the
 *   edge appears to stop short of the corner, which reads as a rendering fault
 *   rather than as a cut. So the edge is drawn as a *shape*: `::before` is the
 *   panel's silhouette filled with the edge colour, and `::after` is the same
 *   silhouette inset by 1px filled with the face colour. What is left showing
 *   between them is a 1px line along every side including the two diagonals.
 *
 * Why `isolation: isolate` and negative z-index rather than a wrapper element:
 *
 *   Both pseudo-elements have to paint *behind the panel's content*. Inside a
 *   stacking context, negative-z-index descendants paint after the element's own
 *   background and before its in-flow content, which is exactly the layer a
 *   panel background belongs in — and it needs no extra element. That matters
 *   more than it sounds: this treatment lands on markup that already exists and
 *   several of those panels are pinned byte for byte by tests
 *   (`<section class="card detail-section">`, `<header class="detail-section-header">`,
 *   `<a class="hud-widget" href="…">`). None of them can gain a wrapper.
 *
 * Why `clip-path` is NOT on the panel itself:
 *
 *   A panel contains absolutely positioned children: `.notification-badge`
 *   sits on a control, and the header menus open below theirs.
 *   `clip-path` clips descendants, so a clipped panel would cut its own menus
 *   off at the edge. Only the two pseudo-elements are clipped, and they contain
 *   nothing.
 *
 * Three custom properties parameterise the whole thing, so a variant — inset,
 * emphasised, warning, critical, selected, compact — is a token change and never
 * a second implementation of the shape. */
.panel,
.card,
.section-card,
.detail-section,
.form-section,
.metric-card,
.hud-widget,
.money-strip,
.attention-rail,
.product-identity,
.wizard-content,
.wizard-variant-card,
.header-menu,
.row-overflow-menu,
.section-picker-panel,
.modal-dialog {
    --cj-panel-cut: var(--cj-chamfer);
    --cj-panel-edge-color: var(--cj-panel-edge);
    --cj-panel-surface: var(--cj-panel-face);
    position: relative;
    isolation: isolate;
    border: 0;
    border-radius: 0;
    background: transparent;
}

/* `:is()` rather than the list written out twice more: the list is one fact and
   should have one home. Specificity is unchanged — `:is()` takes that of its
   most specific argument, and every argument here is a single class. */
:is(.panel, .card, .section-card, .detail-section, .form-section, .metric-card,
    .hud-widget, .money-strip, .attention-rail, .product-identity, .wizard-content,
    .wizard-variant-card, .header-menu, .row-overflow-menu, .section-picker-panel, .modal-dialog)::before,
:is(.panel, .card, .section-card, .detail-section, .form-section, .metric-card,
    .hud-widget, .money-strip, .attention-rail, .product-identity, .wizard-content,
    .wizard-variant-card, .header-menu, .row-overflow-menu, .section-picker-panel, .modal-dialog)::after {
    content: '';
    position: absolute;
    pointer-events: none;
}

/* The edge: the silhouette, in the edge colour. */
:is(.panel, .card, .section-card, .detail-section, .form-section, .metric-card,
    .hud-widget, .money-strip, .attention-rail, .product-identity, .wizard-content,
    .wizard-variant-card, .header-menu, .row-overflow-menu, .section-picker-panel, .modal-dialog)::before {
    z-index: -2;
    inset: 0;
    background: var(--cj-panel-edge-color);
    clip-path: polygon(
        var(--cj-panel-cut) 0,
        calc(100% - var(--cj-panel-cut)) 0,
        100% var(--cj-panel-cut),
        100% calc(100% - var(--cj-panel-cut)),
        calc(100% - var(--cj-panel-cut)) 100%,
        var(--cj-panel-cut) 100%,
        0 calc(100% - var(--cj-panel-cut)),
        0 var(--cj-panel-cut)
    );
}

/* The face: the same silhouette, inset by the width of the edge. Its own cut is
   1px smaller, which is what keeps the diagonal the same 1px as the sides
   instead of the 1.41px a same-size cut would leave. */
:is(.panel, .card, .section-card, .detail-section, .form-section, .metric-card,
    .hud-widget, .money-strip, .attention-rail, .product-identity, .wizard-content,
    .wizard-variant-card, .header-menu, .row-overflow-menu, .section-picker-panel, .modal-dialog)::after {
    z-index: -1;
    inset: 1px;
    background: var(--cj-panel-surface);
    clip-path: polygon(
        calc(var(--cj-panel-cut) - 1px) 0,
        calc(100% - var(--cj-panel-cut) + 1px) 0,
        100% calc(var(--cj-panel-cut) - 1px),
        100% calc(100% - var(--cj-panel-cut) + 1px),
        calc(100% - var(--cj-panel-cut) + 1px) 100%,
        calc(var(--cj-panel-cut) - 1px) 100%,
        0 calc(100% - var(--cj-panel-cut) + 1px),
        0 calc(var(--cj-panel-cut) - 1px)
    );
}

/* The base panel, for anything built after this pass. `.card` remains the name
   the application's existing markup uses and the two are the same object.
   A panel holds no padding itself: the header band and the body each carry
   their own, which is what lets the band reach the panel's edge. */
.panel { padding: 0; }

/* The geometry above needs `position: relative` on every panel, because a
   panel's edge and face are two absolutely positioned pseudo-elements that need
   a containing block. Two of those panels are popovers that must float above the
   page instead. `.row-overflow-menu` is a top-layer popover, `position: fixed`
   further down this file; `.header-menu` declares `position: absolute` five
   hundred lines *above* it, where a later rule of equal specificity silently
   took it away.
 *
 * The consequence was invisible at desktop width and not at all invisible on a
 * phone: both header popovers were laid out in flow inside the header row, so
 * opening the user menu at 390px stretched that row to 353px inside a 390px
 * viewport and the whole page scrolled sideways. Restated here — immediately
 * after the geometry section rather than inside it, because `PanelLanguageTest`
 * counts each panel's appearances in that section and a sixth is a panel with a
 * duplicated edge as far as it can tell. `ApplicationShellTest` pins this one. */
.header-menu { position: absolute; }

/* ---- panel surfaces ---- */

/* A region nested inside a panel. One step lighter, a quieter edge and a smaller
   cut, so it reads as *inside* rather than as a second panel that happens to
   overlap the first. */
.panel-inset {
    --cj-panel-cut: var(--cj-chamfer-sm);
    --cj-panel-edge-color: var(--cj-panel-edge-inset);
    --cj-panel-surface: var(--cj-panel-inset-face);
}

/* ---- panel states ---- */

/* Four states, and each one moves the *edge* — never the face. A panel that
   tints its whole surface for a warning is a panel nobody can read, and a
   dashboard of them is a dashboard nobody looks at twice. */
.panel-emphasis { --cj-panel-edge-color: var(--cj-panel-edge-emphasis); }
.panel-warning { --cj-panel-edge-color: var(--cj-panel-edge-warning); }
.panel-critical { --cj-panel-edge-color: var(--cj-panel-edge-critical); }
.panel-selected {
    --cj-panel-edge-color: var(--cj-panel-edge-selected);
    --cj-panel-surface: var(--cj-panel-selected-face);
}

/* ---- panel headers ---- */

/* `[icon] TITLE ──────────────── optional action`
 *
 * Compact, tracked caps, a small semantic glyph, a hairline under it, and the
 * band reaches both edges of the panel. Reaching the edge is the point: a
 * heading floating inside the panel's padding with a short rule under it is the
 * SaaS card header this pass is replacing. It is what visually joins a header to
 * the table or the form below it.
 *
 * `.panel-head` is for panels built with `x-panel`. The three headers the
 * application already renders — a detail section's, a form section's and a
 * modal's — are given the same treatment where they are declared, rather than
 * being asked to change their markup. */
.panel-head {
    display: flex;
    align-items: center;
    gap: var(--cj-icon-gap);
    min-height: 36px;
    padding: var(--cj-panel-head-pad-y) var(--cj-panel-pad-x);
    border-bottom: 1px solid var(--cj-panel-edge);
}
.panel-title {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    color: var(--cj-text);
    font-family: var(--cj-font-display);
    font-size: .76rem;
    font-weight: 600;
    letter-spacing: var(--cj-track-label);
    text-transform: uppercase;
}
.panel-actions { display: flex; flex: 0 0 auto; flex-wrap: wrap; align-items: center; gap: var(--cj-space-2); }
.panel-body { padding: var(--cj-panel-pad-y) var(--cj-panel-pad-x); }
/* A body whose whole content is a table or a list hands the gutters to it, and
   keeps back the one pixel the panel's own edge is drawn in. */
.panel-body-flush { padding: 1px; }

/* ---- icons in panels ---- */

/* Three treatments, one family, one delivery mechanism — the shell pass's
   `--cj-icon-*` mask selected by `data-icon` and tinted by `currentColor`. The
   glyph is drawn on the element for a plain or status icon and on `::before` for
   a framed one, because a framed icon's own box carries the frame.
 *
 * Deliberately a separate rule from `.nav-item::before, .header-icon`: that one
 * pins an 18px column because the rail's alignment depends on it, and these are
 * sized to the text beside them. */
.icon,
.panel-icon,
.metric-trend {
    display: block;
    flex: 0 0 auto;
    width: 15px;
    height: 15px;
    background-color: currentColor;
    -webkit-mask: var(--cj-glyph, none) center / contain no-repeat;
    mask: var(--cj-glyph, none) center / contain no-repeat;
}
/* A panel's own glyph is gold-dim: present, quiet, and the same on every panel
   so the header band reads as one system. */
.panel-icon { color: var(--cj-gold-dim); }

/* The framed icon — the outlined container the reference's metric tiles use.
   Same cut as everything else, at the smallest size. */
.icon-framed {
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    width: 26px;
    height: 26px;
    border: 1px solid var(--cj-panel-edge);
    background: var(--cj-panel-inset-face);
    color: var(--cj-accent-dark);
    clip-path: polygon(
        var(--cj-chamfer-xs) 0,
        calc(100% - var(--cj-chamfer-xs)) 0,
        100% var(--cj-chamfer-xs),
        100% calc(100% - var(--cj-chamfer-xs)),
        calc(100% - var(--cj-chamfer-xs)) 100%,
        var(--cj-chamfer-xs) 100%,
        0 calc(100% - var(--cj-chamfer-xs)),
        0 var(--cj-chamfer-xs)
    );
}
.icon-framed::before {
    content: '';
    display: block;
    width: 15px;
    height: 15px;
    background-color: currentColor;
    -webkit-mask: var(--cj-glyph, none) center / contain no-repeat;
    mask: var(--cj-glyph, none) center / contain no-repeat;
}

/* A status glyph takes its meaning from the text beside it and its colour from
   the state around it. It is never the only thing saying what the state is —
   `.status-badge` and the attention rows both carry the word as well. */
.icon-ok { color: var(--cj-success); }
.icon-warning { color: var(--cj-warning); }
.icon-critical { color: var(--cj-danger); }
.icon-neutral { color: var(--cj-muted); }

/* ---- the cut on the smaller controls ---- */

/* A field, a choice and a chip are the same family as the panels, so they
   carry the same corner. `button, .button` and `.status-badge` declare their
   own — they need different sizes — and the two icon frames declare theirs,
   which accounts for every cut left in the stylesheet. A page number and the
   row-overflow trigger *were* here too until the button correction below:
   both are declared part of the button family (their own rules still say so
   — `.pagination-page`, `.row-overflow-trigger`), so both moved to
   `--cj-radius-control` with it rather than being left as the one cut corner
   next to every other now-rounded control. */
.form-control,
.form-choice,
.option-value-chip {
    clip-path: polygon(
        var(--cj-chamfer-sm) 0,
        calc(100% - var(--cj-chamfer-sm)) 0,
        100% var(--cj-chamfer-sm),
        100% calc(100% - var(--cj-chamfer-sm)),
        calc(100% - var(--cj-chamfer-sm)) 100%,
        var(--cj-chamfer-sm) 100%,
        0 calc(100% - var(--cj-chamfer-sm)),
        0 var(--cj-chamfer-sm)
    );
}

/* The cut corners' edge, for every bordered control the cut is applied to
   (inputs, selects, textareas, the searchable-select input and choice cards).
   A border cannot follow a clipped corner: its straight runs survive and the
   four 45° diagonals come out unlined, so the edge stopped short at every
   corner (the panel shape explains the same fault, `.panel::before`). The
   border still draws the straight runs, dashed when disabled; these four
   layers draw the diagonals, a 1px stroke on the cut inside each corner
   square, in `--cj-cut-edge`. Every state that sets a `border-color` sets
   `--cj-cut-edge` beside it, and sets its face with `background-color`, so
   no state removes the strokes. They are backgrounds, not a pseudo-element,
   because an `<input>` or `<select>` cannot draw one. `.option-value-chip`
   is a pill with its own edge and is not one of these. */
.form-control,
.form-choice {
    --cj-cut-edge: var(--cj-control-edge);
    background-image:
        linear-gradient(to bottom right, #0000 50%, var(--cj-cut-edge) 50% calc(50% + 1px), #0000 0),
        linear-gradient(to bottom left, #0000 50%, var(--cj-cut-edge) 50% calc(50% + 1px), #0000 0),
        linear-gradient(to top right, #0000 50%, var(--cj-cut-edge) 50% calc(50% + 1px), #0000 0),
        linear-gradient(to top left, #0000 50%, var(--cj-cut-edge) 50% calc(50% + 1px), #0000 0);
    background-position: top left, top right, bottom left, bottom right;
    background-size: var(--cj-chamfer-sm) var(--cj-chamfer-sm);
    background-repeat: no-repeat;
    background-origin: border-box;
    background-clip: border-box;
}

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

/* The metric treatment, shared by the navigational tile the HUD pass introduced
   (`.hud-widget`, which is an `<a>` and opens the page it counts) and the plain
   readout this pass adds (`.metric-card`). They are the same object; one of them
   is a link. Keeping them one rule is what stops the dashboard and the interior
   pages growing two different ideas of what a figure looks like.
 *
 * The grid is `[icon] [label]` on the first row and the figure across both on
 * the second, so the number starts at the panel's own text edge rather than
 * being indented under the icon. Nothing is centred: the reference tiles are
 * left-aligned and read down a column, and centring a stat card is the habit
 * that makes six of them look like six unrelated things. */
.metric-card,
.hud-widget {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-content: start;
    align-items: center;
    gap: var(--cj-space-1) var(--cj-icon-gap);
    padding: var(--cj-metric-pad-y) var(--cj-metric-pad-x);
    color: var(--cj-text);
    text-decoration: none;
}
.metric-card > .icon-framed,
.hud-widget > .icon-framed { grid-row: 1; grid-column: 1; }

/* Small, tracked, dim: a label is what the figure is *of*, and it should never
   compete with the figure. */
.metric-label,
.hud-widget-label {
    grid-column: 2;
    align-self: center;
    min-width: 0;
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: var(--cj-text-micro);
    font-weight: 600;
    letter-spacing: var(--cj-track-label);
    text-transform: uppercase;
}

/* The figure is the dominant element on the tile. Tabular, so a column of them
   lines up on the same stems; gold, because on this HUD a number is money or a
   count and both are gold. */
.metric-figure {
    display: flex;
    grid-column: 1 / -1;
    align-items: baseline;
    gap: 5px;
    min-width: 0;
}
.metric-figure .metric-value { display: inline; }
.metric-unit {
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: .74rem;
    font-weight: 600;
    letter-spacing: .1em;
    text-transform: uppercase;
}

/* The comparison or context line, and the trend glyph that can lead it. Both are
   optional; a metric with nothing to compare against says nothing rather than
   saying "0%". */
/* `min-width: 0` on both of these and on the label: a grid item's automatic
   minimum is its content, so a long note refused to wrap and widened the tile
   instead. */
.metric-context {
    display: flex;
    grid-column: 1 / -1;
    min-width: 0;
    align-items: center;
    gap: 6px;
    color: var(--cj-muted);
    font-size: .76rem;
}
.metric-trend { width: 13px; height: 13px; }
.metric-trend-up { color: var(--cj-success); }
.metric-trend-down { color: var(--cj-danger); }
.metric-trend-flat { color: var(--cj-muted); }

/* Metric states. Each moves the edge, the framed icon and the figure — three
   quiet signals rather than one loud tinted card. `-neutral` is the default and
   changes nothing, so it can be passed unconditionally by a caller that computes
   its state.
 *
 * `-positive` deliberately does not paint the panel olive: "this bucket is
 * clear" is the ordinary state of most tiles most days, and a screen where the
 * ordinary state is decorated has spent its emphasis on nothing. */
.metric-neutral,
.metric-positive { --cj-panel-edge-color: var(--cj-panel-edge); }
.metric-positive .metric-value { color: var(--cj-success); }
.metric-positive > .icon-framed { color: var(--cj-success); }
.metric-warning { --cj-panel-edge-color: var(--cj-panel-edge-warning); }
.metric-warning .metric-value { color: var(--cj-warning); }
.metric-warning > .icon-framed { color: var(--cj-warning); border-color: var(--cj-warning-border); }
.metric-critical { --cj-panel-edge-color: var(--cj-panel-edge-critical); }
.metric-critical .metric-value { color: var(--cj-danger); }
.metric-critical > .icon-framed { color: var(--cj-danger); border-color: var(--cj-danger-border); }

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

/* One system. `.button` carries the shape and the default (primary) skin, so a
   bare `class="button"` is still a primary action; `.button-*` modifiers change
   only what differs. Nothing here re-declares the whole property set. */
/* Olive fill or gold/olive outline — the HUD skill admits no consumer blue and
   no rounded pill. Labels are tracked uppercase, which is what makes a squared
   control read as a HUD control rather than as an unstyled one. */
/* Approved 2026-09: a conventional rounded rectangle, not the panel's cut
   corner. The cut read as a rendering bug on a filled, bordered, text-bearing
   control in a way it never did on a panel (nothing sat close enough to the
   corner to make the diagonal legible as a decision rather than as clipping),
   and the product owner flagged the secondary-button case in particular. The
   border is a complete rectangle now, at `--cj-radius-control` on every side,
   with nothing clipped away — see the focus-ring note further down for what
   this simplified. `.header-control` still opts out of sharing this rule
   outright below; it carries the notification badge and has its own square
   chrome by design, unrelated to this correction. */
button,
.button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    min-height: var(--cj-control-height);
    border: 1px solid var(--cj-accent-hover);
    border-radius: var(--cj-radius-control);
    padding: 7px 14px;
    background: var(--cj-accent);
    color: var(--cj-on-accent);
    font: inherit;
    font-family: var(--cj-font-display);
    font-weight: 600;
    font-size: .84rem;
    letter-spacing: .1em;
    text-transform: uppercase;
    cursor: pointer;
    text-decoration: none;
    transition: background .15s, border-color .15s, color .15s, opacity .15s;
}

/* An icon inside a button. Decorative: the label beside it is the action. */
.button-icon {
    flex: 0 0 14px;
    width: 14px;
    height: 14px;
    background-color: currentColor;
    -webkit-mask: var(--cj-glyph, none) center / contain no-repeat;
    mask: var(--cj-glyph, none) center / contain no-repeat;
}
button:hover,
.button:hover { background: var(--cj-accent-hover); color: var(--cj-on-accent); text-decoration: none; }
/* Pressed, separate from hover (spec A-3). A phone has no hover, and on a slow
   network a tap that changes nothing reads as a missed tap. The inset gold edge
   and the 1px drop use properties no variant's hover sets, so every variant
   shows the same press without a rule for each. */
button:active,
.button:active { box-shadow: inset 0 0 0 2px var(--cj-accent-dark); transform: translateY(1px); }

.button-primary { border-color: var(--cj-accent-hover); background: var(--cj-accent); color: var(--cj-on-accent); }
.button-primary:hover { background: var(--cj-accent-hover); }

/* The gold outline. A secondary action is the one the owner reaches for as
   often as the primary, so it is a full-strength edge on carbon, not a greyed
   box. */
.button-secondary {
    border-color: var(--cj-gold-dim);
    background: transparent;
    color: var(--cj-accent-dark);
}
.button-secondary:hover {
    border-color: var(--cj-accent-dark);
    background: #C4A35A1a;
    color: var(--cj-accent-dark);
}

.button-subtle {
    border-color: transparent;
    background: transparent;
    color: var(--cj-muted);
    font-weight: 600;
}
.button-subtle:hover { background: var(--cj-hover); color: var(--cj-text); }

.button-danger { border-color: var(--cj-danger-hover); background: var(--cj-danger-hover); color: var(--cj-text); }
.button-danger:hover { border-color: var(--cj-danger); background: var(--cj-danger); color: var(--cj-on-accent); }

/* The compact table action: a real control, sized for a dense row. Same
   radius as every other button — unlike the retired chamfer, a rounded
   corner does not need a smaller variant to keep from eating the corner of a
   short control. */
.button-small {
    min-height: 28px;
    padding: 3px 9px;
    font-size: var(--cj-text-xs);
}

/* The controls that are not shaped like buttons, stated here rather than left to
   be discovered.
 *
 * `.header-control` carries `.notification-badge` as an absolutely positioned
 * child and `clip-path` clips descendants, so a cut control would clip its own
 * badge; header chrome is square on purpose anyway. The other three are
 * borderless transparent hit areas — there is no shape to cut, and declaring the
 * cut on them would only mean four more places to keep in step. */
.header-control,
.row-action,
.chip-remove,
.modal-close { clip-path: none; }

/* A cut control un-cuts itself while it is focused.
 *
 * A focus ring is painted outside the border box and `clip-path` clips
 * anything outside the shape, so a chamfered control silently ate its own
 * focus ring — browser verification caught the Add material button focusing
 * with no visible indicator at all. Drawing the ring inside the shape instead
 * is not an option on the filled primary: gold on olive measures 2.1:1, below
 * the 3:1 WCAG 2.2 SC 1.4.11 asks of a focus indicator, where the same ring
 * on the page ground measures 8.8:1.
 *
 * So the corner comes back square for exactly as long as the control is
 * focused, and the ring is whole. The ring is the more important of the two.
 *
 * `button`, `.button`, `.pagination-page` and `.row-overflow-trigger` are
 * gone from this rule as of the 2026-09 button-shape correction: none of the
 * three carries a `clip-path` any more (they moved to a real `border-radius`
 * instead, `--cj-radius-control`), so there is nothing left on them for a
 * focus ring to be clipped by — the global `:focus-visible` rule now draws a
 * complete, unclipped ring on all three the same way it always did on plain
 * unstyled elements. Only the controls that keep the cut corner still need
 * the un-cut-on-focus treatment.
 *
 * `:has(:focus-visible)` on a choice and `:focus-within` on a chip because
 * the ring belongs to a control *inside* them — a radio inside a choice, the
 * remove button inside an option chip — and a parent's clip path clips its
 * descendants as well as itself. A choice keys off the keyboard's
 * `:focus-visible`, so a mouse click draws no ring there to un-clip. */
.form-control:focus-visible,
.form-choice:has(:focus-visible),
.option-value-chip:focus-within { clip-path: none; }
/* Unclipped, the corners are square, so the corner strokes go with the cut. */
.form-control:focus-visible,
.form-choice:has(:focus-visible) { background-size: 0 0; }

/* Destructive, but not louder than the action beside it. Removing a BOM line
   sat in the same orange as "Continue to Production"; a row of eight identical
   primaries tells the owner nothing about which one is safe. */
.button-danger-subtle { background: transparent; color: var(--cj-danger); font-weight: 650; }
.button-danger-subtle:hover { background: var(--cj-danger-bg); color: var(--cj-danger); }

button:disabled,
.button[aria-disabled=true] { opacity: .55; cursor: not-allowed; }
/* A disabled `<button>` never matches `:active`; a disabled `.button` link or
   span would, so it is told not to look pressed. */
.button[aria-disabled=true]:active { box-shadow: none; transform: none; }

/* A submit on its way (mobile plan MX-18, spec E-1). submit-guard.js marks the
   control `aria-disabled` so a repeat press is refused, but it is not a refusal:
   the label keeps full strength and the cursor says "working" rather than "no".
   A short gold sweep along the bottom edge shows it is moving; with reduced
   motion it is a still gold edge. */
button[data-pending],
button[data-pending][aria-disabled=true] { position: relative; overflow: hidden; opacity: 1; cursor: progress; }
button[data-pending]::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: 0;
    width: 40%;
    height: 2px;
    background: var(--cj-accent-dark);
    animation: cj-pending 1.1s linear infinite;
}
@keyframes cj-pending {
    from { transform: translateX(-100%); }
    to { transform: translateX(250%); }
}

/* A text-weight action inside a table row. Explicitly returned to the reading
   sans in its natural case: the HUD skill forbids the stencil face in a table
   body, and `.row-action` is worn by `<button>` elements that would otherwise
   inherit the display treatment from the shared button rule above. */
.row-action {
    display: inline-flex;
    align-items: center;
    min-height: 34px;
    padding: 5px 8px;
    border: 0;
    background: transparent;
    color: var(--cj-accent-dark);
    font-family: inherit;
    font-size: .82rem;
    font-weight: 650;
    letter-spacing: normal;
    text-transform: none;
    cursor: pointer;
    text-decoration: none;
}
.row-action:hover { background: transparent; color: var(--cj-warning); text-decoration: underline; }
.row-action:active { background: var(--cj-hover); box-shadow: inset 0 0 0 1px var(--cj-gold-dim); }
.row-action-danger { color: var(--cj-danger); }
.row-action-danger:hover { color: var(--cj-danger); }

/* The remove control on an option-value chip. Comfortable to tap, and it says
   what it removes rather than presenting a bare glyph. */
.chip-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 36px;
    min-height: 36px;
    padding: 0;
    border: 0;
    border-radius: var(--cj-radius-xs);
    background: transparent;
    color: var(--cj-danger);
    font-size: 1.2rem;
    line-height: 1;
    cursor: pointer;
}
.chip-remove:hover { background: var(--cj-danger-bg); color: var(--cj-danger); }

/* An option value, with the control that removes it. */
.option-value-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--cj-space-1);
    padding: 2px 2px 2px 12px;
    border: 1px solid var(--cj-border-control);
    border-radius: var(--cj-radius-pill);
    background: var(--cj-hover);
    color: var(--cj-text);
    font-size: var(--cj-text-sm);
    font-weight: 650;
}
.option-value-chip form { display: inline; margin: 0; }

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

.form-shell { max-width: 760px; }

/* A grouped set of choices. The browser's default fieldset is a 2px inset ridge
   in its own grey — the one piece of furniture in the application that belonged
   to no system — so it takes the panel language too: the inset surface, a thin
   edge and a tracked-caps legend. */
fieldset {
    margin: 0 0 var(--cj-space-4);
    padding: var(--cj-space-3) var(--cj-space-4);
    border: 1px solid var(--cj-panel-edge-inset);
    border-radius: 0;
    background: var(--cj-panel-inset-face);
}
fieldset > legend {
    padding: 0 var(--cj-space-2);
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: var(--cj-text-micro);
    font-weight: 600;
    letter-spacing: var(--cj-track-label);
    text-transform: uppercase;
}

.form-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: var(--cj-space-4); }
/* A field label sits below a panel title in the hierarchy, so it is smaller and
   lighter than it was: at .84rem/750 it was heavier than the panel header above
   it, which inverted the two. */
.form-field label,
.form-label { font-size: .82rem; font-weight: 700; }

/* The required marker. Previously written as `.required` in two templates with
   no rule anywhere, so it rendered as unstyled body text. */
.required { color: var(--cj-danger); font-weight: 750; }

.form-control {
    width: 100%;
    padding: 10px 12px;
    border: 1px solid var(--cj-control-edge);
    border-radius: var(--cj-radius-sm);
    background-color: var(--cj-surface);
    color: var(--cj-text);
    font: inherit;
}
.form-control:focus { border-color: var(--cj-accent); --cj-cut-edge: var(--cj-accent); }
.form-control:focus-visible { outline: 3px solid var(--cj-focus); outline-offset: 2px; }
/* An explicit placeholder. The browser default was 1.81:1 against a typed
   value, so a hint like "Name, brand, type…" could be read as one. `opacity`
   because Firefox otherwise halves it. */
.form-control::placeholder { color: var(--cj-placeholder); opacity: 1; }
/* One disabled rule for every input, select and textarea. The control's own
   colour and face used to override the browser's dimming, so a locked field
   looked editable. `-webkit-text-fill-color` and `opacity` stop WebKit fading
   the text past legible. */
.form-control:disabled {
    border-style: dashed;
    border-color: var(--cj-disabled-edge);
    --cj-cut-edge: var(--cj-disabled-edge);
    background-color: var(--cj-disabled-face);
    color: var(--cj-disabled-text);
    -webkit-text-fill-color: var(--cj-disabled-text);
    opacity: 1;
    cursor: not-allowed;
}
/* A readonly field is a value, not somewhere to type, so it drops the edge and
   face a field has. It is still focusable, so the keyboard ring stays. */
.form-control[readonly],
.form-control[readonly]:focus { border-color: transparent; --cj-cut-edge: transparent; background-color: transparent; }
/* A select showing its unfiltered default or its not-yet-chosen prompt reads
   like a placeholder. The template marks the option that means "nothing
   chosen" (`data-empty`), because an empty value can be a real answer ("No
   brand"). `:checked` follows the user's choice before Apply, with no script. */
.form-control:has(> option[data-empty]:checked) { color: var(--cj-placeholder); }
/* The open list is unchanged: where the platform draws options and group
   labels with the select's colour (Firefox, Windows), they stay full strength. */
.form-control option,
.form-control optgroup { color: var(--cj-text); }

/* A value Cajella worked out, sitting where the form's other answers sit.
   Aligned like `.form-control` so the Opening inventory group reads as one
   column of fields, and deliberately not bordered like one: it is an <output>,
   not somewhere to type. The figure carries the emphasis instead. */
.form-derived {
    display: block;
    padding: 10px 0;
    color: var(--cj-accent-dark);
    font-variant-numeric: tabular-nums;
    font-size: 1.05rem;
    font-weight: 750;
}

.form-help { color: var(--cj-muted); font-size: .82rem; }

/* Field composition (mobile application spec F-2, plan F-O). A money field's
   currency prefix (`x-field-affix`) and a quantity and its unit side by side
   (`.field-pair`) are a phone presentation, drawn in the 640px block. Above it
   neither wrapper is a box and the prefix is not shown, so the field and the
   form's layout are exactly what they were. */
.field-affix,
.field-pair { display: contents; }
.field-affix-text { display: none; }

/* The Event Mode quantity stepper (mobile plan MX-19, spec F-11), built by
   event-sale.js around the unchanged field: a 44px − and + either side, the
   field taking the rest of the row, at every width. */
.quantity-stepper { display: flex; align-items: stretch; gap: 8px; }
.quantity-stepper .form-control { flex: 1 1 auto; min-width: 0; text-align: center; }
.quantity-stepper-button { flex: 0 0 auto; min-width: 44px; min-height: 44px; padding: 0 12px; font-size: 1.1rem; letter-spacing: 0; }
.form-error { color: var(--cj-danger); font-size: .82rem; }
.has-errors .form-control { border-color: var(--cj-danger); --cj-cut-edge: var(--cj-danger); }
/* A line editor's per-line error sits under its own field (mobile plan §6.4). */
.line-editor td .form-error { display: block; margin-top: 4px; }
/* Add Products (Events EVF-15): a row's target, its own error under it; every
   refused row named above the table, rows on other pages included; the
   selection count beside Add selected; the no-JavaScript limit beside the
   toolbar. */
.add-products-target { display: inline-block; text-align: left; }
.add-products-target .form-control { width: 7.5rem; }
.add-products-target .form-error { display: block; margin-top: 4px; }
/* A row's selection and Allocate are the shared choice cards (FV-D1; MA F-5:
   44px on a phone), sized to their cells rather than the 160px fieldset card. */
.data-table td .form-choice { min-width: 0; margin: 0; }
/* "Up to target" stays one line, as the label it replaced did. */
.data-table td[data-label="Allocate"] .form-choice { white-space: nowrap; }
.add-products-options { margin-top: var(--cj-space-4); }
.add-products-count { margin-right: auto; color: var(--cj-muted); font-weight: 600; }
.form-error-list { margin: 0 0 var(--cj-space-4); padding-left: 1.2rem; color: var(--cj-danger); }
.list-toolbar-note { margin: 0 0 var(--cj-space-3); color: var(--cj-muted); font-size: var(--cj-text-sm); }

/* SKU Assistant (spec §9–§10, plan §10.4). Assistance under the SKU field,
   filled by sku-assistant.js and server-rendered empty and `hidden`. It is
   never a validation error: no `--cj-danger`, no `.form-error`, and a Blocking
   value is a gold warning with a text prefix. It sits in normal flow inside
   the field, never a popover, so it cannot clip, cover the on-screen keyboard
   or overflow at 320px. No display rule here, so `hidden` always wins. */
.sku-assist {
    font-size: .82rem;
    overflow-wrap: anywhere;
    transition: opacity .15s ease;
}
/* A lookup in flight dims the previous result rather than clearing it, so the
   panel does not flicker while typing. */
.sku-assist[aria-busy="true"] { opacity: .6; }
/* Spacing between the parts that are showing; a hidden part takes none. */
.sku-assist > :not([hidden]) ~ :not([hidden]) { margin-top: 6px; }
.sku-assist p { margin: 0; }
.sku-assist-warning {
    padding: 6px 10px;
    border: 1px solid var(--cj-warning-border);
    border-radius: var(--cj-radius-sm);
    background: var(--cj-warning-bg);
    color: var(--cj-warning);
}
.sku-assist-warning a { color: inherit; text-decoration: underline; }
.sku-assist-note { color: var(--cj-muted); }
.sku-assist-suggestions { margin: 0; padding: 0; list-style: none; }
/* A suggestion is a 44px target at every width (spec §10). The highlight is
   the combobox one: where the arrow keys or the pointer are. */
.sku-assist-suggestion {
    min-block-size: 44px;
    padding: 8px 10px;
    border-radius: var(--cj-radius-sm);
    cursor: pointer;
}
.sku-assist-suggestion:hover,
.sku-assist-suggestion.is-active {
    background: var(--cj-highlight-face);
    box-shadow: inset 3px 0 0 var(--cj-selected-edge);
}
.sku-assist-suggestion:focus-visible { outline: 3px solid var(--cj-focus); outline-offset: 2px; }
.sku-assist-matches ul { margin: 0; padding: 0; list-style: none; }
.sku-assist-heading {
    color: var(--cj-muted);
    font-size: .72rem;
    font-weight: 700;
    letter-spacing: var(--cj-track-label);
    text-transform: uppercase;
}
.sku-assist-more,
.sku-assist-quiet { color: var(--cj-muted); }
.sku-assist-sku { font-weight: 600; font-variant-numeric: tabular-nums; }
.sku-assist-matches li + li { margin-top: 2px; }
/* The live region is 1px and clipped, but as a flex item of `.form-field` it
   still took a gap: out of flow, the idle field keeps its base geometry. */
.sku-assist-status { position: absolute; }
/* "Retired" and "this product" read as labels on a match row, not values. */
.sku-assist-tag {
    margin-left: 6px;
    color: var(--cj-muted);
    font-size: .72rem;
    letter-spacing: var(--cj-track-label);
    text-transform: uppercase;
}

.form-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    align-items: center;
    gap: 10px;
    padding-top: var(--cj-space-4);
    margin-top: var(--cj-space-2);
    border-top: 1px solid var(--cj-border-subtle);
}
/* A form section owns its own footer, so a footer inside one does not draw a
   second rule directly under the section's own border. */
.form-section > .form-actions { border-top: 0; margin-top: 0; }

.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--cj-space-4); }

/* A grouped form section is a panel, and its `<h2>` is that panel's header —
   the same full-bleed band, tracked caps and hairline a detail section wears, so
   Business Settings and the Material and Component forms are made of the same
   object as the rest of the application. The markup is untouched: the tests pin
   these headings as a bare `<h2>Identity</h2>`, and the treatment is the
   stylesheet's job anyway. */
.form-section {
    grid-column: 1 / -1;
    padding: var(--cj-panel-pad-y) var(--cj-panel-pad-x);
}
.form-section + .form-section { margin-top: 0; }
.form-section > h2 {
    margin:
        calc(var(--cj-panel-pad-y) * -1)
        calc(var(--cj-panel-pad-x) * -1)
        var(--cj-panel-pad-y);
    padding: var(--cj-panel-head-pad-y) var(--cj-panel-pad-x);
    border-bottom: 1px solid var(--cj-panel-edge);
    font-size: .8rem;
    letter-spacing: var(--cj-track-label);
    line-height: 1.6;
}
.form-section > h2 + .form-help { display: block; margin: calc(var(--cj-space-3) * -1) 0 var(--cj-space-4); }
.form-section > .form-field:last-child { margin-bottom: 0; }

/* ------------------------------------------------- business settings ---- */

/* Business Settings' composition, and only its composition. The reference
   console's settings screen is a two-column grid of cards, each one a framed
   glyph beside a title, a rule under both, a line of description and then its
   content — so that is the shape here.
 *
 * What the reference is NOT copied on: its six cards are Shop Profile, Users &
 * Roles, Notifications, Integrations, Billing and Data & Backup, and Cajella
 * has none of those six. It has four real groups of settings, and a card for a
 * feature that does not exist is worse than a missing card, because the owner
 * believes they have something to configure. Its footer status bar and its
 * per-card counts and dates are left out for the same reason: nothing in the
 * application knows them.
 *
 * No new primitive. A settings card is a `.form-section` — the same panel the
 * Material and Component forms use — with its full-page `grid-column` released
 * so two of them sit side by side. Everything else it inherits. */
/* Approved 2026-09 (audit hardening; reconfirmed with the width
   normalization pass below): a compound selector, not a same-specificity
   rule that only wins because it happens to be declared after `.form-shell`
   in this file. `.form-shell.settings-form` outranks bare `.form-shell`
   regardless of source order, so the full-width intent survives even if this
   stylesheet is ever reordered or split. The Blade class list is unchanged —
   `business/settings.blade.php` already carries both classes.
   This override is not an obsolete width workaround made redundant by the
   page going `page-wide` — it is the reason the settings-grid can use that
   wider canvas at all. `.form-shell` still means "cap at 760px" everywhere
   else it appears (Customer, Channel, Event, Machine... every simple form in
   the app); without this override the settings-grid's two columns would be
   squeezed into it exactly as before, regardless of how wide the outer page
   is. */
.form-shell.settings-form { max-width: none; }
.settings-grid { gap: var(--cj-space-5); align-items: start; }
/* A card is as tall as what is in it, which is where this departs from the
   reference: its six cards hold a title and two lines each, so equal heights
   cost it nothing. Identity holds one field and Costing display holds two
   selects and three paragraphs, so stretching the row left 150px of empty
   bordered card under the business name — which reads as a missing field rather
   than as alignment. A ragged bottom edge is the honest version. */
.settings-grid > .form-section {
    grid-column: auto;
    align-self: start;
    padding: var(--cj-space-5);
}
/* The card header: glyph, title, and the rule the `.form-section` band already
   draws. One step up in size from a form section's heading, because here the
   card title is the most prominent thing on the card. */
.settings-grid > .form-section > h2 {
    display: flex;
    align-items: center;
    gap: var(--cj-space-3);
    margin:
        calc(var(--cj-space-5) * -1)
        calc(var(--cj-space-5) * -1)
        var(--cj-space-4);
    padding: var(--cj-space-3) var(--cj-space-5);
    font-size: .92rem;
}
/* Larger than a metric tile's frame — the reference's settings glyph is the
   biggest thing on the card after the title. */
.settings-grid .icon-framed { width: 34px; height: 34px; }
.settings-grid .icon-framed::before { width: 19px; height: 19px; }
.settings-grid > .form-section > h2 + .form-help { margin: calc(var(--cj-space-2) * -1) 0 var(--cj-space-4); }

/* A choice control inside a fieldset — sized to match `.form-control` and the
   button primitives it sits beside. */
.form-choice {
    display: inline-flex;
    align-self: flex-start;
    align-items: center;
    gap: var(--cj-space-2);
    min-width: 160px;
    margin: 0 var(--cj-space-2) 6px 0;
    padding: 9px 13px;
    border: 1px solid var(--cj-control-edge);
    border-radius: var(--cj-radius-sm);
    background-color: var(--cj-surface);
    color: var(--cj-text);
    font: inherit;
    font-size: .92rem;
    font-weight: 650;
    cursor: pointer;
    transition: background .15s, border-color .15s;
}
/* Hover changes the face only. It used to take the gold edge as well, close
   enough to the checked edge that a hovered card, or one a tap left hovered on
   a phone, read as chosen. */
.form-choice:hover { background-color: var(--cj-hover); }
/* The native radio dot or checkbox tick is the mark, at 16px; it is never
   restyled away, so every state below keeps it. Checked is 800 because a card
   inside a fieldset already rests at 700 (`.form-field label`). The mark keeps
   its 16px beside a label long enough to wrap (cajella#28). */
.form-choice input { flex-shrink: 0; width: 16px; height: 16px; margin: 0; accent-color: var(--cj-accent); cursor: pointer; }
.form-choice:has(input:checked) { border-color: var(--cj-selected-edge); --cj-cut-edge: var(--cj-selected-edge); background-color: var(--cj-selected-face); font-weight: 800; }
/* The keyboard ring only. A mouse or touch click on a card focuses its input
   but is not `:focus-visible`, so it draws nothing. */
.form-choice:has(:focus-visible) { outline: 3px solid var(--cj-focus); outline-offset: 2px; }
/* The card's ring is the input's ring, so the global `:focus-visible` ring on
   the radio or checkbox inside it would be a second one (spec R-6). Only the
   outline goes; the native mark is untouched. */
.form-choice input:focus-visible { outline: none; }
/* Every card in an erroring group takes the error edge, checked or not; the
   checked one still reads through its mark, face and weight. */
.has-errors .form-choice,
.has-errors .form-choice:has(input:checked) { border-color: var(--cj-danger); --cj-cut-edge: var(--cj-danger); }
/* The shared disabled language (spec R-5), as on `.form-control:disabled`. A
   disabled checked card keeps its mark and weight. */
.form-choice:has(input:disabled) {
    border-style: dashed;
    border-color: var(--cj-disabled-edge);
    --cj-cut-edge: var(--cj-disabled-edge);
    background-color: var(--cj-disabled-face);
    color: var(--cj-disabled-text);
    cursor: not-allowed;
}
.form-choice:has(input:disabled) input { cursor: not-allowed; }
/* Windows High Contrast drops the face, so checked keeps a system-coloured
   edge and disabled reads as system grey. The native mark and the focus
   outline are drawn by the system already. */
@media (forced-colors: active) {
    .form-choice:has(input:checked) { border-color: Highlight; }
    .form-choice:has(input:disabled) { border-color: GrayText; color: GrayText; }
    /* The corner strokes are backgrounds and the system chooses the border's
       colour, so the two could not be kept alike. The controls drop the cut
       instead: the system-coloured border is then a complete outline in every
       state, not one stopping at the corners. */
    .form-control,
    .form-choice { clip-path: none; background-image: none; }
    /* The combobox highlight is a face and a box-shadow bar, and both are
       dropped, so it takes a system outline instead. The check mark is text
       and survives as it is. */
    .searchable-select-option:hover,
    .searchable-select-option.is-active,
    .sku-assist-suggestion:hover,
    .sku-assist-suggestion.is-active { outline: 2px solid Highlight; outline-offset: -2px; }
    /* The current tile's bar and edge are panel backgrounds, also dropped
       (UC-7). */
    .hud-widget[aria-current] { outline: 2px solid Highlight; outline-offset: -2px; }
}

/* The filter row is a strip of compact controls, so its labels are the tracked
   caps the rest of the compact chrome wears rather than body-weight text: seven
   bold sentence-case labels above seven fields was the loudest row on a list
   page and none of it was the data. */
.filter-bar { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--cj-space-3); padding: var(--cj-space-3) 0; }
.filter-bar .form-field { margin-bottom: 0; gap: var(--cj-space-1); }
.filter-bar .form-field label,
.filter-bar .form-label {
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: var(--cj-text-micro);
    font-weight: 600;
    letter-spacing: var(--cj-track-label);
    text-transform: uppercase;
}
.filter-actions { display: flex; align-items: flex-end; gap: var(--cj-space-2); }
/* "N filters ·" before Clear (UI clarity UC-8): a filtered list never looks
   unfiltered. It reads with the Clear beside it, so it sits on the buttons'
   centre line and never wraps away from it. */
.filter-summary { align-self: center; color: var(--cj-text); font-size: var(--cj-text-sm); white-space: nowrap; }
/* A list's toolbar and its filter sheet (mobile application plan F-D, F-E).
   Above 640px neither wrapper is there as far as layout goes: search, the
   filters and Apply / summary / Clear are filter-bar items as they always
   were. What exists only for a phone — the Filters control, the toolbar's own
   primary action, the plan-limit line, the sheet's heading and its sort — is
   not shown. */
.list-toolbar,
.list-toolbar-trailing,
.list-sheet { display: contents; }
.list-toolbar-filters,
.list-toolbar-action,
.list-toolbar-limit,
.list-sheet-head,
.list-sheet-sort { display: none; }

/* A list is easier to scan when a row is one line tall. 14px of cell padding
   on an eleven-column table spends 300px on whitespace alone. */
.data-table td { padding: 11px 12px; }
.data-table th { padding: 10px 12px; }

/* The column carrying the record's name. Without a floor it is squeezed by the
   columns beside it and a long name wraps to seven lines, making one row as
   tall as four. The table scrolls instead, which §10 prefers when the columns
   genuinely matter. */
.data-table .cell-name { min-width: 220px; }

/* ------------------------------------------------------------ feedback ---- */

/* A HUD notice is a panel with a coloured left edge — the severity is carried
   by the bar, so the message itself stays readable rather than being tinted to
   the edge of legibility. */
.alert {
    margin-bottom: 20px;
    padding: 12px 16px;
    border: 1px solid;
    border-left-width: 3px;
    border-radius: var(--cj-radius-sm);
}
.alert-success { border-color: var(--cj-success-border); background: var(--cj-success-bg); color: var(--cj-success); }
.alert-info { border-color: var(--cj-info-border); background: var(--cj-info-bg); color: var(--cj-text); }
.alert-warning { border-color: var(--cj-warning-border); background: var(--cj-warning-bg); color: var(--cj-warning); }
.alert-error { border-color: var(--cj-danger-border); background: var(--cj-danger-bg); color: var(--cj-danger); }

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

/* Status reads by meaning, not by decoration: olive is ready, gold is caution,
   red is failure. Squared and bordered so a pill is still distinguishable in a
   dense table from the text beside it, and tracked uppercase so the word is
   scannable at 0.72rem. */
/* Compact, squared and cut like everything else — a chip, not a pill. The
   previous 2px radius was already square-ish; what made these read as pills was
   their height and padding, so both come down and the corner is cut instead. */
.status-badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: 20px;
    padding: 1px 7px;
    border: 1px solid var(--cj-border-control);
    border-radius: 0;
    background-color: var(--cj-neutral-bg);
    color: var(--cj-neutral);
    font-family: var(--cj-font-display);
    font-size: .7rem;
    font-weight: 600;
    letter-spacing: .1em;
    text-transform: uppercase;
    white-space: nowrap;
    clip-path: polygon(
        var(--cj-chamfer-xs) 0,
        calc(100% - var(--cj-chamfer-xs)) 0,
        100% var(--cj-chamfer-xs),
        100% calc(100% - var(--cj-chamfer-xs)),
        calc(100% - var(--cj-chamfer-xs)) 100%,
        var(--cj-chamfer-xs) 100%,
        0 calc(100% - var(--cj-chamfer-xs)),
        0 var(--cj-chamfer-xs)
    );
    /* The cut corners' edge, as on the form controls above: the border draws
       only the straight runs, so without these four strokes every corner of
       a badge was open. Each state sets `--cj-cut-edge` beside its
       `border-color`, and its face with `background-color`, so no state
       removes them. */
    --cj-cut-edge: var(--cj-border-control);
    background-image:
        linear-gradient(to bottom right, #0000 50%, var(--cj-cut-edge) 50% calc(50% + 1px), #0000 0),
        linear-gradient(to bottom left, #0000 50%, var(--cj-cut-edge) 50% calc(50% + 1px), #0000 0),
        linear-gradient(to top right, #0000 50%, var(--cj-cut-edge) 50% calc(50% + 1px), #0000 0),
        linear-gradient(to top left, #0000 50%, var(--cj-cut-edge) 50% calc(50% + 1px), #0000 0);
    background-position: top left, top right, bottom left, bottom right;
    background-size: var(--cj-chamfer-xs) var(--cj-chamfer-xs);
    background-repeat: no-repeat;
    background-origin: border-box;
    background-clip: border-box;
}

/* An optional glyph inside a chip. The word is always present, so the glyph is
   decorative and the state is never carried by colour or shape alone. */
.status-badge-icon {
    flex: 0 0 11px;
    width: 11px;
    height: 11px;
    background-color: currentColor;
    -webkit-mask: var(--cj-glyph, none) center / contain no-repeat;
    mask: var(--cj-glyph, none) center / contain no-repeat;
}

/* The palette, by meaning rather than by word.
 *
 * The three tokens the application already emits — active/complete, warning/
 * draft, inactive, danger, info — keep their exact rules. The rest are the
 * vocabulary the later passes need (ok, low, critical, planned, in progress,
 * completed, overdue) mapped onto the same four meanings, so that a page which
 * starts saying OVERDUE does not also start inventing a colour for it. Every one
 * of them is grouped with the rule it shares, which is why there are four rules
 * here and not eleven. */
.status-active,
.status-complete,
.status-ok,
.status-completed { border-color: var(--cj-success-border); --cj-cut-edge: var(--cj-success-border); background-color: var(--cj-success-bg); color: var(--cj-success); }
.status-warning,
.status-draft,
.status-low,
.status-progress { border-color: var(--cj-warning-border); --cj-cut-edge: var(--cj-warning-border); background-color: var(--cj-warning-bg); color: var(--cj-warning); }
.status-inactive,
.status-neutral { border-color: var(--cj-border-control); --cj-cut-edge: var(--cj-border-control); background-color: var(--cj-neutral-bg); color: var(--cj-neutral); }
.status-danger,
.status-critical,
.status-overdue { border-color: var(--cj-danger-border); --cj-cut-edge: var(--cj-danger-border); background-color: var(--cj-danger-bg); color: var(--cj-danger); }
.status-info,
.status-planned { border-color: var(--cj-info-border); --cj-cut-edge: var(--cj-info-border); background-color: var(--cj-info-bg); color: var(--cj-text); }

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

.table-responsive { overflow-x: auto; }
/* The scroll cue (spec §7.3 item 1, D-6): a gold edge on whichever side has
   more table to scroll to. The two covers scroll with the content and the two
   edges stay put, so an edge shows only once content passes under it, and a
   table that fits shows nothing. The covers are the panel's own face, inherited
   from the panel the table sits in, so at rest they match it exactly, and all
   four stop short of the panel's cut corners, which a cover would otherwise
   paint over. No `overflow` here: the declaration above is the scroll
   container. */
.table-responsive {
    background:
        linear-gradient(to right, var(--cj-panel-surface, transparent) 20px, transparent 48px) left center / 48px calc(100% - 2 * var(--cj-panel-cut, 0px)) no-repeat local,
        linear-gradient(to left, var(--cj-panel-surface, transparent) 20px, transparent 48px) right center / 48px calc(100% - 2 * var(--cj-panel-cut, 0px)) no-repeat local,
        linear-gradient(to right, color-mix(in srgb, var(--cj-gold-dim) 60%, transparent), transparent) left center / 14px calc(100% - 2 * var(--cj-panel-cut, 0px)) no-repeat scroll,
        linear-gradient(to left, color-mix(in srgb, var(--cj-gold-dim) 60%, transparent), transparent) right center / 14px calc(100% - 2 * var(--cj-panel-cut, 0px)) no-repeat scroll;
}

/* Dense and scannable. The header row is the only part of a table that takes
   the display face — the body stays in the reading sans, which the HUD skill
   requires of a table body. */
.data-table { width: 100%; border-collapse: collapse; font-size: .89rem; }
/* The header row is a typographic band, not a filled one. The fill it used to
   carry is what stopped a table sitting inside a panel: at the top of a flush
   table it painted a solid strip across the panel's own 1px edge and over both
   of its cut corners, which is exactly the "floating SaaS element in a card"
   look the panel pass exists to remove. Caps, tracking and a gold hairline
   separate the header from the rows without a block of colour. */
.data-table th {
    padding: 12px 14px;
    border-bottom: 1px solid var(--cj-gold-dim);
    background: transparent;
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: var(--cj-text-xs);
    font-weight: 600;
    letter-spacing: .16em;
    text-align: left;
    text-transform: uppercase;
    white-space: nowrap;
}
.data-table td { padding: 14px; border-bottom: 1px solid var(--cj-border-subtle); vertical-align: middle; }

/* A sortable header's link fills its cell. As bare inline text it was a 14px
   tall tap target, well under anything usable on a phone. */
.data-table th a {
    display: block;
    padding: 12px 14px;
    margin: -12px -14px;
    color: inherit;
    font: inherit;
    text-transform: inherit;
    letter-spacing: inherit;
}
.data-table th a:hover { color: var(--cj-text); text-decoration: none; }
/* A sort that must save the page's input first (Add Products, Events EVF-15)
   is a submit button of the page's form, drawn exactly as the header link it
   stands in for. */
.data-table th .th-sort {
    display: block;
    width: calc(100% + 28px);
    min-height: 0;
    margin: -12px -14px;
    padding: 12px 14px;
    border: 0;
    border-radius: 0;
    background: none;
    color: inherit;
    font: inherit;
    letter-spacing: inherit;
    text-align: inherit;
    text-transform: inherit;
}
.data-table th .th-sort:hover { background: none; color: var(--cj-text); }
.data-table th .th-sort:active { box-shadow: none; transform: none; }

/* A link inside a body cell gets a usable hit area, and the layout does not
   notice.
   A record's name in a table cell was a 16px-tall tap target — the height of its
   own text — against the 32px floor every other control in the application
   meets, which three browser verifications recorded as a finding. The header rule
   above solves the same problem with padding and a cancelling negative margin,
   and that technique does NOT transfer to a body cell: an inline-block's margin
   box still participates in line layout, and the first attempt measured rows at
   69px against 44px. Table density is not something to spend on a tap target.
   So the hit area is a pseudo-element. Pseudo-elements are out of flow, so row
   height, column widths and density are identical to before, and the overlay
   reaches 14px above and below the text — 44px in all — without reaching sideways
   into the next column. Buttons are excluded: they clear the floor already. */
.data-table td a:not(.button) { position: relative; }
.data-table td a:not(.button)::after {
    content: '';
    position: absolute;
    inset: -14px 0;
}
.data-table tbody tr:hover { background: #6B7A3A1a; }
.data-table td small { display: block; color: var(--cj-muted); font-size: .78rem; }

/* A figure column. Right-aligned and tabular so digits line up down the column
   and a total sits under its addends. Gold, because on this HUD a number is
   money or a count and both are gold. */
.data-table th.numeric,
.data-table td.numeric,
.numeric { text-align: right; font-variant-numeric: tabular-nums; }
.data-table td.numeric { color: var(--cj-accent-dark); }

.table-total-label { text-align: right; }

/* An actions cell keeps its controls on one row and stays vertically centred.
   It was `display:flex`, which both broke `vertical-align` and let five
   controls wrap into four lines, making every row of the Materials list about
   200px tall. */
.data-table td.actions { white-space: nowrap; }

/* An identifier: a SKU, code or reference (Marketing Product Screenshots
   PCP-C, F-7). It never breaks inside itself, so "WAL-COAST-4" is never read as
   "WAL-COAST-" over "4"; a hyphen in a code is part of the code, not a place to
   wrap. Only the code is held together. A name beside it keeps wrapping, and the
   column it sits in is free to take its width from the rest of the table. */
.cell-code { white-space: nowrap; }

/* A long descriptive header that may wrap (PCP-C). Headers stay on one line by
   default; on Sales by Product seven of them, "Cost of what you sold" alone
   191px, made the table wider than a 1280px window's content and it scrolled.
   A `th-wrap` header breaks between its words, and only when the row is short of
   room, as `.growth-acquisition-table`'s headers do. Every header in that row
   then sits on the hairline, so a wrapped one and a one-line one share a foot.
   Codes in the body stay on one line (`.cell-code`). */
.data-table th.th-wrap { white-space: normal; }
.data-table tr:has(> th.th-wrap) > th { vertical-align: bottom; }
.data-table td.actions form { display: inline; margin: 0; }
.row-actions { display: inline-flex; align-items: center; gap: var(--cj-space-1); }
.row-actions form { display: inline; margin: 0; }

/* Phone presentation hooks (mobile plan §5.3). A table opts in with
   `data-table-stack` (rows become blocks) or `data-table-pin` (the identity
   column holds still while the rest scrolls); both take effect only in the
   640px block. Above it the one thing they do is keep a phone-only cell out
   of the desktop table. */
.data-table [data-stack="phone"] { display: none; }

/* A toolbar above a table is that panel's header band, so it is the same
   treatment as every other panel header: transparent, compact, hairline under
   it. Filled, it painted over the panel edge and the cut corners the same way
   the table header row did. */
.table-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--cj-space-3);
    padding: var(--cj-panel-head-pad-y) var(--cj-panel-pad-x);
    border-bottom: 1px solid var(--cj-panel-edge);
    background: transparent;
}
.toolbar-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }

/* The shared Actions menu, `x-overflow-menu` (2026-10-01 spec §13): every
 * row and page "⋯" menu in Cajella.
 *
 * The panel is a `popover`, so it lives in the browser's top layer: that is how
 * it gets out of `.table-responsive` (a clip box, because `overflow-x: auto`
 * forces `overflow-y` to `auto` too) and out of the `.card` stacking context —
 * without either container changing, because table scrolling depends on them.
 * Inside the top layer `position: absolute` and `z-index` mean nothing, so the
 * panel is `fixed` and `overflow-menu.js` gives it its coordinates.
 *
 * The UA's popover defaults (centred with `inset: 0; margin: auto`, a border,
 * padding, Canvas colours, `overflow: auto`) are reset so the panel keeps the
 * shared panel geometry it already takes from the list at the top of this file.
 * The `display: none` is restated because `display: flex` below outranks the
 * UA rule that hides a closed popover.
 *
 * The inline `<details>`/`<summary>` menu this replaced, and its absolutely
 * positioned panel, are gone (implementation plan Slice 9). */
.row-overflow { display: inline-block; }
.row-overflow-menu {
    --cj-panel-cut: var(--cj-chamfer-sm);
    --cj-panel-edge-color: var(--cj-panel-edge-emphasis);
    --cj-panel-surface: var(--cj-bg);
    display: flex;
    flex-direction: column;
    align-items: stretch;
    min-width: 170px;
    padding: 6px;
    box-shadow: var(--cj-shadow-lg);
}
.row-overflow-menu form { margin: 0; }
.row-overflow-menu .row-action { display: flex; align-items: center; width: 100%; min-height: 40px; text-align: left; }
/* A page action that is in the header on a wider screen is repeated in the
   menu for a phone (mobile plan §7); above 640px that copy is not shown. */
.row-overflow-menu .row-action-phone { display: none; }
/* A list row whose only menu item is the phone Edit (x-row-actions) has no
   menu above 640px, where Edit is visible in the row. */
.row-overflow.row-overflow-phone { display: none; }
/* The trigger is part of the button family: the shared control radius, no cut. */
.row-overflow-trigger {
    padding: 4px 9px;
    border: 1px solid var(--cj-border-control);
    border-radius: var(--cj-radius-control);
    background: var(--cj-hover);
    color: var(--cj-muted);
    cursor: pointer;
    font: inherit;
    font-weight: 800;
    line-height: inherit;
    letter-spacing: normal;
    text-transform: none;
    display: inline-flex;
    align-items: center;
    min-height: 34px;
}
/* The trigger has never had a hover state; this keeps it that way rather
   than letting the shared `button:hover` accent fill it gold. */
.row-overflow-trigger:hover { border-color: var(--cj-border-control); background: var(--cj-hover); color: var(--cj-muted); }
.row-overflow-trigger[aria-expanded="true"] { border-color: var(--cj-border-hover); background: var(--cj-bg); color: var(--cj-accent-dark); }
.row-overflow-menu[popover] {
    position: fixed;
    inset: auto;
    width: max-content;
    max-width: calc(100vw - 16px);
    margin: 0;
    color: inherit;
    overflow: visible;
}
.row-overflow-menu[popover]:not(:popover-open) { display: none; }
.row-overflow-menu[data-placing] { visibility: hidden; }

.color-swatch {
    display: inline-block;
    width: 12px;
    height: 12px;
    margin-right: 6px;
    border: 1px solid var(--cj-border-control);
    border-radius: var(--cj-radius-xs);
    vertical-align: middle;
}

/* ---------------------------------------------------------- pagination ---- */

/* The one paginator, rendered by resources/views/pagination/cajella.blade.php
   for every ->links() call in the application.

   It replaces Laravel's default Tailwind view, whose utilities this stylesheet
   does not define: `sm:hidden` and `hidden` are what switch that view's two
   responsive halves, so with no rule behind them both halves rendered at once
   and the numbered links stacked with no flex container — 2,272px of pagination
   at 1440 and 826px at 390. Previous and Next reuse .button .button-small
   .button-secondary rather than restating a control, so they inherit the hover,
   focus and disabled treatment the rest of the application already has. */

.pagination {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--cj-space-3);
    padding: var(--cj-space-4) 0 0;
}

.pagination-summary { margin: 0; color: var(--cj-muted); font-size: var(--cj-text-sm); }

.pagination-list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--cj-space-1);
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Previous and Next keep a little air between themselves and the numbers. */
.pagination-step { margin: 0 var(--cj-space-2); }

/* A disabled step is a span, not an anchor, so it is not tabbable and cannot be
   followed. This stops the shared .button:hover accent from firing on it — the
   control has to look inert as well as be inert — without touching the button
   system itself. */
.pagination .button[aria-disabled=true],
.pagination .button[aria-disabled=true]:hover {
    border-color: var(--cj-border);
    background: transparent;
    color: var(--cj-muted);
}

.pagination-page-item { display: inline-flex; }

.pagination-page {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 34px;
    min-height: 34px;
    padding: 0 var(--cj-space-2);
    border: 1px solid transparent;
    border-radius: var(--cj-radius-control);
    color: var(--cj-text);
    font-size: var(--cj-text-sm);
    font-weight: 600;
    text-decoration: none;
}
a.pagination-page:hover { border-color: var(--cj-border-hover); background: var(--cj-hover); color: var(--cj-accent-dark); text-decoration: none; }
a.pagination-page:active { box-shadow: inset 0 0 0 2px var(--cj-accent-dark); }

/* The current page is told apart three ways, so colour is never the only one:
   it is heavier, it is a filled bordered box rather than plain text, and it is
   not a link — there is nothing to hover and nothing to follow. */
.pagination-page.is-current {
    border-color: var(--cj-accent-hover);
    background: var(--cj-accent);
    color: var(--cj-on-accent);
    font-weight: 750;
    cursor: default;
}

.pagination-gap {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    min-height: 34px;
    color: var(--cj-muted);
}

/* The position in words stands in for the window on a phone, so it is absent
   wherever the window is present. `is-always` is the simple paginator, which has
   no window at any width. */
.pagination-position {
    display: none;
    color: var(--cj-muted);
    font-size: var(--cj-text-sm);
}
.pagination-position.is-always { display: inline-flex; margin: 0 var(--cj-space-2); }

/* The paginator's submit mode (Add Products, Events EVF-15): a page number
   that saves the page's input first is a button drawn as the link it stands
   in for. */
button.pagination-page {
    min-height: 34px;
    border-color: transparent;
    background: transparent;
    color: var(--cj-text);
    font-family: inherit;
    font-size: var(--cj-text-sm);
    letter-spacing: normal;
    text-transform: none;
}
button.pagination-page:hover { border-color: var(--cj-border-hover); background: var(--cj-hover); color: var(--cj-accent-dark); }

/* --------------------------------------------------------- empty state ---- */

.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    min-height: 150px;
    padding: var(--cj-space-5);
    color: var(--cj-muted);
    text-align: center;
}
.empty-state strong { color: var(--cj-text); font-size: var(--cj-text-lg); }
.empty-state span { max-width: 52ch; }
.empty-state-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--cj-space-2); }
.empty-state-actions form { margin: 0; }

/* An unknown value. Distinct from a figure that is genuinely zero — see
   `x-money` and Decision Log: "Unset money is not zero". */
.value-unset { color: var(--cj-muted); }

/* ----------------------------------------------------- cards and detail ---- */

/* A carbon panel: the shape, the edge and the face all come from the panel
   geometry above, so all that is left here is what a card is *for* — its
   padding.
 *
 * The gold top hairline every card used to wear is gone. It was applied to every
 * panel on every page, which made it decoration rather than emphasis; emphasis
 * is `.panel-emphasis` now, and a row of panels is separated by the grid gap and
 * by their own cut corners rather than by a stripe. The drop shadow is gone for
 * the same reason the skill rejects glassmorphism, and because a rectangular
 * shadow under a chamfered panel draws the corners the cut just removed. */
.card,
.section-card {
    padding: var(--cj-panel-pad-y) var(--cj-panel-pad-x);
    box-shadow: none;
}

/* A region nested inside a panel is the inset surface, not a second panel and
   not a borderless void. It used to be the latter: `border: 0` left a nested
   card with no edge at all, so a form section inside a card had no boundary and
   its heading rule was the only thing separating it from the section above. */
.card .card,
.card .form-section,
.card .detail-section {
    --cj-panel-cut: var(--cj-chamfer-sm);
    --cj-panel-edge-color: var(--cj-panel-edge-inset);
    --cj-panel-surface: var(--cj-panel-inset-face);
    box-shadow: none;
}

/* A table fills its panel.
 *
 * The header row, the rows and the panel edge then line up, which is what makes
 * the table read as the panel's content rather than as a floating element that
 * happens to be inside one. It is done with negative margins against the panel's
 * own padding tokens, so it needs no markup change: both `x-table-shell` and
 * `x-data-table` emit `.table-responsive` as the card's direct child.
 *
 * The 1px the margin holds back is the panel's edge. A table that reached
 * `inset: 0` would paint its header row and its hover tint over the very line
 * that draws the panel, and over the cut corners with it — which is also why the
 * header row below is a typographic band with a hairline rather than a filled
 * one. */
.card > .table-responsive {
    margin: calc((var(--cj-panel-pad-y) - 1px) * -1) calc((var(--cj-panel-pad-x) - 1px) * -1);
}
/* The toolbar above a flush table is that table's header band, so it carries the
   panel-header padding rather than the toolbar's own. */
.card > .table-responsive > .table-toolbar {
    padding: var(--cj-panel-head-pad-y) var(--cj-panel-pad-x);
}

.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--cj-space-4); }

.detail-section {
    padding: var(--cj-panel-pad-y) var(--cj-panel-pad-x);
    box-shadow: none;
}

/* The heading, its supporting line and the section's actions. The subtitle used
   to sit in the same flex row as the heading, so supporting copy floated to the
   far right of a wide card and read as unrelated text. It belongs under the
   heading it supports; only the actions stay on the right. */
/* This is a panel header, so it is treated as one: the band reaches both edges
   of the panel by cancelling the panel's horizontal padding, it sits tight to
   the top edge, and a hairline joins it to the body below. Before this it was a
   heading inside the padding with a short rule under it — the SaaS card header
   the panel pass is replacing — and at `--cj-text-xl` it was also the second
   largest thing on a detail page after the page's own command header. */
.detail-section-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--cj-space-4);
    margin:
        calc(var(--cj-panel-pad-y) * -1)
        calc(var(--cj-panel-pad-x) * -1)
        var(--cj-panel-pad-y);
    padding: var(--cj-panel-head-pad-y) var(--cj-panel-pad-x);
    border-bottom: 1px solid var(--cj-panel-edge);
}
.detail-section-heading { min-width: 0; }
.detail-section-header h2 {
    margin: 0;
    font-size: .8rem;
    letter-spacing: var(--cj-track-label);
    line-height: 1.6;
}
.detail-section-subtitle { margin: var(--cj-space-1) 0 0; color: var(--cj-muted); font-size: .88rem; }
.detail-section-actions { display: flex; flex-wrap: wrap; gap: var(--cj-space-2); }

.detail-section-body { min-width: 0; }

.detail-rows { display: grid; grid-template-columns: auto 1fr; gap: var(--cj-space-3) 20px; margin: 0; }
.detail-row { display: contents; }
/* `display: contents` outranks the browser's own `[hidden]` rule. */
.detail-row[hidden] { display: none; }
.detail-row dt { color: var(--cj-muted); font-size: .88rem; }
/* A detail value is a value, not a reason for the page to scroll sideways.
   `.detail-rows` is an `auto 1fr` grid and `.detail-row` is `display: contents`,
   so the value is a direct grid item — and a grid item's default `min-width` is
   `auto`, which refuses to shrink below its content. One long unbreakable string
   (a supplier's purchase URL is the usual one) therefore widened the grid past
   its card and took the whole page with it: Task 3B browser verification
   measured 58px of horizontal page overflow at 390px and 27px at 1024px on the
   Component detail page. `min-width: 0` lets the column shrink, and
   `overflow-wrap` gives the string somewhere to break. */
.detail-row dd { margin: 0; min-width: 0; overflow-wrap: anywhere; font-weight: 650; }
/* EVF-10: the Event plan totals band keeps each money figure whole on a phone. */
.plan-totals .detail-row dd { overflow-wrap: normal; }

/* The metric grid, shared by the tiles and by the figure groups on the detail
   pages. Same floor and same gap as `.hud-widgets`, because a row of figures is
   a row of figures wherever it appears. */
.metric-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--cj-grid-gap); }

/* The figure is the punch. Gold, tabular, and in the display face so a column
   of them lines up on the same stems. */
.metric-value {
    display: block;
    color: var(--cj-accent-dark);
    font-family: var(--cj-font-display);
    font-size: var(--cj-text-metric-sm);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    letter-spacing: .03em;
    line-height: 1.15;
}

/* The identity band above a product's tabs. A panel like any other; the olive
   left rule it used to carry is superseded by the shape — a 3px stripe on one
   side of an otherwise 1px edge was the old way of making a panel distinctive,
   and the cut corners do that now for every panel at once. */
.product-identity {
    display: flex;
    flex-wrap: wrap;
    gap: var(--cj-space-3) var(--cj-space-6);
    margin-bottom: var(--cj-space-4);
    padding: var(--cj-panel-head-pad-y) var(--cj-panel-pad-x);
    box-shadow: none;
}
.product-identity-item { display: flex; flex-direction: column; gap: var(--cj-space-1); }
.product-identity-item small {
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: .68rem;
    font-weight: 600;
    letter-spacing: .18em;
    text-transform: uppercase;
}
.product-identity-item strong { color: var(--cj-text); font-size: 1.02rem; }

/* ------------------------------------------------- HUD: count widgets ---- */

/* The three patterns the HUD skill names as components: a count widget that
   navigates, a money figure, and an attention rail. They are shared, not
   dashboard-only — the interior pages use the same vocabulary above their one
   primary table, which is what stops the application growing a second visual
   system for the same job.

   `auto-fit` with a 168px floor gives six tiles across a 1440px page, three at
   tablet and one on a phone, with no breakpoint of their own. */
.hud-widgets {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
    gap: var(--cj-grid-gap);
    margin-bottom: var(--cj-space-5);
}

/* A tile is a link. The whole tile, not a link inside it: the skill says a
   widget opens its destination, and a 168x104 target is easier to hit than a
   caption.
 *
 * Everything about how it *looks* is now the shared metric treatment in the
   panels section — the shape, the edge, the face, the grid, the icon, the label
   and the figure. What is left here is what a tile does that a plain metric card
   does not: it is a fixed minimum height so a row of them is even whatever
   wording each carries, and it responds to a pointer.
 *
 * `::after` was the rotated-square corner tick. It is the panel face now, and
   the tick is superseded rather than moved: the chamfered corner *is* the corner
   treatment, and a reticule mark over the top of it was two marks for one job.
   `overflow: hidden`, which existed to contain that tick, went with it — a
   panel that hides its overflow also hides the row-action menus inside it. */
.hud-widget {
    min-height: 100px;
    font-weight: 400;
    transition: color .15s;
}
.hud-widget:hover {
    --cj-panel-edge-color: var(--cj-panel-edge-emphasis);
    --cj-panel-surface: var(--cj-panel-inset-face);
    color: var(--cj-text);
    text-decoration: none;
}
.hud-widget:hover .hud-widget-value { color: var(--cj-warning); }
.hud-widget:hover > .icon-framed { color: var(--cj-accent-dark); border-color: var(--cj-gold-dim); }
.hud-widget:active {
    --cj-panel-edge-color: var(--cj-accent-dark);
    --cj-panel-surface: var(--cj-sidebar-hover);
}

/* The number is the visual punch the skill asks for, and on a tile it is the
   largest text in the application after a page's own command header. */
.hud-widget-value {
    grid-column: 1 / -1;
    color: var(--cj-accent-dark);
    font-family: var(--cj-font-display);
    font-size: var(--cj-text-metric);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    letter-spacing: .02em;
    line-height: 1;
    transition: color .15s;
}
.hud-widget-note { grid-column: 1 / -1; min-width: 0; color: var(--cj-muted); font-size: .74rem; overflow-wrap: anywhere; }

/* Severity on a tile, through the shared metric states: the edge, the framed
   icon and the figure move, and the face never does.
 *
 * `-ok` is deliberately quiet. It used to draw an olive left rule, which meant
   that on an orderly shop every tile on the dashboard was decorated — and a
   screen where the ordinary state is decorated has spent its emphasis on
   nothing. Gold and red appear only when the count behind them is non-zero,
   which is the rule that keeps them worth noticing. */
.hud-widget-ok { --cj-panel-edge-color: var(--cj-panel-edge); }
.hud-widget-warn { --cj-panel-edge-color: var(--cj-panel-edge-warning); }
.hud-widget-warn .hud-widget-value { color: var(--cj-warning); }
.hud-widget-warn > .icon-framed { color: var(--cj-warning); border-color: var(--cj-warning-border); }
.hud-widget-critical { --cj-panel-edge-color: var(--cj-panel-edge-critical); }
.hud-widget-critical .hud-widget-value { color: var(--cj-danger); }
.hud-widget-critical > .icon-framed { color: var(--cj-danger); border-color: var(--cj-danger-border); }
.hud-widget-critical:hover .hud-widget-value { color: var(--cj-danger); }
.hud-widget-critical:hover > .icon-framed { color: var(--cj-danger); border-color: var(--cj-danger-border); }

/* The tile for the filter in force (UI clarity UC-7, spec R-8). The view sets
   `aria-current`, and this rule keys off it, so the announced state and the
   drawn one cannot disagree.
 *
 * The bar along the foot is the cue that decides it: the product tabs' bar,
   in the same olive. It is drawn on the face layer, inside the edge, so the
   tile keeps its chamfer and gains no element. The edge turns selected only
   where no severity edge is drawn: a warn or critical tile keeps its gold or
   red, because a current Low tile is still low.
 *
 * The face is the shared selected tint laid over the panel face. The tint is
   translucent and the edge layer fills the whole tile beneath the face, so on
   its own it would let the edge colour flood through.
 *
 * The face is drawn here rather than through `--cj-panel-surface`, which hover
   and press set, and the edge rule outranks theirs: pointing at a current tile
   does not take its state away. Hover itself never draws the bar (R-2). */
.hud-widget[aria-current]:not(.hud-widget-warn, .hud-widget-critical) { --cj-panel-edge-color: var(--cj-panel-edge-selected); }
.hud-widget[aria-current]::after {
    background:
        linear-gradient(var(--cj-panel-edge-selected), var(--cj-panel-edge-selected)) center bottom / 100% 3px no-repeat,
        linear-gradient(var(--cj-panel-selected-face), var(--cj-panel-selected-face)),
        var(--cj-panel-face);
}

/* A readout rather than a link. Machines, Packaging Materials and Write-offs
   have no query-string filters, so a tile on those pages has nowhere to send
   the owner that they are not already looking at. It states the figure and
   stops there — the alternative is a widget that links to the page it is on,
   which teaches that tiles do nothing. It is a `<div>`, not an `<a>`, so it is
   also not in the tab order. */
.hud-widget-static { cursor: default; }
.hud-widget-static:hover { border-color: var(--cj-border); background: var(--cj-surface); }
.hud-widget-static:hover .hud-widget-value { color: var(--cj-accent-dark); }

/* EVF-13: the Event financial overview's group headings ("Actual so far",
   "Still to sell — current plan", "Starting plan vs actual"). Each group of
   static tiles is headed with the view it belongs to, in the tile label's own
   voice, so no figure is read as another view's (specification §12.1). */
.overview-group-heading {
    margin: 1.25rem 0 .5rem;
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: var(--cj-text-micro);
    font-weight: 600;
    letter-spacing: var(--cj-track-label);
    text-transform: uppercase;
}
.event-overview > section:first-child > .overview-group-heading { margin-top: 0; }
.event-overview > .overview-detail { margin-top: 1rem; }

/* Balanced tile rows (Marketing Product Screenshots PCP-B, F-5). `auto-fit`
   fills as many columns as fit whatever the number of tiles, so six tiles came
   out 5 + 1 or 4 + 2 and four came out 3 + 1: a tile alone on a row reads as one
   that fell off the row above. Here the column count is always a divisor of the
   tile count (6 → 6/3/2/1, 4 → 4/2/1, 3 → 3/1, 2 → 2/1), and the largest one
   whose columns keep the shared floor (168px; 200px in the brief) is chosen from
   the row's own width.
 *
 * The row cannot query itself, so it is the container and each tile picks its
   span of a 12-column grid: span 2 is six across, 3 is four, 4 is three, 6 is
   two, 12 is one. A span includes the gaps it crosses, so a tile comes out the
   width a grid of that many columns would give it. Each threshold is
   n × floor + (n − 1) × the 12px gap. Other counts (five on Growth, eleven on an
   event) keep `auto-fit`, as do browsers without container queries or `:has()`.
   A phone keeps its own two-up rule in the 640px block. */
@supports (container-type: inline-size) and selector(:has(*)) {
    @media (min-width: 641px) {
        .hud-widgets:is(:has(> :nth-child(2):last-child), :has(> :nth-child(3):last-child),
                        :has(> :nth-child(4):last-child), :has(> :nth-child(6):last-child)) {
            container: hud-widgets / inline-size;
            grid-template-columns: repeat(12, minmax(0, 1fr));
        }
        .brief-main .hud-widgets:is(:has(> :nth-child(2):last-child), :has(> :nth-child(3):last-child),
                                    :has(> :nth-child(4):last-child), :has(> :nth-child(6):last-child)) {
            container-name: hud-widgets-brief;
        }
        .hud-widgets > * { grid-column: span var(--cj-hud-span, 12); }

        @container hud-widgets (min-width: 348px) {
            .hud-widgets:not(:has(> :nth-child(3):last-child)) > * { --cj-hud-span: 6; }
        }
        @container hud-widgets (min-width: 528px) {
            .hud-widgets:is(:has(> :nth-child(3):last-child), :has(> :nth-child(6):last-child)) > * { --cj-hud-span: 4; }
        }
        @container hud-widgets (min-width: 708px) {
            .hud-widgets:has(> :nth-child(4):last-child) > * { --cj-hud-span: 3; }
        }
        @container hud-widgets (min-width: 1068px) {
            .hud-widgets:has(> :nth-child(6):last-child) > * { --cj-hud-span: 2; }
        }

        @container hud-widgets-brief (min-width: 412px) {
            .hud-widgets:not(:has(> :nth-child(3):last-child)) > * { --cj-hud-span: 6; }
        }
        @container hud-widgets-brief (min-width: 624px) {
            .hud-widgets:is(:has(> :nth-child(3):last-child), :has(> :nth-child(6):last-child)) > * { --cj-hud-span: 4; }
        }
        @container hud-widgets-brief (min-width: 836px) {
            .hud-widgets:has(> :nth-child(4):last-child) > * { --cj-hud-span: 3; }
        }
        @container hud-widgets-brief (min-width: 1260px) {
            .hud-widgets:has(> :nth-child(6):last-child) > * { --cj-hud-span: 2; }
        }
    }
}

/* One line for every figure in a row (PCP-B, F-6). Each tile was its own grid,
   so a label that wrapped to two lines pushed its figure below its neighbours'.
   A tile now takes three rows of the row grid (label, figure, note) and lays its
   own content on them through `subgrid`, so the tallest label in a row sets the
   label track for all of them and every figure starts on the same line. The
   tile's own row gap is restored inside the subgrid. Figures and notes sit at
   the top of their tracks, so a shorter one lines up with its neighbours' first
   line instead of floating in the middle. A label with no icon beside it sits on
   its figure, as a short label sits on its control in `.field-pair`, so a
   one-line label keeps its usual distance from the figure next to a wrapped one;
   a label beside an icon stays centred on the icon. A tile with more than one
   note (Growth's traffic tile) keeps its own grid and spans the same three rows.
   The phone list pages' compact count row hides its notes, so its tiles span two
   rows there (in the 640px block). Without `subgrid`, tiles keep their independent grid. */
@supports (grid-template-rows: subgrid) {
    .hud-widgets > .hud-widget { grid-row: span 3; }
    .hud-widgets > .hud-widget:not(:has(> .hud-widget-note ~ .hud-widget-note)) {
        grid-template-rows: subgrid;
        row-gap: var(--cj-space-1);
    }
    .hud-widgets > .hud-widget:not(:has(> .icon-framed)) > .hud-widget-label { align-self: end; }
    .hud-widgets > .hud-widget > .hud-widget-value,
    .hud-widgets > .hud-widget > .hud-widget-note { align-self: start; }
}

/* ---------------------------------------------------------- HUD: money ---- */

/* The few figures a shop owner checks first, on one band rather than in cards
   of their own. Gold, tabular, with dividers between them. */
.money-strip {
    display: flex;
    flex-wrap: wrap;
    gap: var(--cj-space-4) var(--cj-space-6);
    margin-bottom: var(--cj-space-5);
    padding: var(--cj-space-4) var(--cj-panel-pad-x);
}
.money-figure {
    display: flex;
    flex: 1 1 180px;
    flex-direction: column;
    gap: var(--cj-space-1);
    padding-left: var(--cj-space-4);
    border-left: 1px solid var(--cj-panel-edge-inset);
}
.money-figure:first-child { padding-left: 0; border-left: 0; }
.money-label {
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: .66rem;
    font-weight: 600;
    letter-spacing: .2em;
    text-transform: uppercase;
}
.money-value {
    color: var(--cj-accent-dark);
    font-family: var(--cj-font-display);
    font-size: 1.55rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
}
/* The total of the figures beside it, marked as the one to read first. */
.money-figure-total .money-value { font-size: 1.85rem; }
.money-figure-total .money-label { color: var(--cj-accent-dark); }

/* One row or a stack, never a partial wrap (PCP-B, F-4). Wrapping let "Stock on
   hand" drop onto a line of its own at common laptop widths, still carrying its
   left divider and indent. The band is now its own container: while three
   figures fit at their 180px floor with the 32px gaps between them (604px of
   content), they share one row; below that, every figure takes the full width and
   the band becomes the stacked form the phone block already uses, dividers moved
   to top edges. The width of the band decides, not the width of the window.
   The row gap is the stack's 12px; a single row never shows it. Without
   container queries the band keeps its plain wrapping rule. */
@supports (container-type: inline-size) {
    .money-strip { container: money-strip / inline-size; row-gap: var(--cj-space-3); }

    @container money-strip (max-width: 603.98px) {
        .money-figure { flex: 0 0 100%; padding-left: 0; padding-top: var(--cj-space-3); border-left: 0; border-top: 1px solid var(--cj-panel-edge-inset); }
        .money-figure:first-child { padding-top: 0; border-top: 0; }
    }
}

/* ------------------------------------------------ HUD: attention rail ---- */

/* Severity, one line of why, and a jump. Scarce by construction: a row exists
   only when its count is non-zero, so an empty rail is a shop with nothing
   waiting rather than a panel of zeroes. */
/* The rail is a panel whose header is a header band: it holds no padding of its
   own, and the heading and the list carry theirs, so the hairline under the
   heading runs edge to edge and the rows reach both edges too. That is the
   difference between a panel with a title in it and a panel with a header. */
.attention-rail { padding: 0; }
.attention-heading {
    display: flex;
    align-items: center;
    gap: var(--cj-icon-gap);
    min-height: 36px;
    margin: 0;
    padding: var(--cj-panel-head-pad-y) var(--cj-panel-pad-x);
    border-bottom: 1px solid var(--cj-panel-edge);
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: var(--cj-track-label);
    text-transform: uppercase;
}
/* The header's glyph, drawn rather than written: the heading text already says
   what the panel is, so the mark is decorative and a pseudo-element keeps it out
   of the accessibility tree without anyone having to remember `aria-hidden`.
   It also means the two pages that render a rail — the brief and the Materials
   list — gain it without either template changing. */
.attention-heading::before {
    content: '';
    flex: 0 0 14px;
    width: 14px;
    height: 14px;
    background-color: var(--cj-gold-dim);
    -webkit-mask: var(--cj-icon-triangle-alert) center / contain no-repeat;
    mask: var(--cj-icon-triangle-alert) center / contain no-repeat;
}
/* 1px of side padding so a row's severity rule sits *inside* the panel's edge
   rather than painting over it. */
.attention-list { margin: 0; padding: var(--cj-space-1) 1px; list-style: none; }

/* An attention row: severity, what happened, optionally the value or the
   context, and a jump.
 *
 * A row supports more than the two shapes rendered today — `.attention-severity`
 * for the chip, `.attention-detail` and `.attention-context` for a second line,
 * `.attention-value` for a figure or a time — because the later Materials and
 * Production passes need those and a second row primitive for the same job is
 * how an application ends up with two. The two shapes in use (count + why + jump
 * on the brief, why + jump on the Materials list) are unchanged.
 *
 * Severity is a left rule *and* a glyph *and* the colour of the leading figure.
 * Never colour alone: every row states its condition in words. */
.attention-item {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--cj-space-2) var(--cj-space-3);
    min-height: var(--cj-row-height);
    padding: var(--cj-space-2) var(--cj-panel-pad-x);
    border-left: 2px solid var(--cj-border-control);
    color: var(--cj-text);
    font-weight: 400;
    text-decoration: none;
}
/* The severity glyph. Neutral by default; each severity names its own below. */
.attention-item::before {
    content: '';
    flex: 0 0 15px;
    width: 15px;
    height: 15px;
    background-color: var(--cj-muted);
    -webkit-mask: var(--cj-icon-info) center / contain no-repeat;
    mask: var(--cj-icon-info) center / contain no-repeat;
}
.attention-item + .attention-item { border-top: 1px solid var(--cj-border-subtle); }
.attention-item:hover { background: var(--cj-hover); color: var(--cj-text); text-decoration: none; }

/* The count leads the row, because it is what decides whether the row is worth
   reading. */
.attention-count {
    flex: 0 0 auto;
    min-width: 2.2ch;
    color: var(--cj-accent-dark);
    font-family: var(--cj-font-display);
    font-size: 1.15rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    text-align: right;
}
/* The severity chip, for the rows that lead with a word rather than a figure. */
.attention-severity { flex: 0 0 auto; }
/* Message over context, as one block, so a two-line row still has one left
   edge and the value and the jump stay on the row's own baseline. */
.attention-detail { display: flex; flex: 1 1 12rem; flex-direction: column; gap: 2px; min-width: 0; }
.attention-why { flex: 1 1 auto; min-width: 0; font-size: .88rem; }
.attention-detail .attention-why { flex: 0 0 auto; }
.attention-context { color: var(--cj-muted); font-size: .78rem; overflow-wrap: anywhere; }
.attention-value {
    flex: 0 0 auto;
    color: var(--cj-text);
    font-family: var(--cj-font-display);
    font-size: .84rem;
    font-variant-numeric: tabular-nums;
    letter-spacing: .04em;
}
.attention-jump {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    flex: 0 0 auto;
    /* Whatever the row wraps, the jump stays on the right of the last line, so
       it never reads as a third piece of body text. A row with a count keeps
       its message beside the count instead (below). */
    margin-left: auto;
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: .66rem;
    letter-spacing: .16em;
    text-transform: uppercase;
}
/* The chevron the reference rows end on. Drawn, decorative, and it follows the
   colour of the label it belongs to. */
.attention-jump::after {
    content: '';
    width: 12px;
    height: 12px;
    background-color: currentColor;
    -webkit-mask: var(--cj-icon-chevron-right) center / contain no-repeat;
    mask: var(--cj-icon-chevron-right) center / contain no-repeat;
}
.attention-item:hover .attention-jump { color: var(--cj-accent-dark); }

.attention-item-critical { border-left-color: var(--cj-danger); }
.attention-item-critical::before { background-color: var(--cj-danger); -webkit-mask-image: var(--cj-icon-circle-alert); mask-image: var(--cj-icon-circle-alert); }
.attention-item-critical .attention-count { color: var(--cj-danger); }
.attention-item-warning { border-left-color: var(--cj-warning-border); }
.attention-item-warning::before { background-color: var(--cj-warning); -webkit-mask-image: var(--cj-icon-triangle-alert); mask-image: var(--cj-icon-triangle-alert); }
.attention-item-warning .attention-count { color: var(--cj-warning); }
.attention-item-info { border-left-color: var(--cj-accent); }
.attention-item-info::before { background-color: var(--cj-accent-hover); }

/* A row with a count (Marketing Product Screenshots LD-22, spec §18.4.4): glyph,
   count and message share the first line, and a long message wraps inside its
   own column instead of dropping under the count. The message is text in the
   row's own flow; the glyph and the count float into the row's left padding with
   a net width of zero, so every line of the message, wrapped or not, starts at
   one left edge, and every row in a rail starts it at the same one. The jump
   floats right: after the message's last line when it fits there, otherwise on
   one more line, still inside the message column. Rows without a count (the
   list rails) keep the flex row above. */
.attention-item:has(> .attention-count) {
    --attention-line: calc(.88rem * 1.5);
    --attention-count-w: 1.9rem;
    --attention-lead: calc(15px + var(--cj-space-3) + var(--attention-count-w) + var(--cj-space-3));
    display: flow-root;
    padding-block: max(var(--cj-space-2), calc((var(--attention-row-min, var(--cj-row-height)) - var(--attention-line)) / 2));
    padding-left: calc(var(--cj-panel-pad-x) + var(--attention-lead));
    font-size: .88rem;
    line-height: var(--attention-line);
}
.attention-item:has(> .attention-count)::before {
    float: left;
    margin: calc((var(--attention-line) - 15px) / 2) var(--cj-space-3) 0 calc(-1 * var(--attention-lead));
}
/* A float's outer edge may not sit left of the row's content edge, so each
   float pulls its own box back into the padding with a negative margin. */
.attention-item > .attention-count {
    float: left;
    min-width: var(--attention-count-w);
    margin: 0 var(--cj-space-3) 0 calc(-1 * (var(--attention-count-w) + var(--cj-space-3)));
    line-height: var(--attention-line);
}
.attention-item:has(> .attention-count) > .attention-jump {
    float: right;
    height: var(--attention-line);
    margin-left: var(--cj-space-3);
}
/* Just above the brief's stack point the rail is under 220px wide, and the
   widest jump ("Components") would not fit in the message column. There the
   rows take the phone's panel padding and 8px gaps, and the heading keeps in
   line with them. Rails without a count are left as they are. */
.attention-rail:has(.attention-count) { container: attention-rail / inline-size; }
@container attention-rail (width < 220px) {
    .attention-heading,
    .attention-item:has(> .attention-count) { --cj-panel-pad-x: 14px; }
    .attention-item:has(> .attention-count) { --attention-lead: calc(15px + var(--cj-space-2) + var(--attention-count-w) + var(--cj-space-2)); }
    .attention-item:has(> .attention-count)::before { margin-right: var(--cj-space-2); }
    .attention-item > .attention-count { margin: 0 var(--cj-space-2) 0 calc(-1 * (var(--attention-count-w) + var(--cj-space-2))); }
}

.attention-empty {
    display: flex;
    align-items: center;
    gap: var(--cj-space-2);
    margin: 0;
    padding: var(--cj-space-3) var(--cj-panel-pad-x);
    color: var(--cj-muted);
    font-size: .88rem;
}
.attention-empty::before {
    content: '';
    width: 7px;
    height: 7px;
    border-radius: var(--cj-radius-pill);
    background: var(--cj-success);
}

/* ------------------------------------------------ HUD: command brief ---- */

/* The dashboard's two columns: the brief on the left, the rail on the right.
   A single grid rather than nested flex, so the rail's height is the row's
   height and the two columns end level. */
.brief-grid { display: grid; grid-template-columns: minmax(0, 2.1fr) minmax(0, 1fr); gap: var(--cj-space-5); align-items: start; }
.brief-main { min-width: 0; }
.brief-rail { min-width: 0; }

/* ------------------------------------------- admin growth dashboard ---- */

/* /admin/growth (CAJ-MEASURE-M1 §10). The brief layout, the static tiles and
   the attention rail, plus the few things only this page needs: the line
   saying when tracking started, section labels between the two tile rows, the
   traffic notice that stands in for two tiles, and a history panel that must
   never read as live. Tokens only; no new colours. */
.growth-tracking {
    display: flex;
    align-items: center;
    gap: var(--cj-space-2);
    margin: 0 0 var(--cj-space-3);
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    letter-spacing: .06em;
    text-transform: uppercase;
}
.growth-tracking strong { color: var(--cj-accent-dark); font-weight: 600; }
.growth-tracking-time { font-size: .8rem; }
.growth-section-title {
    margin: 0 0 var(--cj-space-2);
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: .82rem;
    font-weight: 600;
    letter-spacing: .1em;
    text-transform: uppercase;
}
.growth-traffic { grid-column: span 2; }
.growth-traffic-notice { color: var(--cj-text); font-size: .86rem; }
.growth-since { font-style: italic; }
.growth-warning { color: var(--cj-warning); }
.growth-dimension-tabs { margin-bottom: 0; border-bottom: 0; }
.growth-table-note { padding: var(--cj-space-2) var(--cj-space-3); }
.growth-history { margin-top: var(--cj-space-5); }
.growth-history .data-table td { color: var(--cj-muted); }
.growth-acquisition-table th { white-space: normal; vertical-align: bottom; }
.growth-acquisition-table th, .growth-acquisition-table td { padding-left: var(--cj-space-2); padding-right: var(--cj-space-2); }
.growth-acquisition-table td:first-child { min-width: 7rem; overflow-wrap: break-word; }
.growth-definitions {
    margin: 0;
    padding: var(--cj-space-3) var(--cj-panel-pad-x);
    color: var(--cj-muted);
    font-size: .74rem;
    letter-spacing: .06em;
    text-transform: uppercase;
}
/* Its phone rules are in the one 640px block below. */

/* The brief holds six tiles beside a rail, so it needs a wider floor than the
   shared 168px: at that floor the column fitted four across and the last two
   wrapped onto a short second row, which read as two tiles that had fallen off
   the first. A 200px floor gives three and three at desktop and two and two and
   two once the rail takes its share, and it does that arithmetically rather than
   with a breakpoint per width. */
.brief-main .hud-widgets { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }

/* ---------------------------------------------------------------- tabs ---- */

/* Approved 2026-09: Product Detail moved to the wide page treatment
   specifically because seven tabs (an eighth on a Product with versions'
   own sub-navigation would not fit) were producing a horizontal scrollbar at
   ordinary desktop widths — the row was wider than the 1240px shell had room
   for. Width alone does not finish the job: rendering the real seven labels
   (measured, not guessed — "Bill of Materials" is the long one) against the
   256px rail at 1024px, one of this fix's own required verification widths,
   still overflowed by ~50px after a first, smaller trim (space-4 to
   space-3, .12em to .1em). `.detail-tab`'s padding is now `--cj-space-1`
   and its letter-spacing `.06em` — the same tracking `.user-control-name`
   already uses at this exact .86rem, so the value is reused from an
   existing label at the same size, not invented for this rule. Verified:
   the row now fits with no scrollbar at 1440/1024/768px; at 390px it still
   scrolls, which is the explicitly allowed mobile behaviour below. */
.detail-tabs {
    display: flex;
    gap: var(--cj-space-1);
    margin-bottom: var(--cj-space-5);
    padding: 0 2px;
    border-bottom: 1px solid var(--cj-border);
    overflow-x: auto;
    scrollbar-width: thin;
}
.detail-tab {
    flex: 0 0 auto;
    margin-bottom: -1px;
    padding: 10px var(--cj-space-1);
    border: 1px solid transparent;
    border-bottom: 2px solid transparent;
    border-radius: var(--cj-radius-sm) var(--cj-radius-sm) 0 0;
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: .86rem;
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    white-space: nowrap;
}
.detail-tab:hover { background: var(--cj-hover); color: var(--cj-text); text-decoration: none; }
.detail-tab-current {
    border-color: var(--cj-border);
    border-bottom-color: var(--cj-accent-hover);
    background: var(--cj-surface);
    color: var(--cj-accent-dark);
}
.detail-tab-current:hover { background: var(--cj-surface); color: var(--cj-accent-dark); }
/* The Product section picker and the copy of a product message beneath it are
   the phone's (spec §11, N-9); above 640px the tab bar and the layout's own
   message are unchanged (N-8). */
.section-picker,
.product-section-flash { display: none; }
/* A Product whose current tab is in the second half of the bar starts the bar
   at its end (MA-15 correction): where the seven tabs are a few pixels wider
   than the bar (about 641–652px and 901–920px), a scroller laid out in reverse
   opens at its end, so the current tab is in view at first layout with no
   script and no movement. `order` puts the tabs back in reading order, so the
   bar looks and tabs exactly as it does elsewhere, and the auto margin keeps
   them at the left wherever they fit. The shared `.detail-tabs` rules are not
   touched. */
.product-sections-from-end > .detail-tabs { flex-direction: row-reverse; }
.product-sections-from-end > .detail-tabs > .detail-tab:nth-child(2) { order: -1; }
.product-sections-from-end > .detail-tabs > .detail-tab:nth-child(3) { order: -2; }
.product-sections-from-end > .detail-tabs > .detail-tab:nth-child(4) { order: -3; }
.product-sections-from-end > .detail-tabs > .detail-tab:nth-child(5) { order: -4; }
.product-sections-from-end > .detail-tabs > .detail-tab:nth-child(6) { order: -5; }
.product-sections-from-end > .detail-tabs > .detail-tab:nth-child(7) { order: -6; }
.product-sections-from-end > .detail-tabs > .detail-tab:last-child { margin-right: auto; }

/* --------------------------------------------------------------- modal ---- */

/* A dialog opened with showModal() lives in the top layer, so the shade behind
   it is ::backdrop, not an element. A tall modal keeps its header and footer
   and scrolls its body, otherwise the submit button sits outside the viewport. */
/* A modal is a panel that genuinely floats, so it is the one panel that keeps a
   shadow — and it takes the emphasis edge, because it is the only thing on the
   screen the owner is being asked to deal with. */
.modal-dialog {
    --cj-panel-edge-color: var(--cj-panel-edge-emphasis);
    width: 90%;
    max-width: 580px;
    color: var(--cj-text);
    box-shadow: var(--cj-shadow-modal);
}
/* The phone filter sheet (`.list-sheet`, a popover) takes the same shade. */
dialog.modal-dialog::backdrop,
.list-sheet::backdrop { background: #05060499; }
dialog.modal-dialog[open] {
    display: flex;
    flex-direction: column;
    max-height: min(90vh, 100% - 2rem);
    max-height: min(90dvh, 100% - 2rem);
    padding: 0;
    overflow: hidden;
}
dialog.modal-dialog[open] .modal-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* The page behind an open dialog does not scroll (spec M-3). The native modal
   already makes it inert; this stops a wheel or a swipe moving it. */
html:has(dialog.modal-dialog[open]) { overflow: hidden; }
dialog.modal-dialog[open] .modal-header,
dialog.modal-dialog[open] .modal-footer { flex: 0 0 auto; }

/* The same header band as every other panel: compact, tracked caps, hairline. */
.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--cj-space-4);
    padding: var(--cj-panel-head-pad-y) var(--cj-panel-pad-x);
    border-bottom: 1px solid var(--cj-panel-edge);
}
.modal-header h2 { margin: 0; font-size: .84rem; letter-spacing: var(--cj-track-label); line-height: 1.6; }
/* The title takes focus only from script, when a tap opens the dialog (spec
   F-9): it is where a screen reader starts, not a control, and is never a Tab
   stop. WebKit rings it after showModal() as if it were one. */
.modal-header h2:focus-visible { outline: none; }
.modal-body { padding: var(--cj-panel-pad-y) var(--cj-panel-pad-x); }
/* The body's section rhythm (Marketing Product Screenshots PCP-D, F-8). The body
   is padding only, so the space between two sections came from the first one's
   bottom margin, and a detail list, a chip row, an alert or a help line brings
   none of its own (or loses it to the rule that sets its margin to 0). Set Price
   puts a cost list straight before a field, and a chip row straight before a
   detail list, and both junctions touched. A section that follows one of those
   now starts 12px down. Margins collapse, so where the section before already
   brings 12px or more (a field's 16px, a paragraph's 1em, an alert's 20px) the
   gap is what it was. The body's sections are its children and, for its one
   form, the form's children; a form that ends in one of those sections spaces
   whatever follows the form. */
.modal-body > :is(.detail-rows, .toolbar-chips, .alert, .form-help) + *,
.modal-body > form > :is(.detail-rows, .toolbar-chips, .alert, .form-help) + *,
.modal-body > form:has(> :is(.detail-rows, .toolbar-chips, .alert, .form-help):last-child) + * { margin-top: var(--cj-space-3); }
.modal-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 10px;
    padding: var(--cj-space-3) var(--cj-panel-pad-x);
    border-top: 1px solid var(--cj-panel-edge);
    background: var(--cj-panel-inset-face);
}
.modal-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 40px;
    min-height: 40px;
    padding: 2px 10px;
    border: 0;
    border-radius: var(--cj-radius-sm);
    background: transparent;
    color: var(--cj-muted);
    font-size: 1.4rem;
    line-height: 1;
}
.modal-close:hover { background: var(--cj-neutral-bg); color: var(--cj-text); }

/* -------------------------------------------------------------- wizard ---- */

/* Product setup. These rules were a <style> block inside x-wizard-layout with a
   hardcoded palette — the step markers were blue and green, which are not
   Cajella colours, making the wizard's most prominent element the one thing on
   screen that did not belong to the product. */
.wizard-container { max-width: 1240px; margin: 0 auto; }
.wizard-header { margin-bottom: var(--cj-space-6); }
.wizard-header h1 {
    margin: 0 0 var(--cj-space-5);
    color: var(--cj-text);
    font-family: var(--cj-font-display);
    font-size: clamp(1.6rem, 2.2vw, var(--cj-text-2xl));
    font-weight: 600;
    letter-spacing: .1em;
    line-height: 1.15;
    text-transform: uppercase;
}

.wizard-progress { display: flex; position: relative; gap: var(--cj-space-2); padding-bottom: var(--cj-space-5); }
.wizard-progress::before {
    content: '';
    position: absolute;
    z-index: 0;
    top: 19px;
    right: 0;
    left: 0;
    height: 2px;
    background: var(--cj-border);
}
.wizard-step { flex: 1; position: relative; z-index: 1; text-align: center; }
/* A squared step marker rather than a circle: the HUD is angular, and a
   36px square reads as a stage on a progress rail as clearly as a disc does. */
.wizard-step .step-number {
    display: inline-block;
    width: 38px;
    height: 38px;
    line-height: 36px;
    border: 1px solid var(--cj-border-control);
    border-radius: var(--cj-radius-pill);
    background: var(--cj-surface);
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-weight: 600;
    margin-bottom: var(--cj-space-2);
}
.wizard-step .step-name {
    display: block;
    color: var(--cj-muted);
    font-family: var(--cj-font-display);
    font-size: var(--cj-text-sm);
    letter-spacing: .1em;
    text-transform: uppercase;
}
.wizard-step.complete .step-number {
    border-color: var(--cj-success-border);
    background: var(--cj-success-bg);
    color: var(--cj-success);
}
.wizard-step.current .step-number {
    border-color: var(--cj-accent-hover);
    background: var(--cj-accent);
    color: var(--cj-on-accent);
}
.wizard-step.current .step-name { color: var(--cj-accent-dark); font-weight: 700; }
.wizard-step-link { display: block; padding: 2px; color: inherit; text-decoration: none; cursor: pointer; }
.wizard-step-link:hover { text-decoration: none; }
.wizard-step-link:hover .step-number { border-color: var(--cj-border-hover); background: var(--cj-hover); }
/* The phone's one-line progress (spec W-1); the rail and the frame heading
   say the same above 640px. */
.wizard-phone-progress { display: none; }

.wizard-content {
    padding: var(--cj-panel-pad-y) var(--cj-panel-pad-x);
    box-shadow: none;
}
.wizard-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-top: var(--cj-space-5);
    padding-top: var(--cj-space-5);
    border-top: 1px solid var(--cj-border-subtle);
}
/* Step 1 of an existing Product: Exit wizard, with the line beside it saying
   that nothing saved is lost (spec W-6, I-6). */
.wizard-exit { display: flex; flex-wrap: wrap; align-items: center; gap: 4px var(--cj-space-3); }
.wizard-exit-note { color: var(--cj-muted); font-size: var(--cj-text-sm); }
/* A variant inside the wizard's own panel: the inset surface, so it reads as a
   region of the step rather than as a panel floating on one. */
.wizard-variant-card {
    --cj-panel-cut: var(--cj-chamfer-sm);
    --cj-panel-edge-color: var(--cj-panel-edge-inset);
    --cj-panel-surface: var(--cj-panel-inset-face);
    margin-bottom: var(--cj-space-4);
    padding: var(--cj-space-4);
}
.wizard-variant-header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--cj-space-3);
    margin-bottom: var(--cj-space-4);
}
.wizard-variant-header h3 { margin: 0; font-size: var(--cj-text-lg); }
/* An in-step action's message, inside the group it changed (MA-19, W-2). */
.wizard-feedback .alert { margin-bottom: var(--cj-space-4); }
.wizard-variant-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--cj-space-2); }
.wizard-variant-actions form { margin: 0; }
.wizard-intro { margin: 0 0 var(--cj-space-4); color: var(--cj-muted); }
/* The step's own title is the step panel's header band. */
.wizard-content > h2:first-child {
    margin:
        calc(var(--cj-panel-pad-y) * -1)
        calc(var(--cj-panel-pad-x) * -1)
        var(--cj-panel-pad-y);
    padding: var(--cj-panel-head-pad-y) var(--cj-panel-pad-x);
    border-bottom: 1px solid var(--cj-panel-edge);
    font-size: .8rem;
    letter-spacing: var(--cj-track-label);
    line-height: 1.6;
}
.form-control-small { width: auto; padding: 6px 10px; font-size: var(--cj-text-sm); }
.wizard-variant-header small { display: block; margin-top: 2px; color: var(--cj-muted); font-weight: 400; }
/* A version card's line count and cost, and the toggle collapsible-lines.js
   adds, are phone-only (spec W-4, plan F-S): above 640px the card's own table
   shows every line and its total, and every card stays open. */
.wizard-version-summary,
.collapsible-toggle { display: none; }
/* A line editor's summary cell (collapsible-lines.js, MA-20) is phone-only
   too: above 640px the table shows every field of every line, as before. */
.line-editor td.line-summary { display: none; }
/* A field with its button beside it (wizard step 2, Product Options). These
   were inline-styled flex rows that could not wrap, so on a phone the button
   squeezed the field; now the button drops below it when the two do not fit.
   `.option-row` is the tighter row under an option's heading. */
.wizard-inline-row,
.option-row { display: flex; flex-wrap: wrap; align-items: end; gap: 1rem; }
.option-row { gap: .5rem; }
.wizard-inline-row > div,
.option-row > div { flex: 1 1 12rem; min-width: 0; }
/* Product Options' add-an-option form: an inset band above the options it adds
   to. These were inline styles; as classes the page follows the shared form
   rules (spec PD-5), and the values are the ones the inline styles had. */
.option-add-form {
    margin-bottom: var(--cj-space-6);
    padding: var(--cj-space-4);
    border-radius: 4px;
    background: var(--cj-hover);
}
.option-add-label { display: block; margin-bottom: var(--cj-space-1); font-weight: 500; }

/* Wizard step 2 (mobile plan MA-17, spec W-3). These were inline styles; the
   values are the ones they had, so the page above 640px looks as it did. */
.wizard-card-title { margin-bottom: var(--cj-space-4); }
.wizard-card-intro { margin-bottom: var(--cj-space-5); color: var(--cj-muted); }
.wizard-note {
    margin-bottom: var(--cj-space-6);
    padding: var(--cj-space-6);
    border-left: 4px solid var(--cj-info);
    border-radius: 8px;
    background: var(--cj-hover);
}
.wizard-note p,
.wizard-default-variant { color: var(--cj-muted); }
.wizard-options {
    margin-bottom: var(--cj-space-6);
    padding: var(--cj-space-5);
    border-radius: 8px;
    background: var(--cj-hover);
}
.wizard-option-header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--cj-space-2);
    margin-bottom: var(--cj-space-4);
}
.wizard-option-header h4 { margin: 0; }
.wizard-option-header form { margin: 0; }
/* An option's values wrap inline, each with the control that removes it. */
.wizard-option-values { display: flex; flex-wrap: wrap; gap: var(--cj-space-2); margin-bottom: var(--cj-space-4); }
.wizard-value-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--cj-space-2);
    padding: var(--cj-space-2) var(--cj-space-4);
    border-radius: 4px;
    background: var(--cj-border);
}
.wizard-value-chip form { display: inline; margin: 0; }
.wizard-options .wizard-inline-row .form-control { margin: 0; }
.wizard-option-new {
    padding: var(--cj-space-5);
    border: 1px dashed var(--cj-border-control);
    border-radius: 8px;
    background: var(--cj-surface);
}
.wizard-warning {
    margin-bottom: var(--cj-space-6);
    padding: var(--cj-space-4);
    border: 1px solid var(--cj-warning-border);
    border-radius: 8px;
    background: var(--cj-warning-bg);
}
.wizard-warning p { margin: 0; color: var(--cj-warning); }
.wizard-existing {
    padding: var(--cj-space-5);
    border: 1px solid var(--cj-border);
    border-radius: 8px;
    background: var(--cj-surface);
}
/* Generated combinations (spec W-3, Q-4b): Generate, the "X of Y selected"
   count and Select all / Select none sit together above the list, with the
   at-least-one error under them, so a rejected Generate lands with the action,
   the count and the error in view. The fieldset groups the boxes for assistive
   technology only; it draws no frame of its own (P-7). */
.combination-group { min-width: 0; margin: 0; padding: 0; border: 0; background: none; }
.combination-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--cj-space-2) var(--cj-space-3);
    margin-bottom: var(--cj-space-3);
}
.combination-count { margin: 0; font-weight: 650; font-variant-numeric: tabular-nums; }
.combination-tools { display: inline-flex; flex-wrap: wrap; gap: var(--cj-space-2); }
.combination-group > .form-error,
.combination-none { display: block; margin: 0 0 var(--cj-space-3); }
.combination-none { color: var(--cj-muted); }
/* A column is at most the list's width, so enlarged text (200%, RB-5) makes one
   full-width column instead of one wider than the phone. */
.combination-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(14rem, 100%), 1fr));
    gap: var(--cj-space-2);
}
.combination-list .form-choice { min-width: 0; margin: 0; }
.combination-exists { margin-left: var(--cj-space-2); color: var(--cj-success); font-size: .875rem; }

/* ---------------------------------------------------------------- auth ---- */

.auth-page { min-height: 100vh; min-height: 100dvh; padding: 8vh 20px; background: var(--cj-bg); }
.auth-shell { max-width: 460px; margin: 0 auto; }
/* The same grid as `.sidebar-brand`, so the mark, the wordmark and the
   sub-label stack the same way on both surfaces — one system, not two. Centred
   as a block rather than by text alignment, because a grid's columns are sized
   to their content. */
.auth-brand {
    display: grid;
    grid-template-columns: auto auto;
    align-items: center;
    justify-content: center;
    column-gap: 10px;
    width: fit-content;
    margin: 0 auto 20px;
    color: var(--cj-text);
    font-family: var(--cj-font-display);
    font-size: 1.25rem;
    font-weight: 700;
    letter-spacing: .2em;
    text-transform: uppercase;
}
/* The same mark the rail wears, so the sign-in screen belongs to the same
   application rather than being a plain form on a dark page. */
/* Approved 2026-09: same correction as `.sidebar-brand::before` — the
   canonical mark via `--cj-brand-mark`, in place of the rotated-square
   placeholder, using the same mask mechanism every glyph in the shell uses. */
.auth-brand::before {
    content: '';
    display: block;
    width: 14px;
    height: 14px;
    background-color: currentColor;
    -webkit-mask: var(--cj-brand-mark) center / contain no-repeat;
    mask: var(--cj-brand-mark) center / contain no-repeat;
}
.auth-brand:hover { color: var(--cj-accent-dark); text-decoration: none; }
.auth-brand small { grid-column: 2; display: block; color: var(--cj-muted); font-size: .6rem; font-weight: 500; letter-spacing: .28em; }
.auth-card { padding: 36px; }
.auth-card h1 {
    margin: 0 0 var(--cj-space-2);
    color: var(--cj-text);
    font-family: var(--cj-font-display);
    font-size: var(--cj-text-2xl);
    font-weight: 600;
    letter-spacing: .08em;
    line-height: 1.15;
    text-transform: uppercase;
}
.auth-card .intro { margin: 0 0 22px; color: var(--cj-muted); }
.auth-card .form-actions { justify-content: stretch; border-top: 0; }
.auth-card .form-actions .button { width: 100%; }
.auth-footnote { margin-top: 20px; color: var(--cj-muted); text-align: center; }
/* Comfortable to tap, not a 19px line of text. */
.auth-footnote a,
.auth-card .intro a { display: inline-block; padding: 10px 4px; }
.auth-tagline { color: var(--cj-muted); text-align: center; }

/* Present for assistive technology, absent visually — an actions column needs
   a header that a screen reader can announce but that would only add noise to
   the visible header row. */
/* Deliberately NOT position:absolute. An absolutely positioned descendant is
   laid out against its nearest positioned ancestor, not against the scroll
   container it sits in — so inside a horizontally scrolling table it escaped
   the container entirely and pushed the page 243px wide on its own. Kept in
   flow at 1px instead, which costs nothing and cannot escape. */
.visually-hidden {
    display: inline-block;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

[hidden] { display: none !important; }

/* ------------------------------------------------------- reduced motion ---- */

/* Everything that moves or fades in Cajella does so to acknowledge a pointer or
   to slide the navigation drawer, and none of it carries information. Someone
   who has asked their system for less motion gets the end state immediately.

   `!important` because the durations it overrides are declared on the controls
   themselves, and this is a user preference rather than another opinion about
   the design. Only the durations are touched: the drawer's `visibility` delays
   still sequence correctly, so the navigation opens and closes exactly as it
   does otherwise — instantly. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        transition-duration: .01ms !important;
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        scroll-behavior: auto !important;
    }

    button[data-pending]::after { width: 100%; animation: none !important; }
}

/* ---------------------------------------------------------- responsive ---- */

/* The rail is only worth its width when there is room left for the content
   beside it. At tablet portrait there is not: a 216px rail against a 768px
   viewport left ~552px of page, so the drawer now starts at 900px rather than
   640px and the in-between band no longer gets a desktop layout it cannot
   afford. */
@media (max-width: 900px) {
    /* The first thing in the sticky header, in its flow. It used to be fixed at
       the viewport's top-left from outside the shell, which painted it over the
       open drawer beside the drawer's own close button and over the page as it
       scrolled beneath (mobile plan MX-1). */
    .sidebar-toggle-button {
        display: inline-flex;
        flex: 0 0 auto;
    }


    /* `visibility: hidden` and not merely `left: -288px`.
       An off-canvas drawer positioned outside the viewport is still *visible* to
       the browser, so its twelve navigation links stay in the tab order: a
       keyboard user at this width tabbed through twelve off-screen destinations
       before reaching the page, which two consecutive browser verifications
       recorded as a finding. Visibility removes a subtree from the tab order and
       from the accessibility tree, needs no JavaScript, and therefore holds
       whether or not `nav.js` has run.

       The two `visibility` transitions are asymmetric on purpose. Opening is
       `0s` with no delay, because `nav.js` moves focus into the drawer in the
       same tick and focus is refused on a subtree that is still hidden — the
       first attempt delayed both directions and silently broke focus-on-open.
       Closing is delayed by the length of the slide, so the drawer stays visible
       while it travels out rather than blinking away. */
    .app-sidebar {
        position: fixed;
        z-index: 5;
        top: 0;
        bottom: 0;
        left: -288px;
        width: 288px;
        flex-basis: 288px;
        padding-top: 64px;
        overflow-y: auto;
        overscroll-behavior: contain;
        visibility: hidden;
        transition: left .2s ease, visibility 0s linear .2s;
    }
    .app-shell.nav-open .app-sidebar {
        left: 0;
        visibility: visible;
        transition: left .2s ease, visibility 0s linear 0s;
    }

    /* A tap outside the drawer closes it, and nothing behind it is clickable
       while it is open. */
    .app-shell.nav-open .sidebar-backdrop {
        display: block;
        position: fixed;
        z-index: 4;
        inset: 0;
        background: #050604b3;
        border: 0;
    }
    .sidebar-close {
        display: inline-flex;
        position: absolute;
        top: 14px;
        right: 14px;
    }

    /* The header stays one row, and is a sticky compact header: toggle, page
       identity, then the bell and the user control, at every scroll position
       (approved D-3). The toggle is in the row now, so the 76px indent past a
       fixed toggle is gone. It sits under the drawer (5) and its backdrop (4),
       which cover it while the drawer is open. `env()` keeps the controls out
       of a notch in landscape; elsewhere it resolves to 0 and the 12px holds. */
    .topbar {
        position: sticky;
        z-index: 3;
        top: 0;
        gap: var(--cj-space-2);
        padding: 12px 18px;
        padding-top: max(12px, env(safe-area-inset-top));
    }
    .topbar-identity { margin-right: auto; }

    /* A focused field or an in-page anchor scrolls into view below the sticky
       header rather than under it (spec T-6). The header's own controls are
       never under the header, so they cancel that padding: without this, focus
       returning to the toggle when the drawer closed, or moving into the user
       menu, "revealed" a control already on screen by scrolling the page to
       the top. */
    html { scroll-padding-top: calc(var(--cj-topbar-h) + var(--cj-space-2)); }
    .topbar * { scroll-margin-top: calc(-1 * (var(--cj-topbar-h) + var(--cj-space-2))); }

    /* The page behind the open drawer holds still; the drawer scrolls on its
       own, and `overscroll-behavior` above stops it handing the scroll on. */
    html:has(.app-shell.nav-open) { overflow: hidden; }

    /* Drawer rows and the switcher are thumb targets (spec S-2). */
    .nav-item { min-height: 44px; }
    .business-switcher select { min-height: 44px; }

    /* A long business name wraps whole instead of being cut off mid-word. */
    .business-identity strong { overflow-wrap: anywhere; }
    .business-switcher-current {
        display: block;
        margin-bottom: 6px;
        color: var(--cj-text);
        font-weight: 700;
        line-height: 1.3;
        overflow-wrap: anywhere;
    }

    /* The business is named once (spec SH-3). The closed select repeated the
       line above, cut off mid-word; its own text is hidden and the words
       "Switch business" sit over it. The native select is still the control
       underneath (MX S-2), and the words let taps through to it. They are
       `aria-hidden`: the select keeps its name, "Current business", and
       announces the business as its value, so a screen reader hears it once. */
    .business-switcher { position: relative; }
    .business-switcher select { color: transparent; appearance: none; }
    .business-switcher select option { color: var(--cj-text); }
    .business-switcher .business-switcher-action {
        display: flex;
        position: absolute;
        right: 2px;
        bottom: 0;
        left: 2px;
        align-items: center;
        min-height: 44px;
        margin: 0;
        padding: 0 9px;
        color: var(--cj-text);
        font-family: inherit;
        font-size: inherit;
        font-weight: 600;
        letter-spacing: normal;
        text-transform: none;
        pointer-events: none;
    }
    /* The browser draws a select's own arrow in its text colour, so hiding the
       text hid the arrow too; the shell's chevron takes its place. */
    .business-switcher .business-switcher-action::after {
        content: '';
        flex: 0 0 18px;
        width: 18px;
        height: 18px;
        margin-left: auto;
        background-color: var(--cj-muted);
        -webkit-mask: var(--cj-icon-chevron-down) center / 18px 18px no-repeat;
        mask: var(--cj-icon-chevron-down) center / 18px 18px no-repeat;
    }
    .page-content { padding: 22px 18px 56px; }
    .detail-grid { grid-template-columns: 1fr; }
    .wizard-progress { gap: var(--cj-space-1); }

    /* The command brief loses its second column before its tiles lose their
       row: the attention rail is content, not chrome, so it stacks under the
       brief rather than being squeezed to an unreadable width. */
    .brief-grid { grid-template-columns: 1fr; }

    /* A header control is a thumb target here, not a pointer target. */
    .header-control { min-height: 44px; }
    .header-control-icon-only { min-width: 44px; }
    .topbar-actions { padding-left: var(--cj-space-2); }
    .user-control { max-width: 11rem; }
    .header-menu-item { min-height: 44px; padding-block: 12px; }

    /* Every table-row and detail-row action is a thumb target here too — the
       audit's own finding: `.button-small` (28px) and `.row-action` (34px)
       were the one place in Cajella still under the 44px floor every other
       control gets on a phone (`.header-control`, `.pagination-step` above).
       Desktop keeps the compact sizing on purpose — a dense table is exactly
       where that density earns its keep with a pointer — so this is scoped to
       the same breakpoint as the rest of the thumb-target correction rather
       than raised globally. */
    .button-small,
    .row-action { min-height: 44px; }

    /* The shared touch floor (spec T-1). Every button variant reads the control
       height, so raising the token here takes all of them to 44px; desktop keeps
       34px. The rest are the controls that declare a smaller size of their own.
       The searchable-select clear stays 24px to look at, inside the field's
       padding, and its tap area is widened instead. */
    :root { --cj-control-height: 44px; }
    .form-control,
    .form-choice,
    .row-overflow-menu .row-action { min-height: 44px; }

    /* A text field under 16px makes iOS Safari zoom the page on focus (spec
       F-1, F-8). `1rem` is 16px because the root size is never changed. Inside a
       table a field inherited the table's .89rem, and the small wizard field is
       smaller again; this sets the fields only, so table text is unchanged. It
       holds through 900px, so no tier with the phone shell zooms on focus (a
       tablet in portrait, a phone in landscape). */
    .form-control,
    .searchable-select-input,
    .form-control-small { font-size: 1rem; }
    .row-overflow-trigger,
    .modal-close,
    .chip-remove,
    .pagination-page { justify-content: center; min-width: 44px; min-height: 44px; }
    .detail-tab { display: inline-flex; align-items: center; min-height: 44px; }
    .attention-item { min-height: 44px; }
    .attention-item:has(> .attention-count) { --attention-row-min: 44px; }
    .wizard-step-link { min-width: 44px; }
    .searchable-select-option { display: flex; align-items: center; min-height: 44px; }
    .searchable-select-clear::before { content: ''; position: absolute; inset: -10px; }

    /* Neighbouring row actions keep 8px between their tap areas (spec T-2). */
    .row-actions { gap: var(--cj-space-2); }

    /* One header-control family (spec SH-1, SH-3). The drawer toggle and the
       drawer close used to be the only framed boxes in the shell, and the toggle
       was a ☰ character set in the global button type (display face, tracking,
       gold inset press). Both are now Lucide glyphs on the `.header-control`
       treatment the bell and the user control already have: borderless at rest,
       44px square, muted until hovered, and `font: inherit` so the button type
       never reaches them. */
    .sidebar-toggle-button,
    .sidebar-close {
        align-items: center;
        justify-content: center;
        min-width: 44px;
        min-height: 44px;
        padding: 6px;
        border: 1px solid transparent;
        border-radius: var(--cj-radius-sm);
        background: transparent;
        color: var(--cj-muted);
        font: inherit;
        line-height: 1;
    }
    .sidebar-toggle-button:hover,
    .sidebar-close:hover {
        border-color: var(--cj-border-control);
        background: var(--cj-sidebar-hover);
        color: var(--cj-text);
    }
    .sidebar-toggle-button .icon,
    .sidebar-close .icon { width: 18px; height: 18px; }

    /* Pressed is the family's own state, not the global button's gold inset and
       1px drop, and it ends when the press ends (SH-2, TI-3). It is declared
       here and not by narrowing `button:active` where it is declared, because
       above 900px the bell and the user control keep the shared press. */
    .sidebar-toggle-button:active,
    .sidebar-close:active,
    .header-control:active {
        box-shadow: none;
        transform: none;
        border-color: var(--cj-border-control);
        background: var(--cj-sidebar-hover);
        color: var(--cj-text);
    }

    /* A touch screen keeps `:hover` on the last control tapped, so the toggle
       stayed lit after the drawer closed. Where there is no hover, a hovered
       control looks like a resting one; an open menu keeps its open state. */
    @media (hover: none) {
        .sidebar-toggle-button:hover:not(:active),
        .sidebar-close:hover:not(:active),
        .header-control:hover:not(:active, [aria-expanded='true']) {
            border-color: transparent;
            background: transparent;
            color: var(--cj-muted);
        }
    }

    /* Back to parent (SH-7): a 44px target on a touch tier (TI-1), pulled up
       into the content's top padding so the taller target adds ~4px. */
    .back-link { min-height: 44px; margin: calc(-1 * var(--cj-space-3)) 0 var(--cj-space-1); }

    /* A dialog keeps its title, the focused field and its footer above the
       keyboard in this tier too: a phone in landscape, a tablet (spec F-10).
       It stays the centred panel, centred in the visible area modal.js tracks
       rather than in the layout viewport the keyboard covers, and no taller
       than that area. With the keyboard closed the two are the same, so it sits
       where it did; without the script the defaults centre it as before. Fixed
       to the viewport for the reason the phone sheet is (the shared panel rule's
       `position: relative`). The 640px sheet rule overrides each of these. */
    dialog.modal-dialog[open] {
        --cj-vvh: 100dvh;
        --cj-vv-mid: 0px;
        --cj-vv-bottom: 0px;
        position: fixed;
        top: var(--cj-vv-mid, 0px);
        bottom: var(--cj-vv-bottom, 0px);
        max-height: min(90vh, 100% - 2rem);
        max-height: min(90dvh, var(--cj-vvh, 100dvh) - 12px);
    }
    /* Compact, while the visible area is too short for the body to show a
       field (modal.js sets it; spec F-10): the dialog takes the whole visible
       height, the header loses its padding (the 44px close button sets its
       height) and the footer keeps 4px around its 44px actions, so the body
       gets what is left. The title's type is unchanged. If even that leaves
       the body less than a field and the room around it, the body keeps that
       much and the dialog scrolls as a whole, so the focused field can still
       be brought fully into view. */
    dialog.modal-dialog[open][data-compact] {
        max-height: min(90dvh, var(--cj-vvh, 100dvh));
        overflow-y: auto;
        overscroll-behavior: contain;
    }
    dialog.modal-dialog[open][data-compact] .modal-body { min-height: calc(var(--cj-control-height) + var(--cj-space-6)); }
    dialog.modal-dialog[data-compact] .modal-header { padding-block: 0; }
    dialog.modal-dialog[data-compact] .modal-footer { padding-block: var(--cj-space-1); }
}

@media (max-width: 640px) {
    /* Admin growth dashboard: the traffic notice takes one column, and the
       acquisition panel's dimension tabs drop under its title. */
    .growth-traffic { grid-column: auto; }
    .growth-acquisition-panel .panel-head { flex-wrap: wrap; }
    .growth-acquisition-panel .panel-actions { flex-basis: 100%; min-width: 0; }
    .growth-dimension-tabs { max-width: 100%; }

    /* Density and geometry come down together, in one place, because they are one
       decision: a 20px gutter and a 9px cut are proportionate to a 1240px page
       and not to a 390px one. Every panel, header band, metric tile and negated
       margin in the stylesheet reads these, so a phone is a token change rather
       than a second layout. */
    :root {
        --cj-panel-pad-y: 14px;
        --cj-panel-pad-x: 14px;
        --cj-panel-head-pad-y: 8px;
        --cj-metric-pad-x: 14px;
        --cj-chamfer: 7px;
        --cj-text-metric: 1.95rem;
        /* The header band comes down with everything else. 76px of chrome is
           proportionate to a 900px window; on a 390px one it is the tallest
           thing on the screen before any content has been read. The sidebar
           brand block reads the same token, so the two stay level.

           Here it is also the band's fixed height (spec SH-9, plan F-C), so the
           sticky elements below it and `scroll-padding-top` can read one figure.
           It holds the tallest header there is: a module line and a record name
           on two 12px lines. In rem, so text zoom grows the band with its text. */
        --cj-topbar-h: 4.125rem;
    }

    /* The title keeps the band's full width here. On a record page it is two
       stacked parts rather than one row: the module on the first line and the
       record on the next, wrapping to at most two lines of its own (approved
       D-2). The record used to be hidden at this width, which left a phone
       reading ORDERS on every order. The separator is a one-row device, so it
       goes; the name itself stays whole in the DOM. */
    .topbar {
        height: var(--cj-topbar-h);
        padding: 8px 18px;
        padding-top: max(8px, env(safe-area-inset-top));
    }
    .topbar h1 {
        flex-direction: column;
        align-items: flex-start;
        gap: 2px;
        font-size: 1.05rem;
        letter-spacing: .1em;
    }
    .topbar-module { max-width: 100%; }
    .topbar-separator { display: none; }
    .topbar-detail {
        display: -webkit-box;
        max-width: 100%;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 2;
        font-size: .75rem;
        line-height: 1.15;
        overflow-wrap: anywhere;
        white-space: normal;
    }

    /* The bell has no notification system behind it, so a phone gives it no
       header space and no place in the accessibility tree (spec SH-8). Above
       640px it is unchanged. */
    .header-control-group:has(> [aria-controls='notifications-panel']) { display: none; }

    /* A direct-child link rule with 6px of vertical padding used to sit here — a
       partial, in-flow attempt at the same tap-target problem the pseudo-element
       overlay above now solves properly. It bought 12px of target and cost 12px
       of row height at the width where density matters most, and having both
       mechanisms would mean two rules for one concern. Removed with the overlay
       landing. */
    .page-header { align-items: flex-start; flex-direction: column; }
    .page-header .page-actions { width: 100%; }
    .page-header .page-actions .button { flex: 1 1 auto; }

    /* A numbered page window does not fit a phone, and squeezing it on is how
       Laravel's default paginator ended up 826px tall here. The window goes and
       the position it stands for is stated in words instead — the same trade the
       wizard progress rail makes with its step labels two rules below. Previous
       and Next stay, and take the full row between them so the tap targets are
       the width of the thumb rather than of the label. */
    .pagination { justify-content: stretch; }
    .pagination-summary { display: none; }
    .pagination-list { flex-wrap: nowrap; justify-content: space-between; width: 100%; gap: var(--cj-space-2); }
    .pagination-page-item { display: none; }
    .pagination-position { display: inline-flex; margin: 0; }
    .pagination-step { min-height: 44px; margin: 0; }

    .form-grid { grid-template-columns: 1fr; }

    /* One form layout on a phone (spec F-1, P-7). A form's sections are not a
       framed panel each: a section is its heading under a divider, on the page
       itself, so a phone shows at most two framed levels and the fields get the
       width the frame and its padding took. It covers the three layout systems:
       `.form-section` (Materials, Components, Business Settings) and the cards a
       `.form-shell` form is made of (every other full-page form). A panel inside
       one of those cards (an Order's totals) stays a panel. The Business
       Settings selectors are named because `.settings-grid > .form-section`
       out-specifies `.form-section`. */
    .form-section,
    .form-shell > .card,
    .settings-grid > .form-section {
        padding: var(--cj-space-4) 0 0;
        border-top: 1px solid var(--cj-border-subtle);
    }
    :is(.form-section, .form-shell > .card)::before,
    :is(.form-section, .form-shell > .card)::after { content: none; }
    .form-section > h2,
    .settings-grid > .form-section > h2 {
        margin: 0 0 var(--cj-space-3);
        padding: 0;
        border-bottom: 0;
        font-size: .84rem;
    }
    .form-section > h2 + .form-help,
    .settings-grid > .form-section > h2 + .form-help { margin: calc(var(--cj-space-2) * -1) 0 var(--cj-space-3); }
    .form-shell > .card > .table-responsive { margin: 0; }
    .settings-grid { gap: var(--cj-space-4); }
    .settings-grid .icon-framed { width: 28px; height: 28px; }
    .settings-grid .icon-framed::before { width: 16px; height: 16px; }
    .form-choice { display: flex; margin-right: 0; }
    .form-actions { justify-content: stretch; }
    .form-actions .button { flex: 1 1 auto; }

    /* A rejected form's alert lists each rejected field as a link to it (spec
       F-4, AC-25; form-errors.js builds the list only in this tier). Each link
       is a thumb target in the alert's own colour, underlined as a link. */
    .error-summary { margin: 0; padding: 0; list-style: none; }
    .error-summary a {
        display: flex;
        align-items: center;
        min-height: 44px;
        padding-block: 4px;
        color: inherit;
        font-weight: inherit;
        text-decoration: underline;
        text-underline-offset: 2px;
        overflow-wrap: anywhere;
    }
    .error-summary a:hover { color: inherit; }

    /* A money field shows its currency inside the field, before the figure
       (spec F-2, AC-23). The symbol is the business's; the field's padding
       makes room for its length (one to three characters, the supported set
       in `Currency`). It does not take taps: the field does. It is raised
       because the field's cut-corner clip path paints it as a layer. */
    .field-affix { position: relative; display: block; --cj-affix-chars: 1; }
    .field-affix[data-affix-chars="2"] { --cj-affix-chars: 2; }
    .field-affix[data-affix-chars="3"] { --cj-affix-chars: 3; }
    .field-affix-text {
        position: absolute;
        inset: 0 auto 0 0;
        display: flex;
        align-items: center;
        padding-left: 12px;
        color: var(--cj-muted);
        font-size: 1rem;
        pointer-events: none;
        z-index: 1;
    }
    .field-affix > .form-control { padding-left: calc(12px + var(--cj-affix-chars) * 1ch + 6px); }
    /* A quantity and its unit on one row (spec F-2): the unit takes the wider
       share because its picker holds words. Each keeps its own label, help and
       error; the subgrid lines the two controls up whatever their labels'
       length, and a short label sits on its control. */
    .field-pair {
        display: grid;
        grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
        grid-template-rows: repeat(4, auto);
        column-gap: var(--cj-space-3);
        margin-bottom: var(--cj-space-4);
    }
    .field-pair > .form-field {
        display: grid;
        grid-row: span 4;
        grid-template-rows: subgrid;
        align-content: start;
        row-gap: 0;
        margin-bottom: 0;
    }
    .field-pair > .form-field > * + * { margin-top: 6px; }
    .field-pair > .form-field > label { align-self: end; }

    .page-action-note { text-align: left; }

    /* The number of filters in use, on a list's phone Filters control. */
    .filter-count {
        min-width: 1.5rem;
        padding: 0 6px;
        border: 1px solid var(--cj-gold-dim);
        color: var(--cj-accent-dark);
        text-align: center;
    }

    /* A list with the shared toolbar (mobile application spec L-1 to L-4, plan
       MA-3). The page reads: header, one row of counts, the toolbar, then the
       records. The page header's lede and primary action give way to it: the
       toolbar carries the page's one primary action, and a disabled one keeps
       its reason under the toolbar, in the flow (L-3, MX T-3). */
    .page-content:has(.list-toolbar) > .page-header { display: none; }

    /* The counts as one compact row (L-2, plan F-G, §6): equal columns, label
       over figure, no icon. The one-line note stays in each count's accessible
       name, clipped rather than removed, so "Low" still says what low means.
       Still links, still `aria-current` for the filter in force. Only on a list
       with the toolbar; the Dashboard's tiles keep the two-up rule above. */
    .page-content:has(.list-toolbar) > .hud-widgets {
        grid-template-columns: none;
        grid-auto-columns: minmax(0, 1fr);
        grid-auto-flow: column;
        gap: var(--cj-space-2);
        margin-bottom: var(--cj-space-3);
    }
    .page-content:has(.list-toolbar) > .hud-widgets > .hud-widget {
        grid-template-columns: minmax(0, 1fr);
        gap: 2px;
        padding: 7px 8px;
    }
    .page-content:has(.list-toolbar) > .hud-widgets > .hud-widget > .icon-framed { display: none; }
    .page-content:has(.list-toolbar) > .hud-widgets .hud-widget-label {
        grid-column: 1;
        letter-spacing: .06em;
        line-height: 1.15;
    }
    .page-content:has(.list-toolbar) > .hud-widgets .hud-widget-value { font-size: 1.25rem; }
    /* The notes are hidden, so a tile takes two rows of the subgrid, not three (PCP-B). */
    @supports (grid-template-rows: subgrid) {
        .page-content:has(.list-toolbar) > .hud-widgets > .hud-widget { grid-row: span 2; }
    }
    .page-content:has(.list-toolbar) > .hud-widgets .hud-widget-note {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    /* The toolbar (L-3, plan F-C/F-D): search, Filters and the primary action
       in one row under the header, sticky at every scroll position and never
       hidden or revealed by scrolling (LS-11). The band is a fixed height, so
       `scroll-padding-top` adds it to the header's (SH-9) and a focused row
       never lands beneath it. The form steps out of the layout so the row's
       containing block is the page rather than the form, and it holds for
       the length of the list. In rem, so text zoom grows the band too. */
    :root { --cj-list-toolbar-h: 3.5rem; }
    html:has(.list-toolbar) { scroll-padding-top: calc(var(--cj-topbar-h) + var(--cj-list-toolbar-h) + var(--cj-space-2)); }
    .filter-bar:has(> .list-toolbar) { display: contents; }
    .list-toolbar {
        position: sticky;
        z-index: 2;
        top: var(--cj-topbar-h);
        display: flex;
        align-items: center;
        gap: 6px;
        height: var(--cj-list-toolbar-h);
        margin: 0 -18px var(--cj-space-3);
        padding: 0 18px;
        border-bottom: 1px solid var(--cj-border);
        background: var(--cj-sidebar);
    }
    .list-toolbar > .form-field { flex: 1 1 auto; min-width: 0; margin: 0; }
    /* The field keeps its "Search" label as its name; the placeholder is the
       visible prompt. */
    .list-toolbar > .form-field > label {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }
    /* Filters, with the filters in use and the sort in force on two micro
       lines; its accessible name says both in words. */
    .list-toolbar-filters:not([hidden]) {
        display: flex;
        flex: none;
        flex-direction: column;
        align-items: flex-start;
        justify-content: center;
        gap: 1px;
        min-width: 44px;
        min-height: 44px;
        padding: 0 6px;
        border: 1px solid var(--cj-border-control);
        background: transparent;
        color: var(--cj-text);
        font-family: var(--cj-font-display);
        font-size: var(--cj-text-micro);
        font-weight: 600;
        letter-spacing: .06em;
        line-height: 1.2;
        text-transform: uppercase;
    }
    .list-toolbar-filters-name { display: flex; align-items: center; gap: var(--cj-space-1); }
    .list-toolbar-filters .filter-count { min-width: 1.1rem; padding: 0 3px; }
    .list-toolbar-filters-sort { color: var(--cj-muted); letter-spacing: .02em; white-space: nowrap; }
    .list-toolbar-action {
        display: inline-flex;
        flex: none;
        align-items: center;
        gap: var(--cj-space-1);
        min-height: 44px;
        padding: 0 10px;
        letter-spacing: .06em;
    }
    .list-toolbar-limit {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--cj-space-2);
        margin-bottom: var(--cj-space-3);
    }
    /* Where the script has shown Filters, its count carries the summary. */
    .filter-bar:has(.list-toolbar-filters:not([hidden])) .filter-summary { display: none; }
    /* A list with no search (Machines, Channels, Events, Write-offs) has only
       its primary action in the toolbar, at the end of the row where every
       other list's is (spec §17, plan MA-7). */
    .list-toolbar:not(:has(> .form-field)) { justify-content: flex-end; }
    /* A list with search and no sheet keeps the rest of its filter bar
       (Packaging's Clear while searching) on a line under the toolbar, in the
       flow, as the plan-limit reason is. */
    .list-toolbar-trailing {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--cj-space-2);
        margin-bottom: var(--cj-space-3);
    }

    /* The filter sheet (L-4, plan F-E). Without the script it is the filters
       and the sort shown under the toolbar, with Apply and Clear. With it, it
       is a popover: a sheet from the bottom edge, above the header and the
       toolbar, scrolling within itself, its heading and Apply / Clear held at
       its two ends. Nothing about it moves or fades, so there is no motion to
       reduce (A-6). */
    .list-sheet {
        display: flex;
        flex-direction: column;
        gap: var(--cj-space-3);
        margin-bottom: var(--cj-space-4);
    }
    .list-sheet > .form-field,
    .list-sheet-sort > .form-field { margin: 0; }
    .list-sheet-sort { display: flex; flex-direction: column; gap: var(--cj-space-3); }
    .list-sheet .filter-actions .button { flex: 1 1 0; }
    .list-sheet[popover]:not(:popover-open) { display: none; }
    .list-sheet:popover-open {
        position: fixed;
        inset: auto 0 0;
        width: auto;
        max-width: none;
        height: auto;
        max-height: calc(100dvh - var(--cj-topbar-h));
        margin: 0;
        padding: 0 18px;
        overflow-y: auto;
        overscroll-behavior: contain;
        border: 0;
        border-top: 1px solid var(--cj-panel-edge-emphasis);
        background: var(--cj-sidebar);
        color: var(--cj-text);
        box-shadow: var(--cj-shadow-modal);
    }
    .list-sheet:popover-open > .list-sheet-head {
        position: sticky;
        z-index: 1;
        top: 0;
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin: 0 -18px;
        padding: var(--cj-space-1) var(--cj-space-1) var(--cj-space-1) 18px;
        border-bottom: 1px solid var(--cj-border);
        background: var(--cj-sidebar);
    }
    .list-sheet-title {
        margin: 0;
        font-family: var(--cj-font-display);
        font-size: var(--cj-text-sm);
        font-weight: 600;
        letter-spacing: .1em;
        text-transform: uppercase;
    }
    .list-sheet-close {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 44px;
        height: 44px;
        padding: 0;
        border: 0;
        background: transparent;
        color: var(--cj-muted);
    }
    .list-sheet:popover-open > .filter-actions {
        position: sticky;
        bottom: 0;
        margin: 0 -18px;
        padding: var(--cj-space-3) 18px max(var(--cj-space-3), env(safe-area-inset-bottom));
        border-top: 1px solid var(--cj-border);
        background: var(--cj-sidebar);
    }

    /* A crowded page header keeps its primary and one secondary action; the
       rest are shown in its menu instead (mobile plan §7, D-4). */
    .page-actions .page-action-desktop { display: none; }
    .row-overflow-menu .row-action.row-action-phone { display: flex; }
    .row-overflow-menu form.row-action-phone { display: block; }
    /* A list row opens its record and keeps Edit in "⋯" (spec L-6; x-row-actions). */
    .row-actions .row-action-desktop { display: none; }
    .row-overflow.row-overflow-phone { display: inline-block; }

    /* A long form keeps its footer at the bottom of the screen while it scrolls
       (spec F-8, D-5; plan §5.4, §8), so Save is always in thumb reach. It is the
       form's own footer, not a copy, so it submits exactly what it did.

       The wizard's footer is the only child of its own small form, which would
       be the whole of a sticky box's room to move; that form steps out of the
       layout (`display: contents`), so the bar sticks within the step panel. Its
       two buttons share one row, so the bar is as short as a form's. */
    .form-actions-sticky,
    .wizard-actions-sticky {
        position: sticky;
        bottom: 0;
        z-index: 2;
        padding-bottom: max(var(--cj-space-3), env(safe-area-inset-bottom));
        background: var(--cj-panel-inset-face);
    }
    form:has(> .wizard-actions-sticky) { display: contents; }
    /* Step 1's bar is in its form's own grid, which is the room it sticks in. */
    .form-grid:has(> .wizard-actions-sticky) { display: grid; }
    /* Exit wizard and Save & Continue share the row; the line that says what
       is kept sits right under them, on one line of its own. */
    .wizard-actions.wizard-actions-sticky:has(.wizard-exit) { flex-wrap: wrap; row-gap: 4px; }
    .wizard-actions-sticky .wizard-exit { display: contents; }
    .wizard-actions-sticky .wizard-exit-note { order: 1; flex-basis: 100%; font-size: var(--cj-text-xs); }
    .wizard-actions.wizard-actions-sticky {
        flex-direction: row;
        flex-wrap: nowrap;
        padding-top: var(--cj-space-3);
    }
    .wizard-actions-sticky .button { flex: 1 1 0; white-space: normal; }
    /* A focused field or an error scrolled into view stops above the bar. */
    /* The storefront's Place order bar (MX-20): the shared sticky treatment
       above, edge to edge across the storefront column's own padding, with
       the button the width of the thumb. */
    .storefront-actions.form-actions-sticky {
        margin: var(--cj-space-2) calc(-1 * var(--cj-space-4)) 0;
        padding-top: var(--cj-space-3);
        padding-inline: var(--cj-space-4);
        border-top: 1px solid var(--cj-border);
    }
    .storefront-actions .storefront-submit { width: 100%; margin-top: 0; }
    html:has(.form-actions-sticky, .wizard-actions-sticky) {
        scroll-padding-bottom: calc(var(--cj-control-height) + var(--cj-space-4) + var(--cj-space-3) + var(--cj-space-2));
    }

    .metric-grid { grid-template-columns: 1fr; }

    /* An empty state's action takes the full width of its panel (spec E-4). */
    .empty-state > .button { align-self: stretch; }
    .empty-state-actions { flex-direction: column; align-items: stretch; align-self: stretch; }
    .empty-state-actions .button { width: 100%; }

    /* Two tiles per row on a phone (mobile plan MX-17, AC-8): one per row put
       four count widgets in about 600px before the list began. The money band
       still becomes a stack: three gold figures side by side at 390px is three
       truncated figures. Its dividers move from left edges to top edges. */
    .hud-widgets,
    .brief-main .hud-widgets { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .hud-widget { min-height: 0; }
    /* `flex: 1 1 180px` is a *main axis* basis, and the main axis here is
       vertical: stacked, each figure was 180px tall and the band was 560px of
       mostly nothing. */
    .money-strip { flex-direction: column; gap: var(--cj-space-3); }
    .money-figure { flex: 0 0 auto; padding-left: 0; padding-top: var(--cj-space-3); border-left: 0; border-top: 1px solid var(--cj-panel-edge-inset); }
    .money-figure:first-child { padding-top: 0; border-top: 0; }
    .attention-item { flex-wrap: wrap; }

    /* On a phone the row scrolls, which is the intended behaviour, not the
       desktop bug above. It says so with the tables' scroll cue (mobile plan
       MX-9): a gold edge on whichever side has more tabs, and nothing once
       there are none. The bar sits on the page rather than in a panel, so the
       covers are the page's own `--cj-bg`. Tabs snap to their start edge, and
       the current one keeps clear of the edge when `tabs.js` brings it into
       view on load. */
    .detail-tabs {
        gap: 2px;
        margin-bottom: 18px;
        scroll-snap-type: x proximity;
        background:
            linear-gradient(to right, var(--cj-bg) 20px, transparent 48px) left center / 48px 100% no-repeat local,
            linear-gradient(to left, var(--cj-bg) 20px, transparent 48px) right center / 48px 100% no-repeat local,
            linear-gradient(to right, color-mix(in srgb, var(--cj-gold-dim) 60%, transparent), transparent) left center / 14px 100% no-repeat scroll,
            linear-gradient(to left, color-mix(in srgb, var(--cj-gold-dim) 60%, transparent), transparent) right center / 14px 100% no-repeat scroll;
    }
    .detail-tab { padding: 10px 12px; font-size: .88rem; scroll-snap-align: start; }
    /* The current tab snaps by its end edge: on load the bar starts at its
       left, so a current tab out of view is to the right, and `nearest` brings
       it in by that edge. A start-edge snap would then pull it back out. */
    .detail-tab-current { scroll-margin-inline: var(--cj-space-6); scroll-snap-align: end; }
    /* The first screen (spec PD-1): the facts are label–value pairs on one
       line where they fit, rather than a label over each value. */
    .product-identity { gap: 4px var(--cj-space-4); padding: 8px 14px; }
    .product-identity-item { flex-direction: row; align-items: center; gap: 6px; }

    /* The Product section picker (spec §11; plan F-R, §5.7). The tab bar gives
       way to it here; the band that holds both is sticky under the header, a
       fixed height so `scroll-padding-top` adds it to the header's (SH-9), and
       a picker link's `#product-sections` lands it exactly at its sticky place.
       In rem, so text zoom grows the band too. */
    :root { --cj-section-picker-h: 3.5rem; }
    html:has(.section-picker) { scroll-padding-top: calc(var(--cj-topbar-h) + var(--cj-section-picker-h) + var(--cj-space-2)); }
    .product-sections {
        position: sticky;
        z-index: 2;
        top: var(--cj-topbar-h);
        display: flex;
        align-items: center;
        height: var(--cj-section-picker-h);
        margin: 0 -18px var(--cj-space-3);
        padding: 0 18px;
        border-bottom: 1px solid var(--cj-border);
        background: var(--cj-sidebar);
        scroll-margin-top: calc(-1 * (var(--cj-section-picker-h) + var(--cj-space-2)));
    }
    .product-sections > .detail-tabs { display: none; }
    .section-picker { display: block; flex: 1 1 auto; min-width: 0; }
    .section-picker-menu {
        --cj-picker-room: calc(100dvh - var(--cj-topbar-h) - var(--cj-section-picker-h) - var(--cj-space-4));
        position: relative;
    }
    /* A HUD control, not the browser's disclosure: no triangle, the shell's
       chevron at the end. The label wraps rather than scroll (N-3). */
    .section-picker-toggle {
        display: flex;
        align-items: center;
        gap: var(--cj-space-2);
        min-height: 44px;
        padding: 4px 10px;
        border: 1px solid var(--cj-border-control);
        border-radius: var(--cj-radius-control);
        color: var(--cj-muted);
        cursor: pointer;
        font-family: var(--cj-font-display);
        font-size: var(--cj-text-xs);
        font-weight: 600;
        letter-spacing: .12em;
        line-height: 1.2;
        list-style: none;
        text-transform: uppercase;
    }
    .section-picker-toggle::-webkit-details-marker { display: none; }
    .section-picker-toggle::after {
        content: '';
        flex: 0 0 18px;
        width: 18px;
        height: 18px;
        margin-left: auto;
        background-color: var(--cj-muted);
        -webkit-mask: var(--cj-icon-chevron-down) center / 18px 18px no-repeat;
        mask: var(--cj-icon-chevron-down) center / 18px 18px no-repeat;
    }
    .section-picker-menu[open] > .section-picker-toggle { border-color: var(--cj-border-hover); }
    .section-picker-menu[open] > .section-picker-toggle::after { transform: rotate(180deg); }
    .section-picker-current {
        min-width: 0;
        color: var(--cj-accent-dark);
        font-size: .88rem;
        letter-spacing: .06em;
    }
    /* The open list: a menu surface over the section, below the picker, or
       above it where `disclosure-nav.js` finds more room there. */
    .section-picker-panel {
        --cj-panel-cut: var(--cj-chamfer-sm);
        --cj-panel-edge-color: var(--cj-panel-edge-emphasis);
        --cj-panel-surface: var(--cj-bg);
        position: absolute;
        top: calc(100% + 6px);
        right: 0;
        left: 0;
        padding: 6px;
        box-shadow: var(--cj-shadow-lg);
    }
    .section-picker-menu[data-open-up] > .section-picker-panel { top: auto; bottom: calc(100% + 6px); }
    .section-picker-menu[data-placing] > .section-picker-panel { visibility: hidden; }
    .section-picker-list {
        max-height: calc(var(--cj-picker-room) - 12px);
        margin: 0;
        padding: 0;
        overflow-y: auto;
        overscroll-behavior: contain;
        list-style: none;
    }
    .section-picker-link {
        display: flex;
        align-items: center;
        min-height: 44px;
        padding: 0 10px;
        border-left: 2px solid transparent;
        color: var(--cj-on-dark);
        font-family: var(--cj-font-display);
        font-size: .88rem;
        font-weight: 600;
        letter-spacing: .06em;
        text-transform: uppercase;
    }
    .section-picker-link:hover { background: var(--cj-hover); color: var(--cj-text); text-decoration: none; }
    .section-picker-link[aria-current="page"] {
        border-left-color: var(--cj-accent-hover);
        background: var(--cj-surface);
        color: var(--cj-accent-dark);
    }
    /* A message after a product action shows under the picker (N-9). */
    .page-content:has(.section-picker) > .alert { display: none; }
    .product-section-flash { display: block; }
    /* A short section can still bring the picker to its sticky place. */
    .product-section { min-height: calc(100dvh - var(--cj-topbar-h) - var(--cj-section-picker-h)); }

    /* A card header is a heading, a subtitle and a row of actions side by side,
       which on a phone becomes three columns narrow enough to break "Bill of
       materials" over three lines. They stack instead. */
    .detail-section-header { flex-direction: column; align-items: stretch; gap: 10px; }
    .detail-section-actions { flex-direction: column; }
    .detail-section-actions .button { width: 100%; }

    /* Six steps do not fit across a phone. The markers stay and the labels go:
       the line above the rail names the Product once and says "Step N of 6 ·
       {name}", in place of the frame's heading and the current step's label,
       so the step's own heading is the one step heading on screen (spec W-1). */
    .wizard-progress { flex-wrap: wrap; justify-content: center; gap: var(--cj-space-2); padding-bottom: 0; }
    .wizard-progress::before { display: none; }
    .wizard-step { flex: 0 0 auto; }
    .wizard-step .step-number { margin-bottom: 0; }
    .wizard-step .step-name { display: none; }
    .wizard-header { margin-bottom: var(--cj-space-4); }
    .wizard-header > h2 { display: none; }
    .wizard-phone-progress {
        display: flex;
        flex-direction: column;
        gap: 2px;
        margin: 0 0 var(--cj-space-3);
    }
    .wizard-phone-product { color: var(--cj-text); font-weight: 600; overflow-wrap: anywhere; }
    .wizard-phone-step {
        color: var(--cj-accent-dark);
        font-family: var(--cj-font-display);
        font-size: var(--cj-text-sm);
        font-weight: 700;
        letter-spacing: .1em;
        text-transform: uppercase;
    }
    .wizard-content { padding: 18px; }
    .wizard-actions { flex-direction: column-reverse; align-items: stretch; }
    .wizard-variant-card { padding: 16px; }
    /* A badge in a narrow wizard cell wraps instead of widening the page. */
    .wizard-content .status-badge { white-space: normal; }
    /* A wizard table takes its own container's padding and stops at that
       container's border, so in a version card at 320px it has about 248px
       rather than 216px. It never crosses the card into the step panel, which
       broke the card's frame (mobile plan MX-10). */
    .wizard-content > .table-responsive { margin-inline: -18px; }
    .wizard-variant-card > .table-responsive { margin-inline: -16px; }
    /* The Copy-from picker sized itself to its longest product code and pushed
       the page 143px wider at 320px. It wraps under its label and fits the card. */
    .wizard-variant-actions { min-width: 0; max-width: 100%; }
    .wizard-variant-actions form {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--cj-space-2);
        max-width: 100%;
    }
    .wizard-variant-actions select { max-width: 100%; min-width: 0; }
    /* Version cards (spec W-4, P-4, A-5; plan MA-18). Each card says how many
       lines it has and what they cost. Where a step lists more than one, the
       card's whole head is one toggle (collapsible-lines.js): it covers the
       card's padding, so a tap anywhere on a closed card opens it, and the
       card's own actions sit above it while it is open. Closed, a card is its
       head alone. Without JavaScript nothing is closed. */
    .wizard-version-summary { display: block; margin: 2px 0 0; color: var(--cj-muted); font-size: var(--cj-text-sm); }
    .wizard-variant-card:has(> .wizard-variant-header > .collapsible-toggle) { margin-bottom: 8px; padding-block: 12px; }
    .wizard-variant-header:has(> .collapsible-toggle) { position: relative; padding-right: 32px; }
    .wizard-variant-header:has(> .collapsible-toggle) .wizard-variant-actions { position: relative; z-index: 1; }
    .collapsible-toggle,
    .collapsible-toggle:hover,
    .collapsible-toggle:active {
        display: block;
        position: absolute;
        inset: -12px -16px;
        min-height: 0;
        padding: 0;
        border: 0;
        border-radius: 0;
        background: transparent;
        box-shadow: none;
        color: var(--cj-muted);
        transform: none;
    }
    .collapsible-toggle:focus-visible { outline-offset: -3px; }
    .collapsible-toggle::after {
        content: '';
        position: absolute;
        top: 12px;
        right: 16px;
        width: 20px;
        height: 20px;
        background-color: currentColor;
        -webkit-mask: var(--cj-icon-chevron-down) center / contain no-repeat;
        mask: var(--cj-icon-chevron-down) center / contain no-repeat;
        transition: transform .15s;
    }
    .collapsible-toggle[aria-expanded='true']::after { transform: rotate(180deg); }
    .wizard-variant-card[data-collapsed] > :not(.wizard-variant-header),
    .wizard-variant-card[data-collapsed] .wizard-variant-actions { display: none; }
    .wizard-variant-card[data-collapsed] .wizard-variant-header { margin-bottom: 0; }
    /* Step 2 (spec W-3, P-7): each option is one framed card inside the step
       panel, not a card inside a padded box inside the panel. Values stay
       chips that wrap inline; the 44px remove control sets their height. */
    .wizard-options { margin-bottom: var(--cj-space-4); padding: 0; border-radius: 0; background: none; }
    .wizard-note { margin-bottom: var(--cj-space-4); padding: var(--cj-space-4); }
    .wizard-option-new,
    .wizard-existing { padding: 16px; }
    .wizard-value-chip { gap: 0; padding: 0 0 0 var(--cj-space-3); }
    .combination-bar > .button-primary { flex: 1 1 100%; }

    /* A record list as a stack of rows (spec §7.2, D-1). The table's markup is
       unchanged: the header row stays in the DOM, clipped as `.visually-hidden`
       clips, and each value cell draws its column's name from its own
       `data-label`. That label is visual; whether a screen reader pairs it with
       the value is checked on a device, not assumed (plan §5.3). The record's
       name leads with its actions top-right, and every other value has a line
       of its own, its label beside it. A column the detail page carries anyway
       is left out with `data-stack="omit"` (plan §6). */
    .data-table-stack,
    .data-table-stack tbody { display: block; }
    .data-table-stack thead {
        display: block;
        width: 1px;
        height: 1px;
        margin: -1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }
    .data-table-stack tr {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        gap: 6px var(--cj-space-3);
        padding: var(--cj-space-3) var(--cj-panel-pad-x);
        border-bottom: 1px solid var(--cj-border-subtle);
    }
    .data-table-stack td {
        display: block;
        grid-column: 1 / -1;
        padding: 0;
        border: 0;
    }
    .data-table-stack td.cell-name { grid-column: 1; grid-row: 1; min-width: 0; overflow-wrap: anywhere; }
    /* The identity is the cell carrying the record link, which is not always a
       `.cell-name` (Packaging, Machines, the Products code). A name cell right
       after it is the second identity line (Products: code, then name). */
    .data-table-stack td:first-child {
        grid-column: 1;
        grid-row: 1;
        min-width: 0;
        overflow-wrap: anywhere;
    }
    .data-table-stack td:first-child + td.cell-name {
        grid-column: 1;
        grid-row: 2;
    }
    /* Where the identity is not the first column, its cell says so with
       `data-stack-identity` (the stacked counterpart of `data-pin`), and the
       first cell flows as an ordinary labelled value (plan §5.3). Markup only:
       the desktop table is unchanged. */
    .data-table-stack tr:has(> [data-stack-identity]) > td:first-child {
        grid-column: 1 / -1;
        grid-row: auto;
    }
    .data-table-stack td[data-stack-identity] {
        grid-column: 1;
        grid-row: 1;
        min-width: 0;
        overflow-wrap: anywhere;
    }
    /* A labelled value with no break opportunity (a long email) wraps inside
       the row instead of running past it. */
    .data-table-stack td[data-label] {
        min-width: 0;
        overflow-wrap: anywhere;
    }
    .data-table-stack td.actions { grid-column: 2; grid-row: 1; align-self: start; }
    /* A long single action ("Create Draft production run") wraps inside a capped
       top-right cell instead of squeezing the identity to a sliver. */
    .data-table-stack td.actions {
        white-space: normal;
        max-width: 9.5rem;
    }
    .data-table-stack td.actions .button {
        white-space: normal;
        text-align: center;
    }
    .data-table-stack td.numeric { text-align: left; }
    .data-table-stack td[data-label]::before {
        content: attr(data-label);
        display: inline-block;
        width: 6.5rem;
        margin-right: var(--cj-space-2);
        color: var(--cj-muted);
        font-family: var(--cj-font-display);
        font-size: var(--cj-text-xs);
        font-weight: 600;
        letter-spacing: .12em;
        text-transform: uppercase;
    }
    .data-table-stack td[data-label] small { padding-left: calc(6.5rem + var(--cj-space-2)); }
    .data-table-stack td:empty,
    .data-table-stack [data-stack="omit"] { display: none; }
    .data-table-stack [data-stack="phone"] { display: block; }
    /* A total is the list's last block: its label where the identity goes,
       the figure beside it at the right (plan §5.3). */
    .data-table-stack tfoot { display: block; }
    .data-table-stack tfoot td:not(:first-child) {
        grid-column: 2;
        grid-row: 1;
        text-align: right;
    }
    .data-table-stack tfoot .table-total-label { text-align: left; }
    /* A line editor (T4, spec §8.4) stacks the same way, but every cell is a
       field: each takes the full width with its label above it, so the item
       picker is the width of the line rather than a 48px column. Nothing in
       the markup moves, so the form submits exactly what it did. */
    .data-table-stack.line-editor td {
        grid-column: 1 / -1;
        grid-row: auto;
    }
    .data-table-stack.line-editor td[data-label]::before {
        display: block;
        width: auto;
        margin: 0 0 4px;
    }
    /* Long line lists (spec F-6, Q-8; plan MA-20): collapsible-lines.js puts
       a summary at the top of each line — the item, quantity × unit price and
       the line total (an expense: label and amount) — and the whole summary,
       row padding included, is the line's toggle. A closed line is its
       summary alone; its fields are hidden, not removed, so they are still
       submitted. Without JavaScript there is no summary and nothing closes. */
    .line-editor td.line-summary {
        position: relative;
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        gap: 2px var(--cj-space-3);
        padding-right: 32px;
    }
    .line-summary-item {
        grid-column: 1;
        grid-row: 1;
        overflow: hidden;
        font-weight: 600;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .line-summary-total { grid-column: 2; grid-row: 1; font-variant-numeric: tabular-nums; }
    .line-summary-detail,
    .line-summary-error { grid-column: 1 / -1; color: var(--cj-muted); font-size: var(--cj-text-sm); }
    .line-summary-error { color: var(--cj-danger); font-weight: 600; }
    .line-summary .collapsible-toggle,
    .line-summary .collapsible-toggle:hover,
    .line-summary .collapsible-toggle:active { inset: calc(-1 * var(--cj-space-3)) calc(-1 * var(--cj-panel-pad-x)) -6px; }
    .line-editor tr[data-collapsed] .line-summary .collapsible-toggle { bottom: calc(-1 * var(--cj-space-3)); }
    .line-editor tr[data-collapsed] > td:not(.line-summary) { display: none; }

    /* A record list's row (T1, `data-table-list`) is a compact record block
       (spec L-5; plan F-H): the identity on the first line, wrapping in full,
       with "⋯" top-right; then every figure and badge as label–value pairs
       that flow together over a line or two instead of a line each. Each pair
       wraps as a unit. Only the presentation changes; the cells, their order
       and their labels are the stacked row's. */
    .data-table-list tr {
        position: relative;
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 4px var(--cj-space-3);
        padding: 10px var(--cj-panel-pad-x);
    }
    /* The identity is the `.cell-name`, or, on a list that has none
       (Packaging, Machines, Customers, Channels, Events, Write-offs), the
       first cell. Those keep their desktop cells as they are: `.cell-name`'s
       220px floor is a desktop rule. */
    .data-table-list td.cell-name,
    .data-table-list tr:not(:has(> td.cell-name)) > td:first-child { flex: 1 1 calc(100% - 44px - var(--cj-space-3)); }
    /* A row with no "⋯" (Production, Requires Production, an order past
       Draft, an Event) gives the name the whole line, and an empty actions
       cell takes no part in it. */
    .data-table-list tr:not(:has(.row-overflow-trigger)) > td.cell-name,
    .data-table-list tr:not(:has(> td.cell-name)):not(:has(.row-overflow-trigger)):not(:has(> td.actions > form)) > td:first-child { flex-basis: 100%; }
    /* A row whose action is a button of its own rather than "⋯" (an open
       Write-off's Complete) keeps it beside the identity, as at base, and its
       pairs start on the next line. */
    .data-table-list tr:has(> td.actions > form) > td:first-child { flex-basis: 0; }
    .data-table-list tr:has(> td.actions > form)::after {
        content: '';
        flex-basis: 100%;
    }
    /* An actions cell with nothing in it (a completed Write-off) holds only
       whitespace, which `:empty` does not match; it takes no gap in the row. */
    .data-table-list td.actions:not(:has(*)) { display: none; }
    /* "⋯" keeps its 44×44 target but reaches into the row's padding above it
       and the gap beneath it, so a one-line name's row is not as tall as the
       trigger plus padding. */
    .data-table-list td.actions {
        flex: none;
        align-self: flex-start;
        max-width: none;
        margin: -6px 0 -4px;
    }
    /* Exactly the 44px the name leaves for it, at any text size. */
    .data-table-list .row-overflow-trigger {
        width: 44px;
        padding-inline: 0;
    }
    .data-table-list td[data-label] {
        order: 1;
        flex: 0 1 auto;
        max-width: 100%;
    }
    /* The label's trailing space keeps it a separate word from a value that
       starts without one ("Unit cost —", not "Unit cost—") when it is read. */
    .data-table-list td[data-label]::before {
        content: attr(data-label) ' ';
        display: inline;
        width: auto;
        margin-right: 2px;
    }
    .data-table-list td[data-label] small {
        display: inline;
        margin-left: 4px;
        padding-left: 0;
    }
    /* The whole row opens the record (spec L-6, TI-2; plan F-I). The identity
       link stays the row's one link and one focus stop; its box is stretched
       over the row, so a tap anywhere else in it is a tap on that link, and a
       swipe that scrolls the page is not a tap. Every other control in the row
       sits above the stretched box and keeps its own target; a positioned
       badge does not, so the box is raised above it. The link gives up
       the `position: relative` every table link takes for its 44px overlay, so
       that overlay measures from the row instead. */
    .data-table-list td.cell-name > a,
    .data-table-list tr:not(:has(> td.cell-name)) > td:first-child > a { position: static; }
    .data-table-list td.cell-name > a::after,
    .data-table-list tr:not(:has(> td.cell-name)) > td:first-child > a::after {
        content: '';
        position: absolute;
        z-index: 1;
        inset: 0;
    }
    .data-table-list tr :is(a, button, input, select, textarea, label):not(td.cell-name > a, tr:not(:has(> td.cell-name)) > td:first-child > a) {
        position: relative;
        z-index: 2;
    }
    .data-table-list tr:has(td.cell-name > a:active, > td:first-child > a:active) { background: var(--cj-hover); }
    /* Where a list's desktop identity is not its name (Products: the code
       column), the name cell carries a phone-only link and code, and its
       desktop text gives way to them (spec L-7). The code is the line under
       the name, as a Component's is. */
    .data-table-list td.cell-name > [data-stack="desktop"] { display: none; }
    /* A short second fact about the identity (a run's date beside its
       reference) shares the identity line rather than taking one of its own. */
    .data-table-list td.cell-name > small.cell-name-inline {
        display: inline;
        margin-left: var(--cj-space-2);
    }

    /* A version row in Bill of Materials, Production, Labor and Packaging
       (spec PD-3, P-4): the version and its Manage action on the first line,
       the action top-right on every row as the stacked row has it, then the
       figures as label–value pairs that flow over a line or two. */
    .data-table-versions tr {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 4px var(--cj-space-3);
        padding: 10px var(--cj-panel-pad-x);
    }
    .data-table-versions tr::after {
        content: '';
        flex-basis: 100%;
    }
    .data-table-versions td:first-child { flex: 1 1 0; }
    .data-table-versions td.actions { flex: none; max-width: 55%; }
    .data-table-versions td[data-label] {
        order: 1;
        flex: 0 1 auto;
        max-width: 100%;
    }
    .data-table-versions td[data-label]::before {
        content: attr(data-label) ' ';
        display: inline;
        width: auto;
        margin-right: 2px;
    }

    /* A comparison table keeps its identity column on screen while the figures
       scroll beneath it (spec §7.3 item 2), and gives it less of the width than
       the desktop floor does, so the figures keep about 60% (item 3). */
    /* The identity is the column marked `data-pin` where a table has one
       (Variant BOM: Item, its second column, with Type scrolling beneath it),
       and the first column otherwise. The footer is never pinned, so a
       `colspan` total label stays where the figures put it (plan §5.3). */
    .data-table-pin:not(:has([data-pin])) > :is(thead, tbody) > tr > :first-child,
    .data-table-pin > :is(thead, tbody) > tr > [data-pin] {
        position: sticky;
        z-index: 1;
        left: 0;
        background: var(--cj-panel-face);
    }
    .data-table-pin .cell-name { min-width: 9rem; }
    /* A pinned row shows one inline action, the rest in its "⋯" menu, as a
       stacked row does (§7.2 item 5). */
    .data-table-pin [data-stack="omit"] { display: none; }
    .data-table-pin [data-stack="phone"] { display: inline-block; }

    /* What the header gives up, in the order it gives it up.
       Navigation access and page identity come first, then the user menu, then
       the bell — so the person's name goes before their avatar, and every
       control stays a control. The record's name is no longer given up: it moves
       to its own line above (approved D-2).

       Both panels hang from the controls row rather than from their own button.
       A 17rem menu anchored to the bell's 44px group started 43px off-screen at
       320px, because the bell sits left of the user control. The groups give up
       their containing block here, so both panels measure from the row's right
       edge, which is the viewport's minus the header gutter, and always fit. */
    .user-control { max-width: none; }
    .user-control-name { display: none; }
    .topbar-actions { position: relative; }
    .header-control-group { position: static; }
    .header-menu,
    .header-menu-wide {
        right: 0;
        min-width: min(17rem, calc(100vw - 28px));
        max-width: calc(100vw - 28px);
    }

    /* A dialog is a full-width sheet on a phone, anchored to the bottom so its
       footer actions are in thumb reach (mobile plan §9). modal.js supplies the
       visible height and places the sheet by its top so that it ends where the
       visible area ends, above the keyboard; without it the sheet sits on the
       bottom edge and fills at most the dynamic viewport, and `vh` is the
       fallback for a browser without `dvh`. Header and footer keep their place
       and only the body scrolls, as at every width. The shared panel rule's
       `position: relative` would make the top-layer dialog `absolute`, placed
       against the document, so a sheet opened on a scrolled page would not sit
       at the bottom of the screen; it is fixed to the viewport instead. */
    dialog.modal-dialog[open] {
        /* Defaults for the values modal.js writes inline while it tracks the
           visual viewport; its inline values win. */
        --cj-vvh: 100dvh;
        --cj-vv-top: 0px;
        --cj-vv-bottom: 0px;
        position: fixed;
        width: 100%;
        max-width: none;
        margin: auto 0 0;
        top: var(--cj-vv-top, 0px);
        bottom: var(--cj-vv-bottom, 0px);
        max-height: calc(100vh - 12px);
        max-height: calc(var(--cj-vvh, 100dvh) - 12px);
    }
    /* A swipe that reaches the end of the body stops there instead of carrying
       on into the page behind. */
    dialog.modal-dialog[open] .modal-body { overscroll-behavior: contain; }
    .modal-footer { padding-bottom: max(var(--cj-space-3), env(safe-area-inset-bottom)); }
    .modal-footer .button { flex: 1 1 0; min-height: var(--cj-control-height); }

    .auth-page { padding: 28px 16px; }
    .auth-card { padding: 24px; }

    /* Billing (BUX), spec §12: the comparison becomes a list per row, and the
       primary actions take the full width. Scoped under `.billing-layout`
       because the base Billing rules come later in the file. */
    .billing-layout .billing-compare-table { display: none; }
    .billing-layout .billing-compare-list { display: block; }
    .billing-layout .billing-actions { flex-direction: column; }
    .billing-layout .billing-cta,
    .billing-layout .billing-cta-button { width: 100%; }
    .billing-layout .billing-plan-options { max-width: none; }
    .billing-layout .billing-headline { font-size: var(--cj-text-lg); }
    /* Detail rows and invoice rows run taller on a phone. */
    .billing-layout .billing-slot-details.billing-slot-rows-2 { --billing-slot-reserve: 6rem; }
    .billing-layout .billing-slot-details.billing-slot-rows-3 { --billing-slot-reserve: 9.75rem; }
    .billing-layout .billing-slot-details.billing-slot-rows-4 { --billing-slot-reserve: 12rem; }
    .billing-layout .billing-details .detail-rows { grid-template-columns: 7rem minmax(0, 1fr); }
}

/* The narrowest tier the shell is verified at.

   Between the drawer toggle on the left and the bell and avatar on the right, a
   390px band leaves about 158px for the title — and PACKAGING MATERIALS is 195px
   even a step down in size and tracking, so it truncated to PACKAGING MATERI…
   Shrinking it far enough to fit on one line would take it below the size of the
   labels in the rail, so it wraps to two instead, and two lines of it still sit
   inside the band's 64px without growing it.

   On a record page the record has the second line and may take a third (the
   640px tier above), so there the module holds to one line and truncates: a
   module that also wrapped would make the band four lines tall, and the module
   is the part the drawer's current item already names. */
@media (max-width: 430px) {
    .topbar h1 { font-size: .92rem; letter-spacing: .06em; line-height: 1.05; }
    /* `overflow: hidden` is what draws the ellipsis at wider widths, and here it
       would crop the second line instead. A module title alone may take two
       lines; with a record it keeps one, and the record keeps two, which is
       what the band's fixed height at ≤640px is sized for. */
    .topbar-identity { overflow: visible; }
    .topbar-module {
        overflow: visible;
        text-overflow: clip;
        white-space: normal;
    }
    .topbar h1:has(.topbar-detail) .topbar-module {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    /* A list toolbar's search keeps its 120px at 320 beside the longest sort
       on Filters ("COMPONENT ▲", spec L-3): the row's gaps and Add's side
       padding each give up 2px. */
    .list-toolbar { gap: 4px; }
    .list-toolbar-action { padding-inline: 8px; }
    /* BUX-C2 O-3: at 320 "Pro Monthly — $14.99/month" needs about 240px on one
       line and its button had 230, so it wrapped to 56px under a 44px Annual.
       The stacked cards and both buttons give up side padding, which leaves
       each label room to spare on one line, so the pair matches. The text
       keeps its size and may still wrap if it must. */
    .billing-layout .billing-plan-option { padding-inline: var(--cj-space-2); }
    .billing-layout .billing-plan-button { padding-inline: var(--cj-space-2); }
}

/* Searchable select (combobox) — public/js/searchable-select.js
   Progressive enhancement over a native <select data-searchable-select>: the
   select itself stays in the DOM (hidden) as the real form control, and this
   is the visible text input + listbox laid over it. Tokens match .form-control
   so an enhanced field is visually indistinguishable from a plain one until
   focused. */
.searchable-select { position: relative; }
/* Room for the clear button and the chevron side by side. */
.searchable-select-input { padding-right: 60px; }
/* Closed, the field reads as a picker: the `▾` the filter toggle uses, at the
   end of the field. It takes no pointer events, so a tap on it reaches the
   input and opens the list. The clear button sits to its left, far enough
   that the clear's 44×44 phone tap area stops short of it. */
.searchable-select::after {
    content: '\25BE';
    position: absolute;
    top: 50%;
    right: 10px;
    transform: translateY(-50%);
    color: var(--cj-muted);
    font-size: .85rem;
    line-height: 1;
    pointer-events: none;
}
/* The "nothing chosen" option (UC-3's `data-empty`) is mirrored onto the
   input, so the prompt renders like a placeholder, as it does on a select. */
.searchable-select-input[data-empty] { color: var(--cj-placeholder); }
.searchable-select-clear {
    position: absolute;
    top: 50%;
    right: 32px;
    transform: translateY(-50%);
    width: 24px;
    height: 24px;
    padding: 0;
    border: 0;
    border-radius: var(--cj-radius-sm);
    background: transparent;
    color: var(--cj-muted);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}
.searchable-select-clear:hover { background: var(--cj-hover); color: var(--cj-text); }
/* The shared press drop (`button:active`, translateY(1px)) would replace the
   centring transform, so the button jumped half its height on mouse-down and
   the mouse-up landed on the input: no click, nothing cleared. Pressed, it
   stays centred; it keeps the shared inset press edge. */
.searchable-select-clear:active { transform: translateY(-50%); }
.searchable-select-clear:focus-visible { outline: 3px solid var(--cj-focus); outline-offset: 2px; }
.searchable-select-listbox {
    position: absolute;
    z-index: 40;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    max-height: 280px;
    overflow-y: auto;
    margin: 0;
    padding: 4px;
    list-style: none;
    background: var(--cj-panel-face);
    border: 1px solid var(--cj-border-control);
    border-radius: var(--cj-radius-sm);
    box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
}
.searchable-select-group {
    padding: 6px 10px 4px;
    color: var(--cj-muted);
    font-size: .72rem;
    font-weight: 700;
    letter-spacing: var(--cj-track-label);
    text-transform: uppercase;
}
.searchable-select-option {
    position: relative;
    padding: 8px 10px 8px 28px;
    border-radius: var(--cj-radius-sm);
    cursor: pointer;
}
/* The highlight is where the arrow keys or the pointer are: a face and a left
   bar. It sets nothing the selection uses, so a highlighted selected option
   keeps its mark, colour and weight. */
.searchable-select-option:hover,
.searchable-select-option.is-active {
    background: var(--cj-highlight-face);
    box-shadow: inset 3px 0 0 var(--cj-highlight-edge);
}
/* The selection is what the field holds: a check mark in the option's gutter,
   gold and bold. The mark is decorative (empty alternative text); the state
   is `aria-selected`. */
.searchable-select-option[aria-selected="true"] { color: var(--cj-accent-dark); font-weight: 700; }
.searchable-select-option[aria-selected="true"]::before {
    content: '\2713';
    content: '\2713' / '';
    position: absolute;
    top: 50%;
    left: 10px;
    transform: translateY(-50%);
    line-height: 1;
}
.searchable-select-empty { padding: 8px 10px; color: var(--cj-muted); font-size: .85rem; }
/* In the top layer (mobile plan §10) the script places the list in fixed
   coordinates, so the in-wrapper offsets are reset, as the overflow menu's are;
   open or closed is the popover state, not `hidden`. */
.searchable-select-listbox[popover] { position: fixed; inset: auto; margin: 0; }
.searchable-select-listbox[popover]:not(:popover-open) { display: none; }

/* ------------------------------------------------- EV-7B storefront ---- */

/* The public Event storefront's own shell — a standalone page, not the
   authenticated app's sidebar/topbar shell, so it needs only a page ground
   and a readable max-width column. Mobile-first: the grid below is
   `auto-fit`, so it is already a single column at phone widths with no
   media query needed, and gains columns as room allows. */
.storefront-body { background: var(--cj-bg); }
.storefront-shell { max-width: 720px; margin: 0 auto; padding: var(--cj-space-5) var(--cj-space-4) var(--cj-space-6); }

.storefront-header { margin-bottom: var(--cj-space-5); }
.storefront-eyebrow {
    margin: 0 0 var(--cj-space-1);
    color: var(--cj-accent-dark);
    font-size: var(--cj-text-xs);
    font-weight: 700;
    letter-spacing: var(--cj-track-label);
    text-transform: uppercase;
}
.storefront-title {
    margin: 0;
    font-family: var(--cj-font-display, inherit);
    font-size: var(--cj-text-2xl);
}
.storefront-meta { margin: var(--cj-space-1) 0 0; color: var(--cj-muted); }

.storefront-section { margin-bottom: var(--cj-space-5); }
/* The confirmation's table sits on the page, not in a panel, so the scroll
   cue's covers take the page ground; without one they were transparent and
   the gold edges showed on a table that does not scroll (MX-20). */
.storefront-section > .table-responsive { --cj-panel-surface: var(--cj-bg); }
.storefront-section-title { margin: 0 0 var(--cj-space-1); font-size: var(--cj-text-lg); }
.storefront-section-subtitle { margin: 0 0 var(--cj-space-3); color: var(--cj-muted); font-size: var(--cj-text-sm); }

.storefront-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--cj-grid-gap); }

.storefront-card {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: var(--cj-space-3);
    padding: var(--cj-panel-pad-y) var(--cj-panel-pad-x);
    background: var(--cj-panel-face);
    border: 1px solid var(--cj-panel-edge);
    border-radius: var(--cj-radius-sm);
}
.storefront-card-name { margin: 0; font-size: var(--cj-text-base); }
.storefront-card-variant { margin: 2px 0 0; color: var(--cj-muted); font-size: var(--cj-text-sm); }
.storefront-card-price { margin: var(--cj-space-2) 0 0; color: var(--cj-accent-dark); font-weight: 700; font-size: var(--cj-text-lg); }
.storefront-card-stock { margin: var(--cj-space-1) 0 0; color: var(--cj-muted); font-size: var(--cj-text-sm); }
.storefront-card-qty { display: flex; align-items: center; gap: var(--cj-space-2); }
.storefront-card-qty label { color: var(--cj-muted); font-size: var(--cj-text-sm); }
.storefront-qty-input { width: 5.5rem; }

/* EV-7C — the order form beneath the catalog grids: reuses `.form-field`/
   `.form-control`/`.form-error` (defined once, above) rather than a second
   set of storefront-only field styles. */
.storefront-submit { margin-top: var(--cj-space-2); }

/* EV-7D — confirmation page and owner-panel QR/copy controls. Reuses the
   EV-7B storefront rules above (`.storefront-header`, `.storefront-section`,
   `.storefront-card-variant`, `.data-table`, `.detail-rows`) rather than a
   second parallel set; the two additions below are genuinely new shapes:
   the QR image container (the `<svg>` inside it is server-rendered by
   `App\Support\Storefront\QrCodeSvg` via the `<x-storefront-qr>` Blade
   component, not client-side JS) and the copyable-URL row on the
   owner-facing Event show page. */
.storefront-qr {
    display: inline-block;
    width: 160px;
    max-width: 100%;
    margin-top: var(--cj-space-2);
}
.storefront-qr svg { display: block; width: 100%; height: auto; }
#storefront-url-value {
    display: inline-block;
    word-break: break-all;
    margin-right: var(--cj-space-2);
}

/* Billing (BUX) — the /billing family pages (BUX spec §5.8, §12; plan BUX-1).
   HUD tokens only: no new colours and none of the public `c-pricing-card`
   styles. At 901px and wider the family's leading blocks take the main column
   and the supporting ones (includes, help) the rail; below that everything
   stacks in the family's order. The page keeps the default width. Its
   breakpoint rules sit in the stylesheet's single 901px and 640px blocks
   (D-6), each under a "Billing (BUX)" comment. */
/* One implicit column by default; the 901px block, which sits earlier in the
   file, adds the rail with `grid-template-columns`, so no rule here sets it. */
.billing-layout { display: grid; grid-auto-columns: minmax(0, 1fr); gap: var(--cj-grid-gap); align-items: start; }
.billing-main,
.billing-rail { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--cj-grid-gap); min-width: 0; }

.billing-headline { margin: 0; font-family: var(--cj-font-display); font-size: var(--cj-text-xl); font-weight: 600; line-height: 1.25; overflow-wrap: anywhere; }
.billing-badges { display: flex; flex-wrap: wrap; gap: var(--cj-space-2); margin: var(--cj-space-2) 0 0; }
.billing-next-step { margin: var(--cj-space-3) 0 0; max-width: 62ch; }
/* BUX-C2 D-1: the local renewal sentence and the hidden copy of the longest
   one the read can bring share one cell, so the line is as tall as either. */
.billing-next-step-slot { display: grid; }
.billing-next-step-slot > * { grid-area: 1 / 1; }
.billing-next-step-reserve { visibility: hidden; }
.billing-actions { display: flex; flex-wrap: wrap; gap: var(--cj-space-2); margin-top: var(--cj-space-4); }
.billing-cta { display: grid; gap: var(--cj-space-1); align-content: start; margin: 0; }
.billing-cta-help { margin: 0; color: var(--cj-muted); font-size: var(--cj-text-sm); }
.billing-actions-note { margin: var(--cj-space-2) 0 0; color: var(--cj-muted); font-size: var(--cj-text-sm); }
.billing-portal-return { margin: var(--cj-space-3) 0 0; color: var(--cj-muted); font-size: var(--cj-text-sm); }
.billing-history .billing-actions { margin-top: 0; }
.billing-cta-button,
.billing-plan-button { min-height: 44px; }
.billing-status .alert,
.billing-plan-choice .alert { margin: var(--cj-space-4) 0 0; }

/* Annual and Monthly as two equal cards (§6.4; BUX-C1): side by side where the
   column fits two, Annual directly over Monthly where it does not. Neither
   button is filled; Annual's recommendation is its flag and its edge. The flag
   sits on the card's top edge, out of flow, so both buttons line up. */
.billing-plan-options { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); gap: var(--cj-space-4) var(--cj-space-3); max-width: 44rem; padding-top: var(--cj-space-2); }
.billing-plan-option { position: relative; display: grid; gap: var(--cj-space-2); align-content: start; margin: 0; padding: var(--cj-space-4) var(--cj-space-3) var(--cj-space-3); border: 1px solid var(--cj-border); }
.billing-plan-option-recommended { border-color: var(--cj-gold-dim); }
.billing-plan-button { width: 100%; white-space: normal; }
.billing-plan-flag { position: absolute; top: 0; left: var(--cj-space-3); margin: 0; padding: 0 var(--cj-space-1); transform: translateY(-50%); background: var(--cj-panel-face); color: var(--cj-accent-dark); font-family: var(--cj-font-display); font-size: var(--cj-text-xs); line-height: 1.4; letter-spacing: var(--cj-track-label); text-transform: uppercase; }
.billing-plan-value { margin: 0; color: var(--cj-muted); font-size: var(--cj-text-sm); }
.billing-plan-note { margin: var(--cj-space-3) 0 0; color: var(--cj-muted); font-size: var(--cj-text-sm); }

/* Free and Cajella Pro (BUX-C1): what Free includes, then where Pro differs —
   a table above the phone tier, a list per row on it. */
.billing-subhead { margin: 0 0 var(--cj-space-2); font-family: var(--cj-font-display); font-size: var(--cj-text-sm); font-weight: 600; letter-spacing: var(--cj-track-label); text-transform: uppercase; color: var(--cj-muted); }
.billing-free-list { margin: 0 0 var(--cj-space-5); padding-left: 1.2em; display: grid; gap: var(--cj-space-1); }
.billing-pro-lead { margin: 0 0 var(--cj-space-2); max-width: 62ch; }
.billing-compare-table { width: 100%; border-collapse: collapse; font-size: .89rem; }
.billing-compare-table th,
.billing-compare-table td { padding: 10px 12px; border-bottom: 1px solid var(--cj-border-subtle); text-align: left; vertical-align: top; }
.billing-compare-table thead th { border-bottom-color: var(--cj-gold-dim); color: var(--cj-muted); font-family: var(--cj-font-display); font-size: var(--cj-text-xs); font-weight: 600; letter-spacing: .16em; text-transform: uppercase; }
.billing-compare-table tbody th { font-weight: 500; }
.billing-compare-table td { width: 22%; font-weight: 650; }
.billing-compare-list { display: none; }
.billing-compare-row { padding: var(--cj-space-3) 0; border-bottom: 1px solid var(--cj-border-subtle); }
.billing-compare-label { margin: 0 0 var(--cj-space-1); }
.billing-compare-row dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--cj-space-2); margin: 0; }
.billing-compare-row dt { color: var(--cj-muted); font-size: var(--cj-text-xs); letter-spacing: var(--cj-track-label); text-transform: uppercase; }
.billing-compare-row dd { margin: 0; font-weight: 650; overflow-wrap: anywhere; }

/* Your Free plan usage (§6.5; BUX-C1): a compact grid of counts. A limit
   reached or nearly reached is marked in words; the fill bar is decorative. */
.billing-usage-summary { margin: 0 0 var(--cj-space-3); max-width: 62ch; }
.billing-usage-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 9rem), 1fr)); gap: var(--cj-space-3) var(--cj-space-4); margin: 0; padding: 0; list-style: none; }
.billing-usage-row { display: grid; gap: var(--cj-space-1); align-content: start; min-width: 0; }
.billing-usage-label { margin: 0; color: var(--cj-muted); font-size: var(--cj-text-sm); overflow-wrap: anywhere; }
.billing-usage-count { margin: 0; color: var(--cj-text); font-weight: 650; font-variant-numeric: tabular-nums; }
.billing-usage-reached .billing-usage-count,
.billing-usage-near .billing-usage-count { color: var(--cj-accent-dark); }
.billing-usage-bar { display: block; height: 4px; background: var(--cj-border); }
.billing-usage-bar > span { display: block; width: var(--billing-usage-fill, 0%); height: 100%; background: var(--cj-accent); }
.billing-usage-near .billing-usage-bar > span { background: var(--cj-gold-dim); }
.billing-usage-reached .billing-usage-bar > span { background: var(--cj-accent-dark); }
.billing-usage-note { display: flex; flex-wrap: wrap; align-items: center; gap: var(--cj-space-1) var(--cj-space-2); margin: var(--cj-space-1) 0 0; color: var(--cj-muted); font-size: var(--cj-text-sm); }

.billing-includes-list,
.billing-when-ends-list { margin: 0; padding-left: 1.2em; display: grid; gap: var(--cj-space-2); }
.billing-includes-note,
.billing-help-note { margin: var(--cj-space-3) 0 0; color: var(--cj-muted); font-size: var(--cj-text-sm); }
.billing-when-ends p,
.billing-help p { margin: 0 0 var(--cj-space-2); }
.billing-help p:last-child { margin-bottom: 0; }

/* Activation pending and recovery (B-PEND, spec §5.5, §9.2; plan BUX-3). The
   check and Manage billing sit side by side, and stack with the other
   actions at the phone tier. */
.billing-pending { margin-top: var(--cj-space-4); }
.billing-pending-progress,
.billing-pending-active p,
.billing-recovery-lead { margin: 0; }
.billing-recovery-lead { font-weight: 600; }
.billing-recovery-support { margin: var(--cj-space-3) 0 0; color: var(--cj-muted); font-size: var(--cj-text-sm); }
.billing-check { display: grid; gap: var(--cj-space-2); align-content: start; }
.billing-check-slot:empty { display: none; }
.billing-check-result,
.billing-check-slot > .billing-check-message { padding: var(--cj-space-3); border: 1px solid var(--cj-border-subtle); border-left: 3px solid var(--cj-gold-dim); }
.billing-check-result p { margin: 0 0 var(--cj-space-1); }
.billing-check-result p:last-child { margin-bottom: 0; }
.billing-check-slot > .billing-check-message { margin: 0; }
.billing-check-message { font-weight: 600; }
.billing-check-detail,
.billing-check-time { color: var(--cj-muted); font-size: var(--cj-text-sm); }
.billing-help-check { margin: var(--cj-space-3) 0; padding-top: var(--cj-space-3); border-top: 1px solid var(--cj-border-subtle); }

/* Deferred Stripe details (spec §10.2, §12; plan BUX-4). Each slot keeps its
   height while it loads, so the page does not jump. Invoices are compact
   two-line rows at every width: date and amount, then Stripe's status, any
   note and the one link. No table, so nothing scrolls sideways. */
/* BUX-C2 D-1: the server marks each slot `billing-slot-reserved`, so first
   paint already holds about what arrives: the state's detail rows, or its
   usual invoices. billing.js drops the class once the fragment is in, and
   the reservation goes with it. A flow root keeps the fragment's own margins
   inside the slot, so the reserved height and the loaded height agree. */
.billing-slot { display: flow-root; }
.billing-slot-reserved { min-height: var(--billing-slot-reserve, 2.75rem); }
.billing-slot-details.billing-slot-rows-2 { --billing-slot-reserve: 4.5rem; }
.billing-slot-details.billing-slot-rows-3 { --billing-slot-reserve: 6.75rem; }
.billing-slot-details.billing-slot-rows-4 { --billing-slot-reserve: 9rem; }
.billing-slot-history.billing-slot-rows-1 { --billing-slot-reserve: 6.5rem; }
.billing-slot-history.billing-slot-rows-3 { --billing-slot-reserve: 19.2rem; }
/* The local rows and the Stripe rows are two lists in one panel: one label
   column for both, and the list's own row gap between them. */
.billing-details .detail-rows { grid-template-columns: 8.5rem minmax(0, 1fr); }
.billing-details .detail-rows + .billing-slot > .detail-rows { margin-top: var(--cj-space-3); }
.billing-slot-loading,
.billing-slot-note { margin: 0; color: var(--cj-muted); font-size: var(--cj-text-sm); }
.billing-slot .billing-actions { margin-top: var(--cj-space-3); }
.billing-invoices { margin: 0 0 var(--cj-space-4); padding: 0; list-style: none; }
.billing-invoice { padding: var(--cj-space-3) 0; border-bottom: 1px solid var(--cj-border-subtle); }
.billing-invoice:first-child { padding-top: 0; }
.billing-invoice-main,
.billing-invoice-meta { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--cj-space-1) var(--cj-space-3); margin: 0; }
.billing-invoice-meta { margin-top: var(--cj-space-2); align-items: center; }
.billing-invoice-description { color: var(--cj-muted); }
.billing-invoice-amount { margin-left: auto; color: var(--cj-accent-dark); font-weight: 650; font-variant-numeric: tabular-nums; }
.billing-invoice-note { color: var(--cj-muted); font-size: var(--cj-text-sm); }
.billing-invoice-link { margin-left: auto; display: inline-flex; align-items: center; min-height: 44px; }

/* BUX-C2 D-1: without JavaScript nothing will fill a reserved slot, so it
   holds only its <noscript> note, and the renewal line only its own sentence,
   never an empty gap. */
@media (scripting: none) {
    .billing-slot-reserved { min-height: 0; }
    .billing-next-step-reserve { display: none; }
}
