/**
 * FRAMEWORK - UNIVERSAL CSS FOUNDATION
 * Rebuilt with zero-redundancy philosophy
 * Version: Restructured 2025
 */

/* ===== IMPORT OPTIONAL FEATURE FILES ===== */
@import 'alerts.css?v=6'; /* ?v busts the @import's own cache: the parent's ?v= does NOT propagate to @import URLs (documented below) - bump this when alerts.css changes */
@import 'nested-forms.css';
@import 'expanding-blocks.css?v=4'; /* bump when expanding-blocks.css changes — @imports cache independently (see alerts.css note above) */
@import 'content-highlights.css?v=2'; /* bump when content-highlights.css changes — @imports cache independently */
@import 'content-feed.css?v=2'; /* bump when content-feed.css changes — @imports cache independently */
@import 'pin-input.css';

/* Site-specific layout (site-layout.css) and print-styles.css are loaded
   separately via <link> in index.php so they get their own ?v=<timestamp>
   cache-bust. @import would bypass the parent stylesheet's cache-busting
   query string — Safari is especially aggressive about caching @imports. */

/* ===== IMPORT SECTION-SPECIFIC STYLESHEETS ===== */
@import 'rental-agreements.css';

/* ========================================================================
   CSS CUSTOM PROPERTIES - COLOR SYSTEM
   ────────────────────────────────────────────────────────────────────────
   Per-site VALUES live in config.php (THEME_* constants) and are emitted
   as :root custom properties by templates/parts/theme-vars.php BEFORE
   this file loads. So --primary, --secondary, --bg-base, --surface-*,
   --text-on-dark/light, --border, --hover-*, --focus-ring are already
   defined when the rules below resolve var() references.

   What stays HERE (framework-level, identical across all 5 sites):
     - Universal constants (--white, --black) that no site customizes.
     - Semantic aliases (--success, --error, --warning, --info, --neutral,
       --border-accent, --border-error) wired to per-site color tokens.
   ======================================================================== */

:root {
    /* ===== UNIVERSAL CONSTANTS (no site customizes these) ===== */
    --white: #ffffff;
    --black: #000000;

    /* ===== SEMANTIC STATE ALIASES =====
       These wire colors to ROLES. Values reference per-site tokens
       (defined via theme-vars.php) so the same alias produces each
       site's brand-consistent state colors. */
    --success:        var(--primary-bright);
    --success-bright: var(--primary-accent);
    --success-dark:   var(--primary-dark);
    --success-bg:     var(--primary-bright);
    --success-text:   var(--primary-bright-text);

    --error:        var(--tertiary);
    --error-bright: var(--tertiary-bright);
    --error-dark:   var(--tertiary-dark);
    --error-bg:     var(--tertiary);
    --error-text:   var(--tertiary-text);

    --warning:        var(--tertiary-accent);
    --warning-bright: var(--tertiary-accent);
    --warning-dark:   var(--tertiary-dark);
    --warning-bg:     var(--tertiary-accent);
    --warning-text:   var(--tertiary-accent-text);

    --info:        var(--secondary);
    --info-bright: var(--secondary-bright);
    --info-dark:   var(--secondary-dark);
    --info-bg:     var(--secondary);
    --info-text:   var(--secondary-text);

    --border-accent: var(--primary-accent);
    --border-error:  var(--error);

    --neutral:       var(--secondary);
    --neutral-hover: var(--secondary-dark);
    --neutral-text:  var(--secondary-text);
}

/* ========================================================================
   CSS CUSTOM PROPERTIES - SPACING SYSTEM (TIER 1 density knobs)
   ────────────────────────────────────────────────────────────────────────
   Every token is var(--theme-*, framework-default). theme-vars.php emits
   BEFORE this file, so a site that wants a different density (sparser,
   tighter) defines --theme-spacing-* there with its OWN values and every
   framework component follows. A site that defines nothing gets these
   defaults. Same pattern for typography and radius below.
   ======================================================================== */

:root {
    --spacing-xs: var(--theme-spacing-xs, 0.2rem);
    --spacing-sm: var(--theme-spacing-sm, 0.4rem);
    --spacing-md: var(--theme-spacing-md, 0.8rem);
    --spacing-lg: var(--theme-spacing-lg, 1.2rem);
    --spacing-xl: var(--theme-spacing-xl, 1.6rem);
    --spacing-xxl: var(--theme-spacing-xxl, 2.4rem);
}

/* ========================================================================
   CSS CUSTOM PROPERTIES - TYPOGRAPHY (TIER 1 type-scale knobs)
   ======================================================================== */

:root {
    --font-size-xs: var(--theme-font-xs, 0.75rem);
    --font-size-sm: var(--theme-font-sm, 0.875rem);
    --font-size-md: var(--theme-font-md, 1rem);
    --font-size-lg: var(--theme-font-lg, 1.125rem);
    --font-size-xl: var(--theme-font-xl, 1.25rem);
    --font-size-h3: var(--theme-font-h3, 1.5rem);
    --font-size-h2: var(--theme-font-h2, 2rem);
    --font-size-h1: var(--theme-font-h1, 2.5rem);
}

/* ========================================================================
   CSS CUSTOM PROPERTIES - LAYOUT & EFFECTS (TIER 1 knobs)
   ======================================================================== */

:root {
    --radius-sm: var(--theme-radius-sm, 4px);
    --radius-md: var(--theme-radius-md, 8px);
    --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2);
    --shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
    --shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.2);
    --transition-fast: 0.15s ease;
    --transition-normal: 0.3s ease;
    --transition-slow: 0.5s ease;
}

/* ========================================================================
   CSS CUSTOM PROPERTIES - COMPONENT TOKENS (TIER 3)
   ────────────────────────────────────────────────────────────────────────
   The tokens the ELEMENT BASELINES below actually consume. Contexts amend
   components by OVERRIDING these tokens in scope (e.g. details { --field-bg:
   var(--surface-lighter); }) — never by re-declaring the component's
   properties. One source of truth for what a field/button IS; contexts only
   recolour/respace it. Values chain to tier-2 roles so site themes flow
   through automatically. ALWAYS override bg and text as a PAIR.
   ======================================================================== */

:root {
    /* Data-entry fields (input/select/textarea) */
    --field-bg:           var(--surface-base);
    --field-text:         var(--surface-text);
    --field-border:       var(--border);
    --field-border-width: 2px;
    --field-pad:          0.75rem;
    --field-radius:       var(--radius-sm);
    --field-focus-border: var(--primary-accent);
    --field-focus-ring:   var(--focus-ring);

    /* Minimum touch-target size (Apple HIG / Material minimum). Applied
       framework-wide under (pointer: coarse) — see MOBILE TOUCH TARGETS. */
    --tap-min: 44px;

    /* Fold lists (FOLD LISTS below): one header line's height (a group's;
       a list item's is a shade shorter, .fold-item), and the room a header
       keeps at its right end for the controls laid over it. */
    --fold-head: 2.5rem;
    --fold-controls-w: 230px;

    /* Native widget theming (checkboxes, radios, progress) — browsers paint
       these in the site's accent instead of vendor blue. */
    accent-color: var(--primary-accent);
}

/* ========================================================================
   AJAX LOADING STATE
   loadContent() adds .loading to the hub target during every in-flight
   request. It was referenced framework-wide but styled NOWHERE — zero visual
   feedback on any AJAX action. Dim the block, block interaction (which also
   prevents double-submit clicks on stale content), and show a spinner.
   ======================================================================== */
.loading {
    position: relative;
    opacity: 0.55;
    pointer-events: none;
    transition: opacity var(--transition-fast);
}

.loading::after {
    content: '';
    position: absolute;
    top: 1rem;
    left: 50%;
    width: 1.75rem;
    height: 1.75rem;
    margin-left: -0.875rem;
    border: 3px solid var(--border-light);
    border-top-color: var(--primary-accent);
    border-radius: 50%;
    animation: loading-spin 0.6s linear infinite;
    z-index: 5;
}

@keyframes loading-spin {
    to { transform: rotate(360deg); }
}

/* ========================================================================
   RESET - ZERO DEFAULT
   ======================================================================== */

* {
    margin: 0;
    padding: 0;
    border: 0;
    box-sizing: border-box;
}

/* ========================================================================
   BASE ELEMENTS
   ======================================================================== */

html {
    font-size: 16px;
    line-height: 1.6;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: var(--bg-base);
    color: var(--text-on-dark);
    min-height: 100vh;
    /* Frame-safety: long unbroken tokens (URLs, emails, filenames, hashes)
       wrap instead of pushing the page into horizontal scroll. Inherited
       everywhere; white-space:nowrap contexts are unaffected. */
    overflow-wrap: break-word;
}

/* ===== FRAME SAFETY — nothing escapes the viewport ===== */

/* Media never overflows its container; height follows to keep aspect. */
img, video {
    max-width: 100%;
    height: auto;
}

/* Code/preformatted blocks scroll internally, never widen the page. */
pre {
    max-width: 100%;
    overflow-x: auto;
}

/* ===== TYPOGRAPHY ===== */

h1, h2, h3, h4, h5, h6 {
    color: var(--text-on-dark-accent);
    font-weight: 600;
    line-height: 1.3;
    margin-bottom: var(--spacing-md);
}

h1 { font-size: var(--font-size-h1); }
h2 { font-size: var(--font-size-h2); }
h3 { font-size: var(--font-size-h3); }
h4 { font-size: var(--font-size-xl); }
h5 { font-size: var(--font-size-lg); }
h6 { font-size: var(--font-size-md); }

/* Form controls don't inherit the page font by default; without this they
   fall back to the UA control font (often near-monospace for textarea).
   Normalize to the cascade font. Font-agnostic: the actual face is a
   per-site call (theme.css), never dictated here. */
input, select, textarea, button { font: inherit; }

/* Number inputs: no increment spinners, framework-wide. They obscure the
   value (especially mobile) and are never used for entry. Opt back in
   per-field only on specific request. */
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

p {
    margin-bottom: var(--spacing-md);
    line-height: 1.7;
}

/* ===== LISTS ===== */

ul, ol {
    list-style-position: inside;
    padding-left: var(--spacing-lg);
    margin-bottom: var(--spacing-md);
}

/* ===== LINKS ===== */

a {
    color: var(--primary-accent);
    text-decoration: none;
    transition: color var(--transition-normal);
}

a:hover {
    text-decoration: underline;
}

a:focus {
    outline: 2px solid var(--primary-accent);
    outline-offset: 2px;
}

/* Content links - distinguishable without color alone (WCAG) */
a:not(.button):not(.nav-link) {
    font-weight: 600;
}

/* ========================================================================
   LAYOUT STRUCTURE
   ======================================================================== */

header {
    background-color: var(--bg-base);
    color: var(--white);
    padding: var(--spacing-md) var(--spacing-xl);
}

#mainContent {
    flex: 1;
    padding: var(--spacing-xl);
    background-color: var(--surface-base);
}

/* Phones: reclaim the wide gutters — on a 360px screen the desktop padding
   costs ~14% of the usable width. */
@media (max-width: 480px) {
    #mainContent {
        padding: var(--spacing-md) var(--spacing-sm);
    }
}

footer {
    background-color: var(--surface-light);
    color: var(--text-on-dark-accent);
    padding: var(--spacing-xl);
    text-align: center;
}

/* Header Logo — site-specific sizing in site-layout.css */

/* ========================================================================
   DISCLOSURE / EXPANDING ELEMENTS — <details>, <summary>
   ────────────────────────────────────────────────────────────────────────
   Baseline applies to every <details>/<summary> on the site. Class-specific
   patterns (.expanding-details, .form-group--collapsible, .toMany-card,
   .account-trigger) layer their own decoration on top. Marker-hide and
   summary cursor live here once instead of being duplicated per class.
   ======================================================================== */

details {
    margin-bottom: var(--spacing-md);
    border-radius: var(--radius-md, 0.5rem);
    overflow: hidden;
    background: var(--surface-light);
}

summary {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.75rem;
    width: 100%;
    padding: 1rem;
    cursor: pointer;
    list-style: none;
    font-weight: 500;
    background: var(--surface-lighter);
    color: var(--text-on-dark-accent);
}

summary:hover {
    background: var(--surface-lightest);
}

/* Hide default disclosure triangle, WebKit (Safari, Chrome, iOS) */
summary::-webkit-details-marker {
    display: none;
}

/* Hide default disclosure marker, Firefox + modern engines */
summary::marker {
    display: none;
    content: '';
}

/* Inline SVG icons inside any summary, sit to the left of the label. */
summary svg {
    flex-shrink: 0;
    width: 2rem;
    height: 2rem;
    color: var(--primary-accent);
}

/* When an SVG is present, the label span takes remaining space so the
   plus/minus indicator stays flush right. */
summary svg + span {
    flex: 1;
}

/* Canada-leaf accent inside a summary, word-space sized, not a 2rem icon. */
summary .canada-leaf {
    width: 0.9em;
    height: 0.9em;
    margin-right: 0.3em;
}

/* Plus/minus indicator on every <details>, no opt-in attribute needed.
   The +/- is the universal disclosure cue across the site. */
summary::after {
    content: '+';
    font-weight: bold;
    font-size: 1.25em;
    margin-left: 0.5rem;
    color: var(--primary-accent);
}

details[open] > summary::after {
    content: '−';
}

/* Body of the details (every direct child that isn't the summary). */
details > *:not(summary) {
    padding: 1rem;
    background: var(--surface-base);
    color: var(--surface-text);
}

/* ========================================================================
   LONG-FORM TEXT — blockquote, code
   ────────────────────────────────────────────────────────────────────────
   Element baselines for rendered formatted text (renderMarkdown() output:
   Input=formatted previews, read views, display-mode fields). Element-
   first: any blockquote/code on the site gets this; site CSS tints on top.
   ======================================================================== */

blockquote {
    margin: 0 0 var(--spacing-md);
    padding: 0.5rem 1rem;
    border-left: 3px solid var(--primary-accent);
    background: var(--surface-light);
    border-radius: 0 var(--radius-md, 0.5rem) var(--radius-md, 0.5rem) 0;
}

blockquote > p:last-child {
    margin-bottom: 0;
}

code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.9em;
    padding: 0.1em 0.35em;
    border-radius: 0.25rem;
    background: var(--surface-light);
}

/* Inserted text: a highlighter mark (the Edit Packets gutter compare marks
   the added words with <ins>; <del> keeps the browser's strike-through).
   Background only, the site's yellow accent mixed over whatever surface it
   sits on, so it reads on dark and light themes; the text colour stays. */
.script-editor__diff ins {
    text-decoration: none;
    background: color-mix(in srgb, var(--accent-yellow) 40%, transparent);
    border-radius: 0.15em;
}

/* ========================================================================
   FORMS — ELEMENT BASELINES (element-first standard)
   ────────────────────────────────────────────────────────────────────────
   THE framework contract: every data-entry control is fully styled AT THE
   ELEMENT LEVEL, consuming the TIER-3 field tokens above. No wrapper class
   is required for a control to render correctly — a bare <input> dropped
   anywhere on any site is bordered, padded, full-width, and mobile-safe.
   .form-group is a SPACING/LABEL wrapper only; it owns no field visuals.

   All baselines sit in :where(...) so they carry ZERO specificity — any
   class rule anywhere overrides them without !important or selector wars.

   Exceptions are declared HERE, once, at the element level (checkbox/radio/
   buttons/range/color). Screen-level "exceptions" that restyle field
   visuals per page are a bug, not a pattern.
   ======================================================================== */

form {
    margin-bottom: var(--spacing-md);
}

/* Text-entry family: everything that reads as a "field". Submit/button/reset
   belong to the BUTTONS baseline; checkbox/radio/range/color/file get their
   own exceptions below. */
:where(input:not([type="checkbox"], [type="radio"], [type="submit"], [type="button"], [type="reset"], [type="range"], [type="color"], [type="file"], [type="hidden"], [type="image"]),
       select,
       textarea) {
    width: 100%;
    padding: var(--field-pad);
    border: var(--field-border-width) solid var(--field-border);
    border-radius: var(--field-radius);
    background-color: var(--field-bg);
    color: var(--field-text);
    /* 1rem resolves to 16px (html is fixed 16px) — at or above the iOS
       threshold, so focusing a field NEVER triggers Safari's auto-zoom. */
    font-size: var(--font-size-md);
    min-width: 0; /* flex-item safety: fields never force their row wider than the frame */
    transition: border-color var(--transition-normal), box-shadow var(--transition-normal);
}

:where(input:not([type="checkbox"], [type="radio"], [type="submit"], [type="button"], [type="reset"], [type="range"], [type="color"], [type="file"], [type="hidden"], [type="image"]),
       select,
       textarea):focus {
    outline: none;
    border-color: var(--field-focus-border);
    box-shadow: 0 0 0 3px var(--field-focus-ring);
}

:where(textarea) {
    resize: vertical;
    min-height: 100px;
}

/* File inputs: keep the field frame but lighter padding — the control's
   native button carries its own chrome. */
:where(input[type="file"]) {
    width: 100%;
    padding: var(--spacing-sm);
    border: var(--field-border-width) dashed var(--field-border);
    border-radius: var(--field-radius);
    background-color: var(--field-bg);
    color: var(--field-text);
    min-width: 0;
}

/* Checkbox & radio: intrinsic size, inline, accent-colored (see :root). */
:where(input[type="checkbox"], input[type="radio"]) {
    display: inline-block;
    width: auto;
    margin: 0 var(--spacing-xs) 0 0;
    vertical-align: middle;
}

/* Range & color: native chrome, no field frame. */
:where(input[type="range"], input[type="color"]) {
    width: 100%;
    padding: 0;
    border: 0;
    background: transparent;
    min-width: 0;
}

/* Date family: iOS Safari gives these an INTRINSIC width and ignores width
   declarations unless appearance is reset — which made date fields spill
   past their fieldset on phones (Enter Expense, 2026-08-05). appearance:none
   makes them size like text fields; the native picker still opens on tap. */
:where(input[type="date"], input[type="datetime-local"], input[type="time"], input[type="month"], input[type="week"]) {
    -webkit-appearance: none;
    appearance: none;
    max-width: 100%;
    /* Native picker glyph is browser-drawn and ignores color: — it follows
       color-scheme. Without this, Chromium draws its near-black default glyph,
       invisible on the dark sites' --field-bg. Token from theme-vars.php:
       dark sites emit 'dark' (light glyph), the light site emits 'light'. */
    color-scheme: var(--color-scheme, normal);
}

/* ── .form-group: spacing + label wrapper ONLY ─────────────────────────── */

.form-group {
    margin-bottom: var(--spacing-md);
}

.form-group label {
    display: block;
    font-weight: 600;
    color: var(--text-on-dark-accent);
    margin-bottom: var(--spacing-xs);
    font-size: var(--font-size-sm);
}

/* Required field indicator rendered by MFF for fields with Validation=required */
.required-indicator {
    color: var(--error);
    margin-left: 2px;
    font-weight: 700;
    font-size: 0.85em;
    vertical-align: super;
}

.form-group input[type="radio"] + label,
.form-group input[type="checkbox"] + label {
    display: inline-block;
    margin: 0 var(--spacing-md) 0 0;
    font-weight: normal;
    cursor: pointer;
    vertical-align: middle;
}

/* Form hints / notes.
   .form-text was used all over the PHP (DatabaseTable "None." notes,
   agreements meta lines, management "Pick what you want to do:") but styled
   NOWHERE — an orphan class rendering as plain body text. It now shares the
   hint treatment so every note reads consistently muted. */
.form-hint,
.form-text {
    font-size: 0.85em;
    color: var(--text-on-dark-secondary);
    margin-top: var(--spacing-xs, 0.25rem);
    margin-bottom: 0;
}

.form-hint-inline {
    font-size: 0.85em;
    color: var(--text-on-dark-secondary);
    font-weight: normal;
}

/* Hints never sit inside an option flow (owner standard 2026-08-05): a hint
   that lands inside a field row or option grid takes a full line of its own
   instead of skewing the distribution. flex-basis covers the flex rows,
   grid-column covers grid variants (.pl-year-strip); each ignores the other. */
:where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row,
       .payment-line-row, .expense-line-row, .entry-dr-cr-row, .checkbox-group,
       .pl-year-strip) :where(.form-hint, .form-text) {
    flex: 0 0 100%;
    width: 100%;
    grid-column: 1 / -1;
}

/* Field-level error state — applied by makeFormFields() when $options['errors'] is passed */
.form-group.has-error input,
.form-group.has-error select,
.form-group.has-error textarea {
    border-color: var(--border-error);
}

.form-group.has-error label {
    color: var(--error);
}

.field-error-message {
    color: var(--error);
    font-size: var(--font-size-sm);
    margin-top: var(--spacing-xs);
    margin-bottom: 0;
}

/* Collapsible optional field (opt-in via makeFormFields option 'collapse_optional')
   Wraps a single optional field in a native <details> so the label becomes a
   thin tappable summary. Marker-hide and summary cursor come from the
   element-level baseline above; this block adds only the form-field-specific
   decoration (border, padding, custom chevron, dense flex layout). */
.form-group--collapsible {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-base);
}
.form-group--collapsible > summary {
    padding: 0.3rem 0.6rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--spacing-xs);
    color: var(--text-on-dark);
    font-size: var(--font-size-sm);
    user-select: none;
}
.form-group--collapsible > summary::after {
    content: "\25B8"; /* ▸ */
    transition: transform 0.15s ease;
    color: var(--text-on-dark-secondary);
    font-size: 0.9em;
    flex: 0 0 auto;
}
.form-group--collapsible[open] > summary::after {
    transform: rotate(90deg);
}
.form-group--collapsible[open] > summary {
    border-bottom: 1px solid var(--border);
}
.form-group--collapsible__label {
    font-weight: 600;
}
.form-group--collapsible__hint {
    font-size: 0.8em;
    color: var(--text-on-dark-secondary);
    font-weight: normal;
    margin-left: auto;
    margin-right: var(--spacing-xs);
}
/* Inner .form-group inherits normal field styling — only tighten spacing so
   the collapsible shell owns the outer margin instead of doubling it up. */
.form-group--collapsible > .form-group {
    margin-bottom: 0;
    padding: var(--spacing-xs) 0.6rem;
}
/* The <summary> already announces the field name; the inner <label> would
   repeat it visually. Hide it from sight but KEEP it in the DOM so the
   for="…" association, screen readers, and click-to-focus behavior all
   still work. Standard "visually hidden" technique, scoped to the
   collapsible so regular forms are unaffected. */
.form-group--collapsible > .form-group > label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ========================================================================
   Inline-create affordance
   Rendered by DatabaseTable::renderInlineCreateAffordance() next to any
   select or searchDB input whose column comment declares CreatableInline.
   Class-scoped — nothing here touches bare <button> or <div>.
   ======================================================================== */
.inline-create-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    margin-top: 0.35rem;
    padding: 0.25rem 0.65rem;
    font-size: var(--font-size-sm, 0.85rem);
    line-height: 1.2;
    /* Filled with the --primary / --primary-text PAIR so the label is
       contrast-guaranteed on every site theme and on any surface (incl. the
       darker inventory-receipt block). The old ghost style (color:--primary on
       transparent) relied on --primary contrasting with whatever was behind it,
       which fails on dark themes where --primary ≈ the background. */
    color: var(--primary-text);
    background: var(--primary);
    border: 1px solid var(--primary);
    border-radius: var(--radius-sm, 4px);
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}
.inline-create-toggle:hover,
.inline-create-toggle:focus-visible {
    background: var(--primary-dark);
    border-color: var(--primary-dark);
    color: var(--primary-text);
    outline: none;
}
.inline-create-toggle[aria-expanded="true"] {
    background: var(--primary-dark);
    border-color: var(--primary-dark);
    color: var(--primary-text);
    border-style: solid;
}
.inline-create-toggle__icon {
    display: inline-block;
    font-weight: 700;
    font-size: 1.05em;
    line-height: 1;
    transform: translateY(-0.05em);
}
.inline-create-toggle[aria-expanded="true"] .inline-create-toggle__icon {
    transform: rotate(45deg);   /* + becomes × when expanded */
    transition: transform 0.15s ease;
}

/* Target slot that receives the injected child form. Empty = invisible. */
.inline-create-target:empty {
    display: none;
}
.inline-create-target {
    margin-top: var(--spacing-xs, 0.5rem);
}

/* The injected child form uses .inline-create-form + standard fieldset styling.
   Visually nest it slightly under the parent field so the relationship is clear. */
.inline-create-form {
    margin-top: var(--spacing-xs, 0.5rem);
    margin-left: var(--spacing-md, 1rem);
    border-left: 3px solid var(--primary);
    background: var(--bg-layer-lighten);
}
.inline-create-form__actions {
    display: flex;
    gap: var(--spacing-sm, 0.5rem);
    margin-top: var(--spacing-sm, 0.75rem);
}

/* Fieldset */
fieldset {
    border: 2px solid var(--border);
    border-radius: var(--radius-md);
    margin: var(--spacing-md) 0;
    padding: var(--spacing-md);
    background: var(--bg-layer-lighten);
    /* Browsers ship fieldset with min-width:min-content — it REFUSES to
       shrink below its widest child, so one wide control (an iOS date input,
       a long select) pushed the whole fieldset past the viewport on phones
       (the tabbed-dispatch block documents the same trap). Element-level
       reset: fieldsets always fit their container; children wrap/shrink. */
    min-width: 0;
}

fieldset legend {
    padding: var(--spacing-xs) var(--spacing-md);
    font-weight: 600;
    color: var(--text-on-dark-accent);
    background: var(--surface-base);
    border: 2px solid var(--border);
    border-radius: var(--radius-sm);
}

/* ========================================================================
   BUTTONS
   ======================================================================== */

.button,
a.button,
button,
input[type="submit"],
input[type="button"],
input[type="reset"] {
    appearance: none;
    background: var(--neutral);
    color: var(--neutral-text);
    padding: 0.75rem 1.5rem;
    border-radius: var(--radius-sm);
    font-weight: 500;
    cursor: pointer;
    transition: all var(--transition-normal);
    font-size: var(--font-size-md);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-decoration: none;
}

.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="button"]:hover {
    background: var(--neutral-hover);
    transform: translateY(-1px);
}

/* Disabled state — applies to .button (incl. .primary/.danger) + raw buttons.
   Faded + not-allowed so a blocked action (e.g. Complete Run held by a stock
   shortfall) reads as inactive instead of clickable. */
.button:disabled,
.button[disabled],
button:disabled,
button[disabled] {
    opacity: 0.5;
    cursor: not-allowed;
    transform: none;
    box-shadow: none;
}

.button.primary,
button.primary {
    background: var(--primary);
    color: var(--text-on-dark);
}

.button.primary:hover {
    background: var(--primary-bright);
}

.button.secondary {
    background: var(--secondary);
    color: var(--text-on-dark);
}

.button.secondary:hover {
    background: var(--secondary-bright);
}

.button.danger {
    background: var(--error);
    color: var(--text-on-dark);
}

.button.danger:hover {
    background: var(--error-bright);
}

/* Caution actions (reset / start-over): the warning semantic pair. */
.button.warning {
    background: var(--warning);
    color: var(--warning-text);
}

.button.warning:hover {
    background: var(--warning-bright);
}

.button.compact {
    padding: 0.4rem 0.8rem;
    font-size: var(--font-size-sm);
}

/* ────────────────────────────────────────────────────────────────────
   Refresh button: re-fires the current selection on an auto_select
   dropdown. Browsers do not fire `change` when the same option is
   reselected, so this affordance gives the user an explicit way to
   re-trigger the cascade. Auto-emitted by makeSmartNestedForm() for
   auto_select + dispatch dropdowns; see framework-nested-forms.md
   §2.4 and §2.7. Sits inline next to its <select>.
   ──────────────────────────────────────────────────────────────── */
.refresh-button {
    background: var(--neutral);
    color: var(--text-on-dark);
    padding: 0;
    width: 2.25rem;
    height: 2.25rem;
    min-height: 2.25rem;
    border-radius: 50%;
    font-size: 1.1rem;
    line-height: 1;
    flex: 0 0 auto;
}

.refresh-button:hover {
    background: var(--neutral-hover);
}

/* Flex row: <select> grows to fill the available width, refresh button keeps
   its own (small) width. Wraps the <select> + .refresh-button emitted by
   makeSmartNestedForm() for auto_select dropdowns so they sit on one line
   instead of the button wrapping under a 100%-wide select. Gap matches the
   margin convention used elsewhere in the form UI. */
.select-with-refresh {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.select-with-refresh select {
    flex: 1 1 auto;
    min-width: 0;
}

/* Missing pay period rows in payroll Source Data section: highlight
   with the warning token so the gap is visually obvious in the report.
   Single rule by design — everything else in the Source Data table
   uses existing report patterns (.report-section, .striped, .numeric,
   <strong> for emphasis). See sections/admin/payroll/remittance.php. */
tr.missing-period-row td {
    background: color-mix(in srgb, var(--warning) 15%, transparent);
}

/* ────────────────────────────────────────────────────────────────────
   Module Selection toggle list (Framework branch / Module Selection).
   Inline single-line rows: checkbox + module label + slug, grouped by
   subject inside collapsible <details> groups. One <li> per module.
   See sections/admin/framework.php. Element-first per the no-inline-
   style rule; orphan-row variant is the only diff.
   ──────────────────────────────────────────────────────────────── */
.module-subject-group {
    margin-bottom: 0.5rem;
}

.module-subject-group > summary .count {
    color: var(--text-muted, inherit);
    font-weight: normal;
}

.module-toggle-list {
    list-style: none;
    padding-left: 0;
    margin: 0.5rem 0 0 0;
}

.module-toggle-list li {
    padding: 0.15rem 0;
}

.module-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
}

.module-toggle-label {
    font-weight: 500;
}

.module-toggle-slug {
    opacity: 0.7;
}

.module-toggle-orphan .module-toggle {
    opacity: 0.7;
}

/* ────────────────────────────────────────────────────────────────────
   Script editor (Input=script) — dual-pane edit/preview wrapper with
   floating sticky controls (toggle top-left, save top-right). The two
   panes share the same min-height so flipping doesn't reflow layout;
   scroll position is synced by the JS module via a ratio. See
   framework-backend.md §"Input=script" for the contract.
   ──────────────────────────────────────────────────────────────── */
.script-editor {
    position: relative;
    margin-top: 0.5rem;
    /* The gutter item: a block's number, a gap, then its pending button. */
    --gutter-mark-h: 1.25rem;
    --gutter-gap: 0.5rem;
}

.script-editor__controls {
    position: sticky;
    top: 0.75rem;
    z-index: 10;
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    pointer-events: none;
    margin-bottom: 0.5rem;
}

.script-editor__controls > button {
    pointer-events: auto;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}

.script-editor__pane {
    /* No fixed height — both panes grow to fit their content so the page
       scrolls naturally and flipping doesn't change the page's overall
       height. The sticky controls stay anchored to the viewport. */
}

.script-editor__pane[hidden] {
    display: none;
}

.script-editor__input {
    /* FIXED-height, internally-scrolling pane — the flip model's contract:
       script-editor.js anchors the edit/preview flip on each pane's own
       scrollTop (block + fraction mapping), so both panes MUST scroll
       internally rather than auto-grow the page. Canonical since 2026-07-18
       (replaced the field-sizing:content auto-grow model fleet-wide). */
    field-sizing: fixed;
    width: 100%;
    height: 60vh;
    min-height: 18rem;
    overflow-y: auto;
    padding: 1rem;
    font: inherit;
    line-height: 1.8;
    background: var(--surface-base);
    color: var(--surface-text);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    resize: none;
    box-sizing: border-box;
    white-space: pre-wrap;
}

.script-editor__pane--preview {
    display: flex;
    align-items: stretch;
}

.script-editor__output {
    flex: 1 1 auto;
    height: 60vh;
    min-height: 18rem;
    overflow-y: auto;
    /* script-editor.js places this scroll itself (flip anchors, landings
       computed for the final layout); the browser's scroll anchoring would
       shift it again under a growing or closing compare panel. */
    overflow-anchor: none;
    padding: 1rem;
    background: var(--surface-base);
    color: var(--surface-text);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-sizing: border-box;
    line-height: 1.8;
}

/* Block-number margin column (owner request 2026-09-26), synced to the
   preview pane's scroll by script-editor.js, same fixed-height/overflow
   contract as .script-editor__output so the two stay the same box height;
   hidden (the attribute, set by script-editor.js) when the rendered format
   carries no [data-source-line] blocks. Wide enough for a tap-sized pending
   button (owner 2026-10-04). */
.script-editor__gutter {
    flex: 0 0 auto;
    width: calc(var(--tap-min) + 0.5rem);
    height: 60vh;
    min-height: 18rem;
    overflow: hidden;
    position: relative;
    background: var(--surface-base);
    border: 1px solid var(--border);
    border-right: none;
    box-sizing: border-box;
}

.script-editor__gutter-inner {
    position: relative;
}

/* One item per block, placed at the block's top by script-editor.js: the
   number, then (a gap below, never beside or over it) the pending button. */
.script-editor__gutter-item {
    position: absolute;
    left: 0;
    right: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--gutter-gap);
}

/* The block number: a plain small numeral that jumps to the edit pane.
   Compact on touch too (PD G2): the pending button is the tap target. */
.script-editor__gutter-mark {
    min-height: 0;
    height: var(--gutter-mark-h);
    padding: 0;
    font-size: var(--font-size-xs);
    line-height: var(--gutter-mark-h);
    color: var(--text-on-dark-secondary);
    background: none;
    border: none;
}

.script-editor__gutter-mark:hover {
    color: var(--surface-text);
    text-decoration: underline;
    background: none;
    transform: none;
}

/* Pending-edit button (owner 2026-10-04, replaces the 2026-09-26 dot): the
   framework's primary button, tap-sized everywhere, the pencil plus the
   count. Open (its compare showing) reads pressed. */
.script-editor__gutter-pending {
    min-width: var(--tap-min);
    min-height: var(--tap-min);
    padding: 0 0.25rem;
    font-size: var(--font-size-lg);
    line-height: 1;
}

.script-editor__gutter-pending[aria-expanded="true"] {
    background: var(--primary-bright);
    box-shadow: inset 0 0 0 2px var(--surface-text);
}

/* A block with a pending edit keeps room for its gutter item (number, gap,
   button), so the next block's number never meets the button, and wears
   the compare panel's rule. It never shrinks: an explicit min-height
   replaces a flex item's automatic minimum (its content), so in a render
   that lays its blocks out as a flex column inside the fixed-height pane
   (EAD's .ads-render) a long block was squeezed to this minimum and its
   last lines ran under its open compare panel (owner, C1S1 block 38,
   2026-10-08). */
.script-editor__output [data-pending] {
    min-height: calc(var(--gutter-mark-h) + var(--gutter-gap) + var(--tap-min));
    flex-shrink: 0;
    padding-left: 0.5rem;
    box-shadow: inset 3px 0 0 var(--accent, #e0a030);
}

/* The inline compare grows open from its block on its grid row (0fr to
   1fr, nothing measured) and closes the same way (--closing). Its margin
   runs from minus the render's own block gap (--render-gap, set by the
   format's render), so it takes and gives back exactly its space: nothing
   below snaps at either end. */
.script-editor__diff {
    display: grid;
    grid-template-rows: 1fr;
    margin: 0;
    opacity: 1;
    border-left: 3px solid var(--accent, #e0a030);
    background: var(--surface-raised, rgba(255,255,255,0.03));
    font-size: 0.9em;
    transition: grid-template-rows var(--transition-normal),
                margin-top var(--transition-normal),
                opacity var(--transition-normal);
}

@starting-style {
    .script-editor__diff {
        grid-template-rows: 0fr;
        margin-top: calc(-1 * var(--render-gap, 0px));
        opacity: 0;
    }
}

.script-editor__diff--closing {
    grid-template-rows: 0fr;
    margin-top: calc(-1 * var(--render-gap, 0px));
    opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
    .script-editor__diff {
        transition: none;
    }
}

.script-editor__diff-body {
    min-height: 0;
    overflow: hidden;
}

.script-editor__diff-inner {
    padding: 0.5rem 0.75rem;
}

.script-editor__diff-label {
    font-weight: 700;
    margin: 0.25rem 0 0.1rem;
}

.script-editor__diff-note {
    color: var(--text-on-dark-secondary);
    font-style: italic;
    margin: 0 0 0.25rem;
}

/* -anchor: an INSERT's "After this block:" text, plain reference (it stays). */
.script-editor__diff-current,
.script-editor__diff-proposed,
.script-editor__diff-anchor {
    white-space: pre-wrap;
    margin: 0 0 0.25rem;
}

.script-editor__diff-current {
    text-decoration: line-through;
    opacity: 0.7;
}

/* ────────────────────────────────────────────────────────────────────
   Checkbox group: a <fieldset class="checkbox-group"> wrapping a list
   of inline <label><input type="checkbox">…</label> pairs (typically
   emitted by admin filter panels). Default browser styling lays labels
   inline; this rule stacks them vertically so each filter reads as
   its own line. See sections/admin/site-content.php for the canonical
   consumer (Manage X pattern's filter dimensions).
   ──────────────────────────────────────────────────────────────── */
.checkbox-group {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
}

.checkbox-group > label {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    cursor: pointer;
}

/* ========================================================================
   TABLES
   ======================================================================== */

table {
    width: 100%;
    border-collapse: collapse;
    margin: var(--spacing-md) 0;
    border-radius: var(--radius-md);
    overflow: hidden;
    box-shadow: var(--shadow-md);
    background: var(--surface-base);
}

table th {
    padding: var(--spacing-md);
    font-weight: 600;
    color: var(--text-on-dark-accent);
    background: var(--surface-lighter);
    border-bottom: 2px solid var(--border);
    text-align: left;
}

table td {
    padding: var(--spacing-md);
    color: var(--surface-text);
    border-bottom: 1px solid var(--border);
    vertical-align: top;
}

table tbody tr {
    transition: background-color var(--transition-fast);
}

table tbody tr:hover {
    background-color: var(--hover-lighten);
}

/* Table variants */
table.striped tbody tr:nth-child(even) td {
    background: var(--bg-layer-lighten);
}

/* A materials/stock row flagged short of stock: a subtle, theme-aware error
   tint that beats .striped alternation (defined after it). Semantic spans
   inside keep their own colour. Mirrors tr.missing-period-row. */
table.striped tbody tr.row-short td,
tr.row-short td {
    background: color-mix(in srgb, var(--error) 16%, var(--surface-base));
    color: var(--surface-text);
}

table.compact th,
table.compact td {
    padding: var(--spacing-sm);
    font-size: var(--font-size-sm);
}

/* Receipt Index entries (books/reports.php rptReceiptIndex) — stacked audit
   list; was inline style, moved here on touch per the no-inline-style rule. */
.receipt-index-entry {
    margin-bottom: 1.5rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--border);
}

/* ── Sortable column headers (initSortableTables, owner standard 2026-08-11)
   Every data table is click-to-sort by default; these rules are the visual
   contract. The arrow keys off aria-sort, which the JS maintains — no state
   classes. Faded ↕ = sortable, solid ↑/↓ = active column + direction. */
th[data-sortable-col] {
    cursor: pointer;
    user-select: none;
    white-space: nowrap;
}
th[data-sortable-col]::after {
    content: '\2195';   /* ↕ */
    margin-left: 0.35em;
    opacity: 0.35;
    font-weight: normal;
}
th[data-sortable-col][aria-sort="ascending"]::after {
    content: '\2191';   /* ↑ */
    opacity: 1;
    color: var(--primary-accent);
}
th[data-sortable-col][aria-sort="descending"]::after {
    content: '\2193';   /* ↓ */
    opacity: 1;
    color: var(--primary-accent);
}
th[data-sortable-col]:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: -2px;
}

/* Table cell utilities */
.numeric {
    text-align: right;
    /* Tabular figures in the page's own face — columns align without the
       typewriter look of the old 'Courier New' assignment. */
    font-variant-numeric: tabular-nums;
    font-weight: 500;
}

.date {
    white-space: nowrap;
    color: var(--text-on-dark-secondary);
}

.status {
    text-align: center;
    font-weight: 500;
}

.actions {
    white-space: nowrap;
}

.actions button {
    margin: 0 var(--spacing-xs);
}

/* Responsive tables */
@media (max-width: 768px) {
    table {
        font-size: var(--font-size-sm);
    }
    
    table th,
    table td {
        padding: var(--spacing-sm);
    }
    
    .actions {
        white-space: normal;
    }
    
    .actions button {
        display: block;
        width: 100%;
        margin-bottom: var(--spacing-xs);
    }
}

/* ========================================================================
   CARDS & BLOCKS
   ======================================================================== */

.card,
.dashboard-block,
.management-block {
    background: var(--surface-base);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: var(--spacing-lg);
    margin: var(--spacing-md) 0;
}

/* ========================================================================
   UTILITY CLASSES
   ======================================================================== */

/* Padding */
.pad-xs { padding: var(--spacing-xs); }
.pad-sm { padding: var(--spacing-sm); }
.pad-md { padding: var(--spacing-md); }
.pad-lg { padding: var(--spacing-lg); }
.pad-xl { padding: var(--spacing-xl); }

.no-pad { padding: 0; }
.no-pad-top { padding-top: 0; }
.no-pad-bottom { padding-bottom: 0; }
.no-pad-left { padding-left: 0; }
.no-pad-right { padding-right: 0; }

/* Margin */
.margin-xs { margin: var(--spacing-xs); }
.margin-sm { margin: var(--spacing-sm); }
.margin-md { margin: var(--spacing-md); }
.margin-lg { margin: var(--spacing-lg); }
.margin-xl { margin: var(--spacing-xl); }

.no-margin { margin: 0; }
.no-margin-top { margin-top: 0; }
.no-margin-bottom { margin-bottom: 0; }
.no-margin-left { margin-left: 0; }
.no-margin-right { margin-right: 0; }

.margin-top-md { margin-top: var(--spacing-md); }
.margin-top-lg { margin-top: var(--spacing-lg); }

/* Borders */
.bordered { border: 1px solid var(--border); }
.border-light { border: 1px solid var(--border-light); }
.border-accent { border: 1px solid var(--border-accent); }
.border-error { border: 1px solid var(--border-error); }

.border-top { border-top: 1px solid var(--border); }
.border-bottom { border-bottom: 1px solid var(--border); }
.border-left { border-left: 3px solid var(--border-accent); }
.border-right { border-right: 1px solid var(--border); }

/* Backgrounds */
.bg-surface { background: var(--surface-base); }
.bg-surface-light { background: var(--surface-light); }
.bg-surface-lighter { background: var(--surface-lighter); }
.bg-layer { background: var(--bg-layer-lighten); }

/* Text */
.text-secondary { color: var(--text-on-dark-secondary); }
.text-accent { color: var(--text-on-dark-accent); }
.text-center { text-align: center; }
.text-right { text-align: right; }
.text-left { text-align: left; }

/* Semantic state colours — bound to canonical tokens, theme-aware */
.text-error   { color: var(--error); }
.text-success { color: var(--success); }
.text-warning { color: var(--warning); }

/* Widths */
.input-narrow { max-width: 8em; }

/* Printable view chrome (makePrintableView) + read-only display mode */
.print-controls { margin-bottom: var(--spacing-md); }

.form-readonly {
    margin: 0;
    padding: var(--spacing-xs) 0;
}

/* GL worked-document hierarchy (run-view.php and future GL reports).
   Reads top-down in calculation order; visual prominence GROWS toward the
   bottom line. Three tiers:
     1. .calc-detail     - quiet working arithmetic
     2. .calc-milestone  - stage results (Base Pay, GROSS PAY, totals)
     3. .net-pay-card    - the unmissable bottom line */
.calc-detail {
    font-size: var(--font-size-sm);
    color: var(--text-on-dark-secondary);
}

.calc-path { margin: 0 0 var(--spacing-xs) 0; }

.calc-path.chosen { font-weight: 700; }

.calc-chosen-tag {
    display: inline-block;
    padding: 0 var(--spacing-xs);
    border: 1px solid var(--border-accent);
    border-radius: 4px;
    color: var(--text-on-dark-accent);
    font-size: var(--font-size-sm);
    font-weight: 700;
    text-transform: uppercase;
}

.calc-flag {
    border-left: 3px solid var(--border-accent);
    padding-left: var(--spacing-xs);
    font-weight: 600;
}

.calc-milestone {
    background: var(--bg-layer-lighten);
    border-left: 3px solid var(--border-accent);
    padding: var(--spacing-md);
    font-size: 1.1rem;
    font-weight: 700;
}

.calc-subordinate {
    font-size: var(--font-size-sm);
    color: var(--text-on-dark-secondary);
}

.calc-subordinate h4 {
    font-size: var(--font-size-sm);
    color: var(--text-on-dark-secondary);
}

/* Stage-total table rows read as milestones */
tr.total-row td {
    font-weight: 700;
    font-size: 1.05rem;
    background: var(--bg-layer-lighten);
}

.net-pay-card {
    background: var(--bg-layer-lighten);
    border-left: 5px solid var(--success);
}

.net-pay-card table { width: 100%; }

.net-pay-card tr {
    font-size: 1.8rem;
    font-weight: 700;
}

.net-pay-card .numeric { color: var(--success); }

/* Display */
.hidden { display: none; }

/* The hidden ATTRIBUTE must always win. Author CSS beats the UA stylesheet,
   so any element-level display rule (.picker-item { display:block }, a flex
   row, etc.) silently defeats the browser's [hidden]{display:none} - which
   made every data-list-filter consumer whose rows carry an explicit display
   look "broken": setupListFilters sets row.hidden and nothing disappears
   (the Transaction Register's account/vendor picker filter, 2026-07-29).
   This is the one canonical use of !important: the attribute's semantics
   are unconditional. Per-component patches like .script-editor__pane[hidden]
   are now redundant but harmless. Note data-list-filter only toggles
   visibility - a hidden checked checkbox stays checked AND still submits,
   so filter-then-check-then-refilter keeps every selection. */
[hidden] { display: none !important; }
/* .no-print sets NO display on screen — see print-styles.css. */

/* Screen reader only */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ========================================================================
   FUNCTIONAL CLASSES (Keep if used by JS/PHP)
   ======================================================================== */

/* Nested form target */
.nested-form-target {
    margin-top: var(--spacing-md);
}

/* Button group - inline button layout */
.button-group {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-sm);
    align-items: center;
}

/* Account actions - consistent layout across all account sections */
.account-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--spacing-sm);
    margin-top: var(--spacing-lg);
}

.account-actions .button.muted {
    background: var(--surface-light);
    color: var(--text-on-dark-secondary);
    font-size: var(--font-size-sm);
}

.account-actions .button.muted:hover {
    background: var(--surface-lighter);
    color: var(--text-on-dark);
}

.account-actions .action-primary {
    margin-right: var(--spacing-md);
}

.account-actions .action-group {
    display: flex;
    gap: var(--spacing-xs);
}

@media (max-width: 600px) {
    .account-actions {
        flex-direction: column;
        align-items: stretch;
    }

    .account-actions .action-primary {
        margin-right: 0;
        margin-bottom: var(--spacing-sm);
    }

    .account-actions .action-group {
        flex-direction: column;
    }

    .account-actions .button {
        width: 100%;
        justify-content: center;
    }
}

/* Dashboard action grid - 2 column mobile-first */
.dashboard-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--spacing-sm);
    margin-top: var(--spacing-md);
}

.dashboard-grid .button {
    width: 100%;
    justify-content: center;
    text-align: center;
}

.dashboard-grid .inline-form {
    display: contents;
}

.dashboard-grid .inline-form .button {
    width: 100%;
    justify-content: center;
}

/* Shop terminal (shop-dashboard.php): function buttons + exit actions.
   Desktop = wrap-flex row with real gaps (both axes); phones = the same
   2-up grid of full-width buttons the employee dashboard uses — one
   standard look for dashboard action clusters. */
.task-buttons,
.shop-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-sm);
    align-items: center;
}

.shop-actions {
    margin-top: var(--spacing-md);
}

.shop-actions form {
    display: contents;
}

@media (max-width: 640px) {
    .task-buttons,
    .shop-actions {
        display: grid;
        grid-template-columns: 1fr 1fr;
    }

    .task-buttons .button,
    .shop-actions .button {
        width: 100%;
        justify-content: center;
        text-align: center;
    }
}

/* Clock status - compact display for dashboard */
.clock-status {
    font-weight: 600;
    font-size: var(--font-size-sm);
    padding: var(--spacing-xs) 0;
    margin: 0;
}
.clock-status.clocked-in { color: var(--success); }
.clock-status.clocked-out { color: var(--error); }

/* Simulate login - admin only */
.simulate-login {
    margin-top: var(--spacing-md);
    padding-top: var(--spacing-sm);
    border-top: 1px solid var(--surface-lighter);
}

.simulate-login label {
    font-size: var(--font-size-sm);
    display: block;
    margin-bottom: var(--spacing-xs);
}

.simulate-input-row {
    display: flex;
    gap: var(--spacing-xs);
}

.simulate-input-row input {
    flex: 1;
    min-width: 0;
}

/* Inline form - for forms that should display inline */
.inline-form {
    display: inline-block;
    margin: 0;
}

.inline-form button {
    margin: 0;
}

/* INLINE-EDIT (fieldset-scope editor injected into a list-row slot).
   Same nested-form exception as inline-create; contract lives in
   universal.js setupInlineEditButtons(). Empty slot = invisible.

   The .inline-edit-slot owns the placement (full-width, wraps below its row).
   The .inline-edit-form fieldset itself is display:contents — transparent to
   the row layout, so its makeFormFields children render directly in the slot
   with no nested-fieldset box. SINGLE source of truth for this class: an older
   "table row editing" duplicate lower in this file was removed (2026-07-17). */
.inline-edit-slot:empty {
    display: none;
}
.inline-edit-slot {
    flex-basis: 100%;
    min-width: 100%;
    margin-top: var(--spacing-xs, 0.5rem);
}
.inline-edit-form {
    display: contents;
}

/* Autofetch dropdown */
.autoFetchContainer {
    position: relative;
}

.autoFetchOptions {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--white);
    border: 1px solid var(--border-light);
    border-top: none;
    max-height: 200px;
    overflow-y: auto;
    z-index: 1000;
}

.autofetch-option {
    padding: var(--spacing-sm);
    cursor: pointer;
    border-bottom: 1px solid var(--border);
    color: var(--black);
}

.autofetch-option:hover,
.autofetch-option.autofetch-active {
    background-color: var(--primary);
    color: var(--white);
}

/* Validation display */
.validation-summary {
    margin: var(--spacing-sm) 0;
}

.validation-changes {
    margin: 0;
    padding-left: 1.5em;
    list-style-type: disc;
}

/* File upload states */
.file-preview {
    font-size: var(--font-size-sm);
    color: var(--text-on-dark);
    margin-top: var(--spacing-xs);
}

.file-error {
    background: var(--error-bg);
    border: 1px solid var(--error);
    padding: var(--spacing-xs) var(--spacing-sm);
    border-radius: var(--radius-sm);
    margin-top: var(--spacing-xs);
    color: var(--error-text);
}

/* Toggle switch (if functionally needed) */
.switch-toggle {
    position: relative;
    display: flex;
    flex-direction: column;
    cursor: pointer;
}

.switch-toggle input[type="checkbox"] {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.switch-slider {
    /* Owner ruling 2026-07-28: "the toggle needs a RED for the NO / deactivated
       toggle appearance to differentiate it". The old OFF state was --neutral
       (=--secondary, a muted green-grey) against an ON state of --primary (deep
       green) — two dark greens, near-indistinguishable. NOTE --error resolves to
       --tertiary, which is ORANGE on this site, so a literal red needs the
       brand-neutral --accent-red hue. State is also carried by knob POSITION
       (::before translateX), so this is not colour-alone signalling (WCAG 1.4.1). */
    width: 50px;
    height: 24px;
    background: var(--accent-red);
    border-radius: 12px;
    position: relative;
    transition: background var(--transition-normal);
}

.switch-slider::before {
    content: '';
    position: absolute;
    width: 20px;
    height: 20px;
    left: 2px;
    top: 2px;
    background: var(--white);
    border-radius: 50%;
    transition: transform var(--transition-normal);
}

.switch-toggle input[type="checkbox"]:checked + .switch-slider {
    /* ON state lifted from --primary to --primary-accent for the same reason as
       the active tab above (owner ruling 2026-07-28: deep green does not read as
       "on"). Gives an unambiguous bright-green / red pair against the OFF state. */
    background: var(--primary-accent);
}

.switch-toggle input[type="checkbox"]:checked + .switch-slider::before {
    transform: translateX(26px);
}

/* Honeypot - truly hidden */
[name="company_name"] {
    position: absolute !important;
    left: -9999px !important;
    width: 1px !important;
    height: 1px !important;
    opacity: 0 !important;
    pointer-events: none !important;
}

/* ========================================================================
   TOMANY - One-to-Many Child Record Cards
   ======================================================================== */

.toMany-container {
    margin: var(--spacing-lg) 0;
}

.toMany-card {
    border: 1px solid var(--border-light);
    border-radius: var(--radius-md);
    background: var(--bg-layer-lighten);
    margin-bottom: var(--spacing-md);
}

.toMany-card > summary {
    padding: var(--spacing-sm) var(--spacing-md);
    font-weight: 500;
    cursor: pointer;
    color: var(--text-on-dark-accent);
}

.toMany-card[open] > summary {
    border-bottom: 1px solid var(--border);
    margin-bottom: 0;
}

.toMany-card-body {
    padding: var(--spacing-md);
}

.toMany-delete-label {
    display: inline-flex !important;
    align-items: center;
    gap: var(--spacing-sm);
    font-weight: normal !important;
    color: var(--error);
    font-size: var(--font-size-sm);
    cursor: pointer;
}

/* Per-card Through-toMany save action + its fire-and-forget sink. The sink is
   normally an empty AJAX target (the save endpoint returns only alerts) and
   takes no space — but if a transport failure ever writes an error into it
   (loadContent's catch), :empty stops matching and the message becomes
   visible instead of silently vanishing into a hidden div. See
   DatabaseTable::renderToManyField() Through mode + setupToManyCardSave(). */
.toMany-card-actions {
    margin-top: var(--spacing-md);
}

.through-card-sink:empty {
    display: none;
}

.toMany-delete-label input[type="checkbox"] {
    width: auto;
}

/* ========================================================================
   SEARCHDB - Autocomplete Wrapper
   ======================================================================== */

.search-db-wrapper {
    position: relative;
}

.search-db-wrapper .autoFetchOptions {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--white);
    border: 1px solid var(--border-light);
    border-top: none;
    max-height: 200px;
    overflow-y: auto;
    z-index: 1000;
    border-radius: 0 0 var(--radius-sm) var(--radius-sm);
    box-shadow: var(--shadow-md);
}

/* ========================================================================
   TOGGLE FIELDSET - One-to-One with toggle in legend
   ======================================================================== */

fieldset[data-toggle-group] > legend {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
}

fieldset[data-toggle-group] > legend .switch-toggle {
    flex-direction: row;
    align-items: center;
    gap: var(--spacing-sm);
}

/* The flag control comes from createFormField (one renderer, so Visible=/Edit=
   gate it like any other field), which wraps it in .form-group. Inside a legend
   that wrapper is a control in a heading, not a stacked form row - so the row
   spacing and the form-label typography are both wrong here. */
fieldset[data-toggle-group] > legend .form-group {
    margin-bottom: 0;
}

fieldset[data-toggle-group] > legend .form-group label {
    font: inherit;
    color: inherit;
    margin-bottom: 0;
}

.toggle-related-content {
    padding-top: var(--spacing-sm);
}


/* ════════════════════════════════════════════════════════════════════════
   MOBILE TOUCH TARGETS — framework-wide standard
   Under a coarse pointer (touch), EVERY interactive control meets the
   --tap-min minimum (44px, Apple HIG / Material). Desktop pointer keeps its
   denser sizing. Zero-specificity baselines + explicit bumps for the known
   compact variants, so nothing falls through.
   ════════════════════════════════════════════════════════════════════════ */

@media (pointer: coarse) {
    :where(button, .button,
           input[type="submit"], input[type="button"], input[type="reset"],
           select,
           input:not([type="checkbox"], [type="radio"], [type="range"], [type="color"], [type="hidden"], [type="image"])) {
        min-height: var(--tap-min);
    }

    /* Compact variants opt back UP to the minimum on touch */
    .button.compact,
    .btn-small,
    .tab-nav button {
        min-height: var(--tap-min);
    }

    /* Picker options and collapsible grid rows are tap targets too
       (till audit 2026-09-30: both measured under 44px). */
    .autofetch-option,
    .row-grid details > summary {
        min-height: var(--tap-min);
    }
    .autofetch-option {
        display: flex;
        align-items: center;
    }

    .refresh-button {
        width: var(--tap-min);
        height: var(--tap-min);
        min-height: var(--tap-min);
    }

    /* Checkboxes / radios: bigger hit area on touch */
    :where(input[type="checkbox"], input[type="radio"]) {
        width: 1.3rem;
        height: 1.3rem;
    }
}

/* ════════════════════════════════════════════════════════════════════════
   MOBILE-RESPONSIVE ADMIN — Global patterns for all admin sections
   Added to ensure every form, table, and interactive block works on mobile.
   ════════════════════════════════════════════════════════════════════════ */

/* ────────────────────────────────────────────────────────────────────────
   TABLE RESPONSIVE WRAPPERS
   Tables inside these wrappers scroll horizontally on narrow screens without
   triggering browser back-swipe (which requires touch-action handling).
   JS (initTableResponsive) auto-wraps unhandled tables with .js-table-scroll-wrap.
   ─────────────────────────────────────────────────────────────────────── */

.table-responsive,
.table-container,
.js-table-scroll-wrap {
    width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    border-radius: var(--radius-sm);
}

.table-responsive > table,
.table-container > table,
.js-table-scroll-wrap > table {
    min-width: 100%;
}

@media (max-width: 768px) {
    .table-responsive > table,
    .table-container > table,
    .js-table-scroll-wrap > table {
        white-space: nowrap;
    }
}

/* ────────────────────────────────────────────────────────────────────────
   FIELD ROW — THE multi-field row primitive (framework standard)
   One flex row of fields that wraps on desktop and stacks on phones.
   Use class="field-row". Legacy names (.form-row-inline, .dynamic-field-row,
   .pos-field-row) are ALIASES of this primitive during migration — do not
   write new markup with them. Width modifiers on the child .form-group:
     .flex-2      wider field (memo, description)
     .flex-narrow / .flex-auto  intrinsic width (checkbox, button)
     .full-width  own line
     .flex-qty / .flex-price / .flex-gc   POS-calibrated widths
   ONE stack breakpoint for every row: 640px.
   ─────────────────────────────────────────────────────────────────────── */

.field-row,
.form-row-inline,
.dynamic-field-row,
.pos-field-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-sm);
    align-items: flex-end;
}

/* Row decoration that stays specific to the dynamic (repeating-line) variant */
.dynamic-field-row {
    padding: var(--spacing-sm) 0;
    border-bottom: 1px solid var(--border-light);
}

:where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group {
    flex: 1 1 120px;
    margin-bottom: 0;
}

:where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group.flex-2 {
    flex: 2 1 200px;
}

:where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group.flex-narrow,
:where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group.flex-auto {
    flex: 0 0 auto;
}

:where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group.full-width {
    flex: 1 1 100%;
}

:where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group.flex-qty   { flex: 0 1 80px;  }
:where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group.flex-price { flex: 1 1 130px; }
:where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group.flex-gc    { flex: 1 1 160px; }

@media (max-width: 640px) {
    .field-row,
    .form-row-inline,
    .dynamic-field-row,
    .pos-field-row {
        flex-direction: column;
        align-items: stretch;
        gap: var(--spacing-xs);
    }

    :where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group,
    :where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group.flex-2,
    :where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group.flex-narrow,
    :where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group.flex-auto,
    :where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group.flex-qty,
    :where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group.flex-price,
    :where(.field-row, .form-row-inline, .dynamic-field-row, .pos-field-row) .form-group.flex-gc {
        flex: 1 1 auto;
        width: 100%;
    }
}

/* ────────────────────────────────────────────────────────────────────────
   TAB NAVIGATION
   Horizontally-scrollable tab bar — tabs never wrap to a second line,
   scroll instead on narrow screens.
   ─────────────────────────────────────────────────────────────────────── */

.tab-nav {
    display: flex;
    flex-wrap: nowrap;
    gap: var(--spacing-xs);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    padding-bottom: var(--spacing-xs);
    margin-bottom: var(--spacing-md);
    scrollbar-width: thin;
    scrollbar-color: var(--border-light) transparent;
}

.tab-nav::-webkit-scrollbar {
    height: 4px;
}

.tab-nav::-webkit-scrollbar-track {
    background: transparent;
}

.tab-nav::-webkit-scrollbar-thumb {
    background-color: var(--border-light);
    border-radius: var(--radius-sm);
}

/* Forms inside .tab-nav become transparent to the flex layout so buttons
   sit directly in the flex row. !important overrides style="display:inline" */
.tab-nav form {
    display: contents !important;
}

.tab-nav button {
    flex-shrink: 0;
    padding: var(--spacing-xs) var(--spacing-md);
    min-height: 38px;
    background: var(--surface-light);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: var(--font-size-sm);
    white-space: nowrap;
    color: var(--text-on-dark);
    transition: background 0.15s, border-color 0.15s;
}

.tab-nav button.active {
    /* Owner ruling 2026-07-28: "a dark green tab does NOT indicate ACTIVE to me,
       a bright green one does". --primary is the deep brand shade (near the page
       surface on dark themes); --primary-accent is the site's BRIGHT accent (the
       same hue that carries links and the <summary> +/- cue), so an active tab
       now reads as lit rather than merely filled. Paired text token per §4. */
    background: var(--primary-accent);
    border-color: var(--primary-accent);
    color: var(--primary-accent-text);
    font-weight: 600;
}

/* Visible keyboard focus for the tab strip (incl. the §2.8 tabbed-dispatch
   stage, which is arrow-key navigable). Applies to all .tab-nav consumers. */
.tab-nav button:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
}

.tab-nav button:hover:not(.active) {
    background: var(--border-light);
}

.tab-content {
    min-height: 60px;
}

/* ────────────────────────────────────────────────────────────────────────
   BADGES — Inline status / type indicators
   ─────────────────────────────────────────────────────────────────────── */

.badge {
    display: inline-flex;
    align-items: center;
    padding: 2px 8px;
    border-radius: 99px;
    font-size: 0.75em;
    font-weight: 600;
    line-height: 1.4;
    white-space: nowrap;
    vertical-align: middle;
}

/* Badge state colours ride the semantic token families (one home, 2026-09-25;
   was Bootstrap-derived literal hex that ignored every site's theme). */
.badge-success { background: color-mix(in srgb, var(--success) 25%, var(--surface-light)); color: var(--success-bright); }
.badge-danger  { background: color-mix(in srgb, var(--error) 25%, var(--surface-light));   color: var(--error-bright); }
.badge-warning { background: color-mix(in srgb, var(--warning) 25%, var(--surface-light)); color: var(--warning-bright); }
.badge-info    { background: color-mix(in srgb, var(--info) 25%, var(--surface-light));    color: var(--info-bright, var(--info)); }
.badge-neutral { background: var(--surface-light); color: var(--text-on-dark-secondary); border: 1px solid var(--border-light); }

/* ────────────────────────────────────────────────────────────────────────
   GL FINANCIAL STATEMENT REPORTS (gl-reports.php)
   Shared accents for the statement suite: balance-tie verdicts, subtotal /
   total rows, and attention-row highlighting on the Outstanding dashboard.
   ──────────────────────────────────────────────────────────────────────── */
.report-balanced   { color: #155724; font-weight: 600; }
.report-imbalanced { color: #721c24; font-weight: 600; }

table.striped tbody tr.report-subtotal td,
table.striped tr.report-subtotal th {
    border-top: 2px solid var(--border-light);
    font-weight: 700;
}

table.striped tbody tr.report-alert-row td {
    background: #f8d7da;
    color: #721c24;
}

/* Period-selection form sits above each report; spacing only, layout reuses
   .form-row-inline / .form-group from the forms section. */
.report-period-form { margin-bottom: var(--spacing-sm); }

/* Month Review report (gl-reports.php rptMonthReview). Two-line account
   summaries inside the baseline <details>/<summary> disclosure pattern;
   green/red month totals reuse .text-success/.text-error. */
.mr-head { display: flex; flex-direction: column; gap: 0.15rem; flex: 1; min-width: 0; }
.mr-row { display: flex; justify-content: space-between; align-items: baseline; gap: 0.75rem; }
.mr-row > span:first-child { min-width: 0; overflow-wrap: anywhere; }
.mr-meta { font-size: 0.8em; font-weight: 400; opacity: 0.8; }
.mr-amt { font-variant-numeric: tabular-nums; white-space: nowrap; }
.mr-flag {
    background: var(--warning-bg);
    color: var(--warning-text);
    font-size: 0.7em;
    font-weight: 700;
    letter-spacing: 0.05em;
    padding: 0.1rem 0.5rem;
    border-radius: 999px;
    white-space: nowrap;
}
.mr-group {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.75rem;
    margin: var(--spacing-md) 0 var(--spacing-sm);
    border-bottom: 2px solid currentColor;
}
.mr-group h4 { margin: 0; text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.8em; }
.mr-none { padding: 0 1rem 1rem; font-style: italic; }
details.mr-acct > table.striped { margin: 0; width: 100%; }

/* Card deposits: each batch day as the Moneris close-out prints it (books/deposits.php renderDepositCloseouts).
   Mobile first: one column of three-column tables; wider screens set the card types side by side. */
details.closeouts > summary { cursor: pointer; }
section.closeout { margin: var(--spacing-md) 0; padding-top: var(--spacing-sm); border-top: 1px solid var(--border-light); }
section.closeout h4 { margin: 0 0 var(--spacing-xs); font-size: var(--font-size-md); }
section.closeout h5 { margin: var(--spacing-md) 0 var(--spacing-xs); font-size: var(--font-size-sm); }
section.closeout table { width: 100%; margin: 0 0 var(--spacing-sm); }
section.closeout caption { text-align: left; font-weight: 600; padding: var(--spacing-xs) 0; }
section.closeout .closeout-cards { display: grid; gap: var(--spacing-sm); grid-template-columns: 1fr; }
@media (min-width: 48em) {
    section.closeout > table { max-width: 32rem; }
    section.closeout .closeout-cards { grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); }
}

/* ────────────────────────────────────────────────────────────────────────
   AGREEMENTS MODULE (admin endpoints + member My Agreements page)
   ──────────────────────────────────────────────────────────────────────── */
.agreement-body {
    /* Fixed 2026-08-05: previously consumed the EXTINCT --color-* vocabulary
       (silently resolved to nothing). Current tokens only. */
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--spacing-md);
    margin: var(--spacing-md) 0;
    max-height: 60vh;
    overflow-y: auto;
    background: var(--surface-base);
    line-height: 1.55;
}

@media print {
    .agreement-body { max-height: none; overflow: visible; border: none; }
}

.agreement-card { margin-bottom: var(--spacing-lg); }

.agreement-action-required {
    border-left: 4px solid var(--warning);
    padding: var(--spacing-sm) var(--spacing-md);
    margin-bottom: var(--spacing-md);
}

.agreement-accept-form { margin-top: var(--spacing-md); }

.agreement-toggle label {
    display: flex;
    align-items: flex-start;
    gap: 0.7rem;
    cursor: pointer;
}

.agreement-toggle input[type="checkbox"] {
    margin-top: 0.2rem;
    width: 1.2rem;
    height: 1.2rem;
    flex-shrink: 0;
}

.agreement-name-warn { color: var(--warning); }

/* Wet-ink signing sheet (printable): one block per unsigned party. */
.agreement-sign-block {
    margin: var(--spacing-lg) 0;
    max-width: 28rem;
    break-inside: avoid;
}

.agreement-sign-line {
    border-bottom: 1px solid currentColor;
    padding: 1.4rem 0 0.2rem 0;
    margin-bottom: var(--spacing-sm);
}

/* ────────────────────────────────────────────────────────────────────────
   PAGE HEADER + MANAGEMENT BLOCK — Mobile compact
   ─────────────────────────────────────────────────────────────────────── */

@media (max-width: 768px) {
    .page-header {
        padding-top: var(--spacing-sm);
        padding-bottom: var(--spacing-sm);
    }

    .page-header h2 {
        font-size: 1.25rem;
    }

    .management-block {
        padding: var(--spacing-sm);
    }
}

/* DYNAMIC FIELD ROWS — consolidated into the FIELD ROW primitive above
   (.dynamic-field-row is now an alias; its repeating-line decoration rides
   with the alias rule). */

/* ════════════════════════════════════════════════════════════════════════
   POS INTERFACE — Point of Sale
   Layout inspired by Square / Shopify POS:
     - Large tap targets (≥ 44px, Apple HIG / Material minimum)
     - Scrollable item and payment tables (no sideways swipe on the page)
     - Stacked field rows on mobile
     - Prominent totals
   ════════════════════════════════════════════════════════════════════════ */

/* POS page header bar */
.pos-header {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-sm);
    align-items: baseline;
    margin-bottom: var(--spacing-md);
}

.pos-header h3 {
    margin: 0;
    flex: 1 1 auto;
}

/* ── Scrollable data tables ─────────────────────────────────────────── */

.line-items-list {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    margin-bottom: var(--spacing-md);
    border-radius: var(--radius-sm);
}

.line-items-list table {
    min-width: 600px;
}

/* The cart is a .stack-table: under 640px each line is a labelled card, so a
   sale never scrolls sideways (till audit 2026-09-30). */
@media (max-width: 640px) {
    .line-items-list .stack-table {
        min-width: 0;
    }
}

/* Cart fields stay at 16px (no iOS focus zoom): the element baseline
   `input, select, textarea, button { font: inherit }` outranks the
   zero-specificity field rule, so inside the compact table they would
   inherit its smaller size (till audit 2026-09-30). */
.line-items-list :where(input, select) {
    font-size: var(--font-size-md);
}

/* Cart fields sized to what they hold (owner 2026-10-07): Qty 3 digits,
   Price 5 digits and cents, the discount type and its amount side by side.
   The width they free goes to Item and For (the table lays itself out). */
.line-items-list input[name$="[quantity]"] {
    width: 5em;
}

.line-items-list input[name$="[unitPrice]"] {
    width: 7.5em;
}

.line-items-list select[name$="[discountType]"] {
    width: 6em;
}

.line-items-list input[name$="[discountValue]"] {
    width: 6em;
}

.line-items-list td:has(> select[name$="[discountType]"]) {
    white-space: nowrap;
}

/* Client context at the till (2026-09-25): the recent-visits and notes
   lists cap their height and scroll inside, so a long history never takes
   the ticket over. */
.pos-client-scroll {
    max-height: 40vh;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
}

.existing-payments {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    margin-bottom: var(--spacing-md);
    border-radius: var(--radius-sm);
}

.existing-payments table {
    min-width: 440px;
}

/* ── Invoice / payment totals summary ──────────────────────────────── */

.invoice-summary,
.payment-totals {
    background: var(--surface-light);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-sm);
    padding: var(--spacing-sm) var(--spacing-md);
    margin-bottom: var(--spacing-md);
}

.invoice-summary table,
.payment-totals table {
    width: 100%;
    font-size: var(--font-size-sm);
}

.invoice-summary td:last-child,
.payment-totals td:last-child {
    text-align: right;
    font-weight: 600;
}

/* ── Add-item and add-payment form panels ──────────────────────────── */

.add-item-form,
.add-payment-form {
    background: var(--surface-light);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-sm);
    padding: var(--spacing-md);
    margin-bottom: var(--spacing-md);
}

/* The ticket's stage headings (Step 1 - Client, Step 2 - Items, Step 3 -
   Payment; owner 2026-09-30): each opens its panel. */
.pos-step { margin: 0 0 var(--spacing-sm); }

/* ── POS field row ─────────────────────────────────────────────────── */
/* .pos-field-row is an ALIAS of the FIELD ROW primitive (see FIELD ROW
   section) — layout, width modifiers, and the 640px stack all live there. */

/* ── Sell Gift Certificate panel ───────────────────────────────────── */

.sell-gc-form {
    background: var(--surface-light);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-sm);
    padding: var(--spacing-md);
    margin-bottom: var(--spacing-md);
}

.sell-gc-form h4 {
    margin-top: 0;
    margin-bottom: var(--spacing-sm);
}

/* ── Tax exempt toggle ─────────────────────────────────────────────── */

.tax-exempt-section {
    padding: var(--spacing-sm) 0;
    border-top: 1px solid var(--border-light);
    margin-bottom: var(--spacing-md);
}

.tax-exempt-section .pos-field-row {
    align-items: center;
}

.tax-exempt-section label {
    display: inline-flex !important;
    align-items: center;
    gap: var(--spacing-xs);
    white-space: nowrap;
    cursor: pointer;
}

.tax-exempt-section label input[type="checkbox"] {
    width: auto;
    flex-shrink: 0;
}

/* ── POS completion / receipt screen ───────────────────────────────── */

.pos-complete,
.completion-message {
    text-align: center;
    padding: var(--spacing-xl) var(--spacing-md);
}

/* ── Button groups (POS action row + general) ──────────────────────── */
/* Base .button-group flex layout lives in FUNCTIONAL CLASSES above —
   duplicate definition removed 2026-08-05. */

/* Forms inside button-group become transparent to flex layout.
   !important overrides style="display:inline" on the form tags. */
.button-group form {
    display: contents !important;
}

/* Keep an action cluster on a single line (no wrap). Used for per-row action
   buttons in results tables (e.g. the Universal Finder's Edit / Void column):
   the cell grows to fit the buttons side-by-side and the surrounding
   .table-container scrolls horizontally if the row is genuinely too narrow,
   instead of stacking the buttons vertically. */
.button-group--nowrap {
    flex-wrap: nowrap;
}

/* Menu-choice button blocks (makeSmartNestedForm buttons mode, auto-picked
   for <=6 flat options): options fill the row evenly — tap once, not
   tap-select-tap. Forms are display:contents (rule above), so the buttons
   are the flex items; equal flex-basis 0 shares the row equally and the
   min-width floor wraps gracefully on narrow screens. */
.button-menu > form > .button {
    flex: 1 1 0;
    min-width: 140px;
    min-height: 44px;
}

/* ── Mobile POS adjustments ────────────────────────────────────────── */
/* Field-row stacking now comes from the FIELD ROW primitive (640px) and
   tap-target minimums from the MOBILE TOUCH TARGETS standard — the old
   POS-only 768px overrides are retired. */

/* ════════════════════════════════════════════════════════════════════════
   AUTO-WRAPPED TABLE CONTAINERS
   JS (initTableResponsive) wraps unhandled tables with this class. Scroll
   behavior comes from the shared TABLE RESPONSIVE WRAPPERS rule; only the
   margin below is specific to the auto-wrapped variant.
   ════════════════════════════════════════════════════════════════════════ */

.js-table-scroll-wrap {
    margin-bottom: var(--spacing-sm);
}

/* ════════════════════════════════════════════════════════════════════════
   PRINT OVERRIDES
   Restore normal table rendering for print / PDF export.
   ════════════════════════════════════════════════════════════════════════ */

@media print {
    .table-responsive,
    .table-container,
    .js-table-scroll-wrap,
    .line-items-list,
    .existing-payments {
        overflow: visible !important;
    }

    .table-responsive > table,
    .table-container > table,
    .js-table-scroll-wrap > table,
    .line-items-list > table,
    .existing-payments > table {
        min-width: 0 !important;
        white-space: normal !important;
        width: 100% !important;
    }

    .tab-nav,
    .button-group,
    .no-print {
        display: none !important;
    }
}

/* ────────────────────────────────────────────────────────────────────────
   TIME IN PROGRESS INDICATOR
   Open / unclosed time records show "In progress" in bright green so they
   stand out immediately in any time report or management view.
   ─────────────────────────────────────────────────────────────────────── */

.time-in-progress {
    color: var(--success-bright);
    font-weight: 700;
    white-space: nowrap;
}

/* ════════════════════════════════════════════════════════════════════════
   RECEIPT / FILE IMAGE THUMBNAILS
   Used by:
     - DatabaseTable.php Input=file render (form-side .file-thumbnail)
     - helpers.php renderReceiptCell() (listing-side .receipt-image with
       size modifiers --thumb, --medium, --large)
   All click-through to full size via anchor wrap (cursor:zoom-in signals
   the affordance). loading="lazy" on each img keeps long lists fast.
   ─────────────────────────────────────────────────────────────────────── */

/* Form-side: file-thumbnail wraps an anchor + img inside a file input field. */
.file-thumbnail {
    margin-top: 0.5rem;
}

.file-thumbnail img {
    border: 1px solid var(--border-light);
    border-radius: var(--radius-sm);
    cursor: zoom-in;
    display: block;
}

/* Listing-side: receipt-image is the base class with size modifiers. */
.receipt-image img {
    cursor: zoom-in;
    display: inline-block;
}

.receipt-image--thumb img {
    max-height: 40px;
    max-width: 60px;
    object-fit: cover;
}

.receipt-image--medium img {
    max-width: 200px;
    max-height: 200px;
    margin-top: 0.5rem;
}

.receipt-image--large img {
    max-width: 300px;
    max-height: 300px;
    margin-top: 0.5rem;
    border: 1px solid var(--border-light);
}

/* ════════════════════════════════════════════════════════════════════════
   HEIC CONVERSION STATUS INDICATOR
   Used by enhanceFileInputs() in universal.js — appended next to a file
   input while a HEIC photo is being decoded to JPEG client-side.
   Styling consumes canonical tokens so each site paints in its own theme.
   ─────────────────────────────────────────────────────────────────────── */

.heic-status-indicator {
    display: inline-block;
    margin-left: 0.5rem;
    font-size: var(--font-size-sm);
    color: var(--info);
}

.heic-status-indicator--error {
    color: var(--error);
}

/* Receipt missing-from-disk placeholder. Used by renderReceiptCell() when
   the DB row references a file that's no longer on disk (lost backup,
   deployment wipe, etc.). Tells the user the truth instead of showing a
   broken <img>. */
.receipt-missing {
    display: inline-block;
    padding: 0.125rem 0.5rem;
    font-size: var(--font-size-sm);
    font-style: italic;
    color: var(--warning-dark);
    background: var(--warning-bg);
    border-radius: var(--radius-sm);
}

/* ========================================================================
   CURRENCY INPUT WRAPPER + ADDING-MACHINE CALCULATOR WIDGET
   ────────────────────────────────────────────────────────────────────────
   - Inputs marked data-currency="true" are auto-wrapped by JS in
     .currency-input-wrapper, which adds a $ prefix (CSS) and a calculator
     icon button to the right.
   - Click the icon -> .calc-widget overlay opens, anchored beside the input
     on desktop, docked to the bottom on mobile.
   - All colors come from per-site theme tokens; this stays color-literal-free.
   ======================================================================== */

.currency-input-wrapper {
    position: relative;
    display: inline-flex;
    align-items: stretch;
    width: 100%;
}
.currency-input-wrapper > input {
    flex: 1 1 auto;
    padding-left: 1.6rem;
    padding-right: 2.2rem;
}
.currency-input-wrapper::before {
    content: '$';
    position: absolute;
    left: 0.55rem;
    top: 50%;
    transform: translateY(-50%);
    pointer-events: none;
    opacity: 0.7;
    font-size: 0.95em;
    /* Sits ON the input's surface — pair with the field tokens, never the
       ambient text color (which can mismatch the field bg in themed panels). */
    color: var(--field-text);
}
.currency-calc-trigger {
    position: absolute;
    right: 0.25rem;
    top: 50%;
    transform: translateY(-50%);
    background: transparent;
    border: 0;
    padding: 0.25rem;
    margin: 0;
    cursor: pointer;
    /* Icon sits ON the input's surface — pair with the field tokens, never
       inherit the ambient text color (mismatches the field bg in themed panels). */
    color: var(--field-text);
    opacity: 0.65;
    display: inline-flex;
    align-items: center;
    line-height: 1;
}
.currency-calc-trigger:hover {
    opacity: 1;
    color: var(--primary);
}
.currency-calc-trigger:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
    border-radius: 3px;
    opacity: 1;
}

.calc-widget {
    position: absolute;
    z-index: 9999;
    width: 280px;
    background: var(--surface-base);
    color: var(--surface-text); /* paired token, not ambient inherit */
    border: 1px solid var(--border);
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
    display: none;
    flex-direction: column;
    overflow: hidden;
    user-select: none;
}
.calc-widget.open {
    display: flex;
}
.calc-widget.mobile {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    max-width: 100%;
    max-height: 80vh;
    border-radius: 12px 12px 0 0;
}

.calc-tape {
    background: var(--surface-light);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 0.85rem;
    padding: 0.5rem 0.75rem;
    max-height: 8rem;
    min-height: 2.5rem;
    overflow-y: auto;
    border-bottom: 1px solid var(--border-light);
    opacity: 0.85;
}
.calc-widget.mobile .calc-tape {
    max-height: 28vh;
}
.calc-tape-row {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    line-height: 1.4;
}
.calc-tape-op {
    width: 1.2em;
    text-align: center;
    opacity: 0.75;
}
.calc-tape-val {
    text-align: right;
    flex: 1;
}
.calc-tape-total {
    border-top: 1px dashed var(--border);
    margin-top: 0.25rem;
    padding-top: 0.25rem;
    font-weight: 600;
}

.calc-display {
    background: var(--surface-base);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 1.6rem;
    text-align: right;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--border-light);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.calc-keys {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 1px;
    background: var(--border-light);
}

.calc-key {
    background: var(--surface-base);
    color: inherit;
    border: 0;
    padding: 0.85rem 0;
    font-size: 1.15rem;
    cursor: pointer;
    min-height: 48px;
    font-family: inherit;
    transition: background 0.1s ease;
    touch-action: manipulation; /* disables iOS double-tap zoom on the keys */
}
.calc-key:hover {
    background: var(--surface-light);
}
.calc-key:active {
    background: var(--surface-lighter);
}
.calc-key:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: -2px;
}
.calc-key-fn {
    background: var(--surface-light);
    font-size: 1rem;
}
.calc-key-op {
    background: var(--surface-lighter);
    color: inherit;
    font-weight: 700;
    font-size: 1.3rem;
}
.calc-key-eq {
    background: var(--primary);
    color: var(--primary-text);
    grid-row: span 2;
    font-weight: 600;
    font-size: 1.05rem;
}
.calc-key-eq:hover {
    background: var(--primary-bright);
}
.calc-key-zero {
    grid-column: span 2;
}

/* ────────────────────────────────────────────────────────────────────
   Stage bar (Input=stageBar): ordered single-state stepper.
   Horizontal track of tappable dots, one per option in declared order.
   Current = filled + enlarged, past = muted fill, future = hollow.
   Steps are <button type="button"> for native tap/keyboard handling —
   explicit resets below override the element-level button baseline.
   Tap targets sized for touch (44px min). Rendered by
   DatabaseTable::renderStageBar(); behavior in universal.js
   setupStageBars(). See framework-backend.md §"Input=stageBar".
   ──────────────────────────────────────────────────────────────── */
.stage-bar {
    margin: var(--spacing-sm) 0;
}

.stage-bar-track {
    /* Content-sized, NOT full width (owner 2026-08-08): in a form the bar
       should be exactly big enough for its labelled steps — the old
       full-width stretch spread four dots across the whole fieldset. */
    display: inline-flex;
    align-items: flex-start;
    position: relative;
}

/* Connecting line behind the dots — mixed from the SECONDARY TEXT token,
   not the border token: borders sit near the surface on dark themes and
   the rail vanished (owner contrast note 2026-08-08). */
.stage-bar-track::before {
    content: '';
    position: absolute;
    top: 15px;
    left: 10%;
    right: 10%;
    height: 2px;
    background: color-mix(in srgb, var(--text-on-dark-secondary, #999) 55%, transparent);
}

.stage-bar-step {
    /* Anchor for the absolutely-positioned who/when caption */
    position: relative;
    /* Reset the element-level button baseline */
    background: transparent;
    padding: var(--spacing-xs) var(--spacing-sm);
    border-radius: var(--radius-sm);
    color: var(--text-on-dark);
    font-size: var(--font-size-sm);
    font-weight: 400;

    /* Steps size to their LABELS (min floor keeps short ones tappable and
       the rhythm even) instead of sharing the container's full width. */
    flex: 0 1 auto;
    min-width: 5.5rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.3rem;
    min-height: 44px;   /* touch target */
    position: relative; /* sits above the track line */
    cursor: pointer;
}

.stage-bar-step:hover {
    background: var(--bg-layer-lighten, transparent);
    transform: none;
}

.stage-bar-dot {
    width: 16px;
    height: 16px;
    border-radius: 50%;
    /* Hollow (future) dots outline in secondary-text strength so every
       stage is visible before it's reached (contrast note 2026-08-08). */
    border: 2px solid var(--text-on-dark-secondary, var(--neutral));
    background: var(--surface-base);
    display: block;
}

.stage-bar-step.is-past .stage-bar-dot {
    background: var(--text-on-dark-secondary, var(--neutral));
    border-color: transparent;
}

.stage-bar-step.is-current .stage-bar-dot {
    width: 20px;
    height: 20px;
    margin-top: -2px;
    background: var(--primary);
    border-color: var(--primary);
}

.stage-bar-step.is-current .stage-bar-step-label {
    font-weight: 700;
    color: var(--primary-accent);
}

.stage-bar-meta {
    /* The who/when caption must NOT participate in the step button's width:
       when the first real stage taps landed (2026-08-12), the caption
       inflated the current step to caption-width, wrecking the bar's even
       distribution and pushing the connector past the end dots. Absolutely
       positioned under its own dot instead; the track's bottom margin below
       reserves the line it occupies so it can't overlap following content. */
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    width: max-content;
    max-width: 16rem;
    font-size: var(--font-size-xs);
    opacity: 0.8;
    line-height: 1.2;
    text-align: center;
}

.stage-bar-track {
    margin-bottom: 1.4em;   /* reserved room for the absolute caption */
}

/* Stage bar on phones (owner design 2026-08-11): five labelled stops
   overflow a phone-width card, so labels STAGGER — odd steps label ABOVE
   the track, even steps BELOW, each label centered on its own dot. Every
   stage stays readable BEFORE tapping, which matters because a tap on this
   bar COMMITS the stage (autosave): hiding labels would turn discovery
   into accidental writes. Each step becomes a 3-row grid (label / dot /
   label) with BOTH label rows reserved on every step, so the dots stay on
   one aligned line and the connector can sit at the track's center. The
   who/when meta caption is clipped (not display:none — screen readers
   keep it); phones get the state, the audit detail lives on desktop. */
@media (max-width: 480px) {
    .stage-bar-step {
        display: grid;
        grid-template-rows: minmax(1.2em, auto) auto minmax(1.2em, auto);
        justify-items: center;
        align-items: center;
        row-gap: 2px;
        min-width: 44px;
        padding: var(--spacing-xs) 2px;
    }
    .stage-bar-dot { grid-row: 2; }
    .stage-bar-step-label {
        grid-row: 3;
        font-size: 0.72rem;
        line-height: 1.2;
        white-space: nowrap;
    }
    .stage-bar-step:nth-child(odd) .stage-bar-step-label { grid-row: 1; }
    .stage-bar-step .stage-bar-meta {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }
    /* Dots sit at the vertical center of the uniform step grid, so the
       connector follows them there (was a fixed 15px tuned to top-dots). */
    .stage-bar-track::before {
        top: calc(50% - 1px);
    }
}

/* ===== UNIVERSAL FINDER ===== */
/* Filter form emitted by DatabaseTable::makeFilterFields(). The .form-group
   inputs inherit the standard element-level form styling above; only the
   from/to range pairing needs its own layout rule. */

.finder-filters {
    margin-bottom: var(--spacing-md);
}

.filter-range {
    display: flex;
    gap: var(--spacing-sm);
    align-items: center;
}

.filter-range input {
    flex: 1 1 0;
    min-width: 0;
}

/* Daily Tasks - completed cards stay visible but dimmed (still correctable). */
.task-card-complete {
    opacity: 0.55;
}


/* ═══ Framework chart primitive (chart-render.js) ══════════════════════════
   <div class="framework-chart" data-chart='{json}'> — server emits data only,
   chart-render.js draws the SVG from measured width. Series palette is themable
   per site by overriding --chart-series-N. Identical ×5 (manifest FRAMEWORK). */
.framework-chart {
    position: relative;
    width: 100%;
    margin: var(--spacing-md, 1rem) 0;
    --chart-series-1: #2c7be5;
    --chart-series-2: #d9534f;
    --chart-series-3: #28a745;
    --chart-series-4: #f0ad4e;
    --chart-series-5: #6f42c1;
    --chart-series-6: #17a2b8;
    --chart-series-7: #e83e8c;
    --chart-series-8: #6c757d;
}

.framework-chart svg {
    display: block;
    width: 100%;
    height: auto;
}

.chart-grid-line {
    stroke: var(--border-light, #e3e3e3);
    stroke-width: 1;
}

.chart-zero-line {
    stroke: var(--border, #999);
    stroke-width: 1;
}

.chart-axis-label {
    fill: var(--neutral-text, #666);
    font-size: 11px;
}

.chart-hover-guide {
    stroke: var(--border, #999);
    stroke-width: 1;
    stroke-dasharray: 2 3;
}

.chart-legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-xs, 0.25rem) var(--spacing-md, 1rem);
    font-size: var(--font-size-sm, 0.875rem);
    margin-bottom: var(--spacing-xs, 0.25rem);
}

.chart-legend-item {
    display: inline-flex;
    align-items: center;
    gap: 0.35em;
}

.chart-legend-dash {
    opacity: 0.75;
    font-style: italic;
}

.chart-swatch {
    display: inline-block;
    width: 0.75em;
    height: 0.75em;
    border-radius: 2px;
    background: var(--swatch-color, currentColor);
    flex: 0 0 auto;
}

.chart-legend-bucket {
    font-weight: 600;
}

.chart-legend-bucket:empty {
    display: none;
}

.chart-legend-val {
    font-variant-numeric: tabular-nums;
    opacity: 0.85;
}

.chart-legend-val:not(:empty)::before {
    content: '·';
    margin: 0 0.3em;
}


/* Time-lane chart (chart-render.js type 'lanes'): lanes of bands and marks
   on one time axis, with zoom, pan, a jump list and lane toggles. Geometry
   rides the --x / --w / --row custom properties the script sets; each lane
   carries its --tone (an --accent-* token) and the paired --tone-text.
   Fills: solid, hatched, dashed (an idea), outline; --conflict underlines.
   Rows are dense for a fine pointer; a coarse (touch) pointer gets rows at
   the framework's touch standard, --tap-min (PD ruling 2026-10-03). */
.framework-chart.lanes-chart {
    --lane-band-h: 26px;
    --lane-row: 30px;
    --lane-mark-row: 22px;
    --lane-axis-mark-w: 6px;
    display: grid;
    gap: 0.5rem;
}

@media (pointer: coarse) {
    .framework-chart.lanes-chart {
        --lane-band-h: var(--tap-min, 44px);
        --lane-row: calc(var(--tap-min, 44px) + 4px);
        --lane-mark-row: var(--tap-min, 44px);
        --lane-axis-mark-w: 12px;
    }
}

.lanes-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem 0.75rem;
}

.lanes-controls .button {
    padding: 0.3rem 0.75rem;
}

.lanes-controls select {
    width: auto;
    max-width: 100%;
}

.lanes-toggles {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem 0.75rem;
}

.lanes-toggles label {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    font-size: var(--font-size-sm);
}

.lanes-plot {
    position: relative;
    overflow: hidden;
    touch-action: pan-y;
    user-select: none;
    cursor: grab;
    border: 1px solid var(--border);
    border-radius: var(--radius-md, 0.5rem);
    background: var(--surface-base);
}

.lanes-plot.lanes-dragging {
    cursor: grabbing;
}

.lanes-plot:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
}

.lanes-axis {
    position: relative;
    height: 1.9rem;
    border-bottom: 1px solid var(--border);
    font-size: 0.72rem;
    color: var(--text-on-dark-secondary);
}

.lanes-tick {
    position: absolute;
    top: 0;
    left: var(--x);
    height: 100%;
    padding: 0.45rem 0 0 0.3rem;
    border-left: 1px solid var(--border);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

.lanes-grid {
    position: absolute;
    inset: 1.9rem 0 0 0;
    pointer-events: none;
}

.lanes-grid span {
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--x);
    border-left: 1px solid var(--border-light);
}

.lanes-lane {
    position: relative;
    border-bottom: 1px solid var(--border);
}

.lanes-lane:last-child {
    border-bottom: 0;
}

.lanes-lane-head {
    position: relative;
    z-index: 2;   /* lane names read above a drawn break */
    padding: 0.4rem 0.6rem 0.1rem;
    font-size: 0.78rem;
    color: var(--text-on-dark-secondary);
}

.lanes-lane-head b {
    color: var(--tone);
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.lanes-lane-body {
    position: relative;
    height: calc(var(--band-rows) * var(--lane-row) + var(--mark-rows) * var(--lane-mark-row) + 10px);
}

.lanes-band,
.lanes-band:hover {
    --band-bg: var(--tone);
    position: absolute;
    left: var(--x);
    top: calc(var(--row) * var(--lane-row) + 4px);
    width: var(--w);
    height: var(--lane-band-h);
    min-height: 0;   /* the row height above is the touch rule; the bare button minimum would stack rows */
    display: block;
    padding: 0 0.45rem;
    border: 1.5px solid var(--tone);
    border-radius: 4px;
    background: var(--band-bg);
    color: var(--tone-text);
    font-size: 0.76rem;
    font-weight: 600;
    line-height: calc(var(--lane-band-h) - 3px);
    text-align: left;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    transform: none;
    transition: none;
}

.lanes-fill-hatched,
.lanes-fill-hatched:hover {
    --band-bg: repeating-linear-gradient(135deg, var(--tone) 0 6px, color-mix(in srgb, var(--tone) 55%, transparent) 6px 12px);
}

.lanes-fill-dashed,
.lanes-fill-dashed:hover {
    --band-bg: transparent;
    border-style: dashed;
    color: var(--surface-text);
}

.lanes-fill-outline,
.lanes-fill-outline:hover {
    --band-bg: transparent;
    color: var(--surface-text);
}

.lanes-band.lanes-conflict {
    box-shadow: inset 0 -4px 0 var(--accent-red);
}

.lanes-band:hover {
    filter: brightness(1.12);
}

.lanes-band:focus-visible,
.lanes-mark:focus-visible,
.lanes-axis-mark:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 1px;
}

.lanes-mark,
.lanes-mark:hover {
    position: absolute;
    left: var(--x);
    top: calc(var(--band-rows) * var(--lane-row) + 6px + var(--row) * var(--lane-mark-row));
    height: var(--lane-mark-row);
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-height: 0;
    padding: 0;
    border: 0;
    background: none;
    color: var(--surface-text);
    font-size: 0.74rem;
    font-weight: 400;
    white-space: nowrap;
    transform: none;
    transition: none;
}

.lanes-mark::before {
    content: "";
    flex: none;
    width: 9px;
    height: 9px;
    transform: rotate(45deg);
    background: var(--tone);
}

.lanes-mark.lanes-conflict {
    color: var(--accent-red);
    font-weight: 600;
}

.lanes-more {
    position: relative;
    z-index: 2;
    margin: 0;
    padding: 0 0.6rem 0.4rem;
    font-size: 0.78rem;
    color: var(--text-on-dark-secondary);
}

/* An axis break between segments (the visitor view's story-present zone):
   a drawn discontinuity, so no distance reads across it. */
.lanes-break {
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--x);
    width: var(--w);
    z-index: 1;
    pointer-events: none;
    border-left: 2px dashed var(--text-on-dark-secondary);
    border-right: 2px dashed var(--text-on-dark-secondary);
    background: repeating-linear-gradient(-60deg, var(--surface-base) 0 5px, var(--border) 5px 7px);
}

/* Series (2026-10-04): the checklist on its own row of the controls, each
   series' tone as a swatch; its line runs under the bands in its --tone.
   Lane groups: a lane inside another indents its name by --depth; the fold
   button sits after the name (touch size from the framework baseline). */
.lanes-series {
    flex-basis: 100%;
}

.lanes-series summary {
    cursor: pointer;
    font-size: var(--font-size-sm);
    font-weight: 600;
}

.lanes-series-acts {
    display: flex;
    gap: 0.4rem;
    margin-bottom: 0.5rem;
}

.lanes-series-list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem 0.75rem;
}

.lanes-series-list label {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    font-size: var(--font-size-sm);
}

.lanes-swatch {
    width: 0.8rem;
    height: 0.8rem;
    border-radius: 2px;
    background: var(--tone);
}

.lanes-plot > .lanes-lines {   /* outranks the chart primitive's svg sizing */
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}

.lanes-lines polyline {
    fill: none;
    stroke: var(--tone);
    stroke-width: 2.5;
    stroke-linejoin: round;
    stroke-linecap: round;
    opacity: 0.85;
}

.lanes-inside .lanes-lane-head {
    padding-left: calc(0.6rem + var(--depth) * 1.1rem);
}

/* Ruler bookmarks (axisMarks, 2026-10-06): a tick button hanging from the
   ruler's bottom edge, --lane-axis-mark-w wide (its hit area; the drawn
   tick is the ::before), a major one taller and in the primary tone. On
   focus its label shows beside it (on hover the title does). */
.lanes-axis-mark,
.lanes-axis-mark:hover {
    position: absolute;
    bottom: 0;
    left: calc(var(--x) - var(--lane-axis-mark-w) / 2);
    width: var(--lane-axis-mark-w);
    height: 0.65rem;
    min-height: 0;   /* the hit area is the mark's width, not the touch baseline: marks thin out instead */
    padding: 0;
    border: 0;
    background: none;
    transform: none;
    transition: none;
    z-index: 2;
}

.lanes-axis-mark::before {
    content: "";
    position: absolute;
    left: calc(50% - 1px);
    bottom: 0;
    width: 2px;
    height: 100%;
    background: var(--text-on-dark-secondary);
}

.lanes-axis-major,
.lanes-axis-major:hover {
    height: 1.15rem;
}

.lanes-axis-major::before {
    left: calc(50% - 1.5px);
    width: 3px;
    background: var(--primary);
}

.lanes-axis-mark:focus {
    z-index: 4;
}

.lanes-axis-mark:focus::after {
    content: attr(aria-label);
    position: absolute;
    top: calc(100% + 2px);
    left: 0;
    padding: 0.1rem 0.4rem;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--surface-base);
    color: var(--surface-text);
    font-size: 0.74rem;
    white-space: nowrap;
}

.lanes-axis-end:focus::after {
    left: auto;
    right: 0;
}

.lanes-fold,
.lanes-fold:hover {
    margin-left: 0.5rem;
    padding: 0.05rem 0.55rem;
    border: 1px solid var(--tone);
    border-radius: 999px;
    background: none;
    color: var(--tone);
    font-size: 0.72rem;
    font-weight: 600;
    transform: none;
}

/* ═══ Account Trends UX: checkbox pickers, year pager, wide-pivot table ═════
   Companions to the chart primitive (chart-render.js). Identical ×5. */
.trend-picker {
    margin: var(--spacing-sm, 0.5rem) 0;
}

.trend-picker summary {
    cursor: pointer;
    font-weight: 600;
}

.picker-filter {
    max-width: 20rem;
    margin: var(--spacing-xs, 0.25rem) 0;
}

.picker-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-md, 1rem);
    max-height: 16rem;
    overflow-y: auto;
    border: 1px solid var(--border-light, #ddd);
    border-radius: 4px;
    padding: var(--spacing-sm, 0.5rem);
}

.picker-group {
    flex: 1 1 14rem;
    min-width: 12rem;
}

.picker-group h5 {
    margin: 0 0 0.25rem 0;
}

.picker-item {
    display: block;
    padding: 0.1rem 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.col-pager {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-xs, 0.25rem);
    margin-top: var(--spacing-sm, 0.5rem);
}

.col-pager .col-pager-active {
    outline: 2px solid var(--primary, #2c7be5);
    outline-offset: -1px;
}

/* Pager buttons are touch-sized everywhere (Q70); the narrow mode's
   previous / next sit either side of the column's label. */
.col-pager .button {
    min-height: var(--tap-min);
}

.col-pager-years,
.col-pager-step {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--spacing-xs, 0.25rem);
}

.col-pager-step .button {
    min-width: var(--tap-min);
}

.col-pager-label {
    min-width: 6rem;
    text-align: center;
    font-weight: 600;
}

.pivot-scroll {
    overflow-x: auto;
    max-width: 100%;
}

/* Sticky header (Q70): on a desktop screen the pivot scrolls inside a
   container no taller than 70% of the window and its header rows stay in
   view. Phones get no inner scroll (director fix B: the page scrolls, no
   scroll trap; the narrow pager shows one column there). Screen only, so
   paper is never clipped. */
@media screen and (min-width: 641px) {
    .pivot-scroll {
        max-height: 70vh;
        overflow: auto;
    }

    .pivot-table thead {
        position: sticky;
        top: 0;
        z-index: 3;
    }
}

/* ── Sticky first column (statement standard, frontend §12) ──────────────
   Wide columnar reports (multi-year P&L, pivots) keep their identifying
   first column pinned while the number columns scroll sideways — the row
   never loses its name. Backgrounds must be OPAQUE (the striped overlay is
   translucent and would let scrolled numbers ghost through the pinned cell),
   so even rows re-mix the stripe over the solid surface. Apply the class to
   the <table> (Income Statement years, Account Trends' pivot).
   The table CLIPS instead of hiding its overflow (owner 2026-10-03): the
   global `table { overflow: hidden }` makes a table its own scroll
   container, so a sticky cell stuck to the table, which never scrolls, and
   slid away with it. overflow: clip keeps the rounded corners but is not a
   scroll container, so sticky resolves against the real scroller
   (.table-container / .pivot-scroll). A 1px right edge shows the figures
   scrolling behind the pinned column. */
table.sticky-first-col {
    overflow: clip;
}

.sticky-first-col th:first-child,
.sticky-first-col td:first-child {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--surface-base);
    box-shadow: inset -1px 0 0 var(--border);
}

/* Header-style rows (subtotals, totals, Net) keep their own opaque surface. */
.sticky-first-col thead th:first-child,
.sticky-first-col tbody th:first-child,
.sticky-first-col tfoot th:first-child {
    background: var(--surface-lighter);
}

.sticky-first-col thead th:first-child {
    z-index: 3;
}

table.striped.sticky-first-col tbody tr:nth-child(even) td:first-child {
    background: color-mix(in srgb, var(--white) 5%, var(--surface-base));
}

/* A dimmed row (prior year) must not dim its pinned cell: translucent, the
   scrolled figures would show through it. Its label stays italic. */
.sticky-first-col .report-prior-row th:first-child {
    opacity: 1;
}

.pivot-table {
    font-size: var(--font-size-sm, 0.875rem);
}

.pivot-table th,
.pivot-table td {
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* The row-label column: pinned by .sticky-first-col on the table; here
   only its width and wrapping. */
.pivot-table .pivot-fixed {
    max-width: 15rem;
    white-space: normal;
}

.pivot-table .pivot-year {
    text-align: center;
}

/* Opaque header cells: scrolled rows never show through the sticky header. */
.pivot-table thead th {
    background: var(--surface-lighter);
}

/* All P&L accounts (Q70): the group heading rows, and Net income under a
   double rule. Subtotals use the statement suite's .report-subtotal. */
.pivot-table tr.pivot-group th {
    text-align: left;
    font-weight: 700;
}

.pivot-table tfoot tr.pivot-net th {
    border-top: 3px double var(--border, #999);
    font-weight: 700;
}

/* Account Trends cells (Q70): each amount is the button that opens its
   entries in the pop-out. It reads as the figure, underlined dotted, solid
   on hover and focus; touch gets the 44px minimum from the touch standard. */
.pivot-table button.pivot-cell {
    width: 100%;
    padding: 0.1rem 0.25rem;
    background: none;
    border: 0;
    color: inherit;
    font: inherit;
    text-align: right;
    text-decoration: underline dotted;
    text-underline-offset: 0.2em;
    transition: none;
}

.pivot-table button.pivot-cell:hover,
.pivot-table button.pivot-cell:focus-visible {
    background: none;
    transform: none;
    text-decoration-style: solid;
}

.report-prior-row td,
.report-prior-row th {
    opacity: 0.7;
}

/* Account Trends difference row (vs prior year) */
.report-diff-row th {
    border-top: 2px solid var(--border, #999);
}

.trend-up,
.trend-down {
    font-weight: 600;
    white-space: nowrap;
}

/* P&L compare-years checkbox strip */
.pl-year-strip {
    flex-wrap: wrap;
    gap: var(--spacing-xs, 0.25rem) var(--spacing-md, 1rem);
    align-items: center;
}

.pl-year-strip .picker-item {
    display: inline-flex;
    align-items: center;
    gap: 0.3em;
}

/* Phones: the field-row alias would stack the years into one long column —
   short checkbox labels condense to a 3-up grid instead (owner 2026-08-05). */
@media (max-width: 640px) {
    .form-row-inline.pl-year-strip {
        display: grid;
        grid-template-columns: repeat(3, 1fr);
        gap: var(--spacing-xs);
    }
}

/* ═══ Row-actions + stack-table standards (frontend §11e) ═══════════════════
   .row-actions: THE aligned per-row action buttons standard — one flex row of
   .button.small, right-aligned, never reflowing the record row. Disclosure
   actions (.action-disclosure summary-as-button + .action-popover panel)
   overlay instead of expanding in place.
   .stack-table: THE responsive record-table standard — cells carry
   data-label="…"; under 640px the table collapses to labeled cards.
   Identical ×5. Use these everywhere a record list has actions — never
   restyle per screen (same discipline as the color tokens). */
.row-actions {
    display: flex;
    gap: var(--spacing-xs, 0.25rem);
    justify-content: flex-end;
    align-items: center;
}

.row-actions .button {
    margin: 0;
}

.row-actions-cell {
    vertical-align: middle;
}

.row-actions .action-disclosure {
    position: relative;
    margin: 0;
    display: inline-block;
}

.row-actions .action-disclosure > summary {
    list-style: none;
    cursor: pointer;
}

.row-actions .action-disclosure > summary::-webkit-details-marker {
    display: none;
}

.row-actions .action-disclosure > summary::after {
    content: none;
}

.row-actions .action-popover {
    position: absolute;
    right: 0;
    top: calc(100% + 4px);
    z-index: 25;
    display: flex;
    gap: var(--spacing-xs, 0.25rem);
    align-items: center;
    padding: var(--spacing-sm, 0.5rem);
    background: var(--bg-base, #fff);
    border: 1px solid var(--border, #ccc);
    border-radius: 6px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
    min-width: 18rem;
}

.cell-nowrap {
    white-space: nowrap;
}

.register-table {
    font-size: var(--font-size-sm, 0.875rem);
}

.register-table td,
.register-table th {
    vertical-align: middle;
}

@media (max-width: 640px) {
    .stack-table thead {
        display: none;
    }

    /* tfoot too: left as a table-footer-group it wraps its total row in an
       anonymous shrink-to-fit table, a narrower card (till 2026-09-30). */
    .stack-table,
    .stack-table tbody,
    .stack-table tfoot,
    .stack-table tr,
    .stack-table td {
        display: block;
        width: 100%;
    }

    .stack-table tr {
        border: 1px solid var(--border-light, #ddd);
        border-radius: 6px;
        margin-bottom: var(--spacing-sm, 0.5rem);
        padding: var(--spacing-sm, 0.5rem);
    }

    .stack-table td {
        border: 0;
        padding: 0.15rem 0;
        display: flex;
        justify-content: space-between;
        gap: var(--spacing-md, 1rem);
        text-align: right;
    }

    .stack-table td[data-label]::before {
        content: attr(data-label);
        font-weight: 600;
        text-align: left;
        flex: 0 0 auto;
    }

    .stack-table td.row-actions-cell {
        display: block;
        text-align: left;
    }

    .stack-table .row-actions {
        justify-content: flex-start;
        flex-wrap: wrap;
    }

    .stack-table .row-actions .action-popover {
        left: 0;
        right: auto;
    }
}

/* Register workbench specifics */
.wb-slot-row td {
    padding: 0;
    border: 0;
}

.wb-slot-row .inline-edit-slot:empty {
    display: none;
}

.wb-slot-row .inline-edit-slot:not(:empty) {
    padding: var(--spacing-md, 1rem);
}

.register-voided-row {
    opacity: 0.6;
}

/* Inline-create duplicate guard warning (frontend §11f) */
.inline-create-warning {
    border-left: 4px solid var(--warning, #f0ad4e);
    background: var(--warning-bg, rgba(240, 173, 78, 0.12));
    padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
    margin-bottom: var(--spacing-sm, 0.5rem);
    border-radius: 4px;
}

.inline-create-warning .button {
    margin: 0.15rem 0.25rem 0.15rem 0;
}

/* ────────────────────────────────────────────────────────────────────
   §2.8 TABBED DISPATCH — file-tab strip fused to the panel below.
   The strip lives in a .nested-form-section fieldset and the panel is
   the following .nested-form-target, so the fieldset chrome is stripped
   and the active tab's bottom edge is erased into the panel to read as
   one continuous surface (classic file-tab metaphor).
   ──────────────────────────────────────────────────────────────── */
fieldset.nested-form-section:has(> form.tab-nav[data-tabbed-dispatch]) {
    border: none;
    background: none;
    padding: 0;
    margin-bottom: 0;
    /* <fieldset> ships with a browser-default min-width:min-content, so it
       REFUSES to shrink below its contents. That defeated the tabs' own
       flex-shrink/ellipsis (measured: fieldset 1041px wide inside an 820px
       viewport, pushing the whole PAGE into horizontal scroll at 6 tabs).
       Resetting it lets the strip shrink and the labels ellipsize instead. */
    min-width: 0;
}

fieldset.nested-form-section:has(> form.tab-nav[data-tabbed-dispatch]) > legend {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

form.tab-nav[data-tabbed-dispatch] {
    position: relative;
    z-index: 1;
    gap: 2px;
    margin-bottom: 0;
    padding-bottom: 0;
    border-bottom: none;   /* the panel's umbrella owns the horizontal line */
    /* THE SEAM FIX (2026-07-28). The base .tab-nav sets overflow-x:auto for
       scrollable tab strips; per CSS spec, overflow-x:auto with overflow-y
       visible forces overflow-y to compute to AUTO as well — so the strip
       became a scroll container and CLIPPED its own children vertically.
       That silently scissored the selected tab's margin-bottom:-2px overhang,
       so it could never paint over the panel's top line and the "file tab"
       never actually connected. Measured before this fix: overflow-y=auto,
       overhang present in layout (getBoundingClientRect) but not in paint.
       tabbed-dispatch is capped at 6 tabs and STACKS below 768px, so it does
       not need the scroll affordance the generic .tab-nav provides.
       Same failure class as the .action-popover/.table-container clipping
       documented above — a scroll ancestor eating a deliberate overhang. */
    overflow: visible;
}

form.tab-nav[data-tabbed-dispatch] > button {
    position: relative;
    /* The strip is overflow:visible so the selected tab's -2px seam can paint
       (see the parent rule). That removes the scroll containment the generic
       .tab-nav gets from overflow-x:auto, so without this the 6-tab cap could
       push the PAGE into horizontal scroll on a narrow desktop (measured: 6
       tabs at an 820px viewport = 1052px document width). Letting the tabs
       shrink and ellipsize keeps them on ONE row — which the seam requires,
       since a wrapped row would sit away from the panel edge — and keeps the
       page contained. Overriding .tab-nav button's flex-shrink:0. */
    flex-shrink: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    padding: var(--spacing-sm) var(--spacing-md);
    border: 1px solid var(--border);
    border-bottom: none;
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
    background: var(--surface-light);
    color: var(--text-on-dark);
    font-weight: 500;
}

/* Hover is a TINT SHIFT ONLY — never movement, and never a fill that reaches
   the umbrella's top edge. Movement is suppressed for every tab (the generic
   button:hover lift would otherwise shift the selected tab and break the seam
   it forms with the panel). */
form.tab-nav[data-tabbed-dispatch] > button:hover,
form.tab-nav[data-tabbed-dispatch] > button:focus-visible {
    transform: none;
}

form.tab-nav[data-tabbed-dispatch] > button:hover:not(.active) {
    background: color-mix(in srgb, var(--surface-lighter) 70%, var(--primary) 12%);
    color: var(--text-on-dark-accent);
}

form.tab-nav[data-tabbed-dispatch] > button.active:hover {
    background: var(--surface-base);
}

/* The selected tab is a continuation of the panel: same fill, and it sits ON
   TOP of the umbrella's top edge (negative margin + higher z-index) so the
   line is physically covered for the tab's width rather than faked. */
form.tab-nav[data-tabbed-dispatch] > button.active {
    z-index: 2;
    margin-bottom: -2px;
    padding-bottom: calc(var(--spacing-sm) + 2px);
    background: var(--surface-base);
    border-color: var(--primary);
    border-bottom: none;
    color: var(--text-on-dark-accent);
    font-weight: 600;
}

fieldset.nested-form-section:has(> form.tab-nav[data-tabbed-dispatch]) + .nested-form-target:not(:empty) {
    position: relative;
    z-index: 0;
    margin-top: 0;
    padding: var(--spacing-lg);
    background: var(--surface-base);
    border: none;
    border-radius: 0 0 var(--radius-md) var(--radius-md);
}

/* The "umbrella": the tab strip's line plus short side rails with rounded top
   corners, fading out quickly so the panel is implied rather than boxed in.
   The mask ramp is an alpha channel (black = keep, transparent = drop), not a
   theme colour — it never paints, so it is not a hardcoded-colour violation. */
fieldset.nested-form-section:has(> form.tab-nav[data-tabbed-dispatch]) + .nested-form-target:not(:empty)::before {
    content: '';
    position: absolute;
    inset: 0 0 auto 0;
    height: 7rem;
    border: 1px solid var(--primary);
    border-top-width: 2px;
    border-bottom: none;
    /* TOP-LEFT SQUARED (owner, 2026-07-28): the umbrella's rounded top-left
       curved away from underneath the first tab, so the selected tab appeared
       to float above a curve instead of continuing the panel — it "ruins the
       illusion". The panel itself was already square on top; this pseudo-element
       was the one rounding that corner. Top-RIGHT keeps its radius so the
       umbrella still reads as a panel rather than a hard box. */
    border-radius: 0 var(--radius-md) 0 0;
    pointer-events: none;
    -webkit-mask-image: linear-gradient(to bottom, #000 0, rgba(0, 0, 0, 0) 100%);
            mask-image: linear-gradient(to bottom, #000 0, rgba(0, 0, 0, 0) 100%);
}

/* ────────────────────────────────────────────────────────────────────
   Row-action popovers vs scrolling table wrappers.
   .action-popover is absolutely positioned, but .table-container /
   .js-table-scroll-wrap / .table-responsive set overflow-x:auto, and a
   scroll container CLIPS absolutely-positioned descendants — so the Void
   reason panel opened invisibly. While a row-action disclosure is open,
   stop clipping. Horizontal scroll returns the moment it closes.
   ──────────────────────────────────────────────────────────────── */
.table-container:has(.action-disclosure[open]),
.js-table-scroll-wrap:has(.action-disclosure[open]),
.table-responsive:has(.action-disclosure[open]) {
    overflow: visible;
}

/* ────────────────────────────────────────────────────────────────────
   CREATE-NEW standard (owner standard 2026-07-28): every maintenance
   list opens with a COLLAPSED "Add New X" <details class="create-new">.
   The summary is success-tinted so the create affordance is instantly
   findable at the top of any list. Submitting the form inside targets
   the LIST's own block, so the fresh row appears on refresh.
   ──────────────────────────────────────────────────────────────── */
details.create-new > summary {
    background: color-mix(in srgb, var(--success) 24%, var(--surface-lighter));
    color: var(--text-on-dark-accent);
    font-weight: 600;
}

details.create-new > summary:hover {
    background: color-mix(in srgb, var(--success) 34%, var(--surface-lighter));
}

details.create-new[open] > summary {
    border-bottom: 2px solid var(--success);
}

/* Inline-edit slot rows: when the slot is EMPTY (editor closed / cancelled),
   hide the whole carrier row — otherwise the empty <td> band lingers as dead
   space between records. Companion to .inline-edit-slot:empty{display:none}. */
tr.wb-slot-row:has(> td > .inline-edit-slot:empty) {
    display: none;
}

/* ────────────────────────────────────────────────────────────────────
   Quick-filter + bulk-actions layout (maintenance surface standard).
   .list-filter: the LABELED client-side filter emitted by
   renderListFilter() — label explains, placeholder only hints.
   .bulk-actions: selection controls in their own breathing row, not
   crushed against the select-all checkbox.
   ──────────────────────────────────────────────────────────────── */
.list-filter {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    margin: var(--spacing-sm) 0 var(--spacing-md);
    max-width: 34rem;
}

.list-filter > label {
    font-weight: 600;
    white-space: nowrap;
    color: var(--text-on-dark-accent);
}

.list-filter > input {
    flex: 1 1 auto;
}

.bulk-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--spacing-sm);
    margin: var(--spacing-sm) 0;
    padding: var(--spacing-sm);
    background: var(--bg-layer-lighten);
    border-radius: var(--radius-sm);
}

.bulk-actions > label {
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-xs);
    margin-right: var(--spacing-md);
    font-weight: 600;
}

/* Inactive rows in maintenance lists (recipes, etc.): dimmed, not hidden. */
table tbody tr.row-inactive td {
    opacity: 0.55;
}

/* Autofetch dropdowns must escape their container: <details> clips children
   via overflow:hidden (corner-radius clipping), which cut off the type-ahead
   options list inside create-new blocks. When open AND hosting an autofetch,
   let content overflow — the summary keeps its rounded top either way. */
details[open]:has(.autoFetchContainer) {
    overflow: visible;
}

/* ────────────────────────────────────────────────────────────────────
   Admin home tree row (Tasks / Reports / Settings / Framework).
   Layout owned here (inline styles removed from management.php).
   On phones the descriptions are hidden — the four labels carry the
   meaning and the descriptions were eating half the viewport.
   ──────────────────────────────────────────────────────────────── */
.management-tree-row {
    display: flex;
    gap: 1rem;
    flex-wrap: wrap;
}

.tree-desc {
    display: block;
    margin-top: 0.25rem;
}

@media (max-width: 768px) {
    .tree-desc {
        display: none;
    }
}

/* ────────────────────────────────────────────────────────────────────
   Tab hubs on phones (STANDARD): tabs STACK vertically, full width.
   The desktop file-tab illusion (overlap + matching fill) needs the
   strip adjacent to the panel; stacked, the selected tab signals with
   a solid primary fill instead. Applies to every tabbed-dispatch hub.
   ──────────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
    form.tab-nav[data-tabbed-dispatch] {
        flex-direction: column;
        overflow-x: visible;
        border-bottom: none;
        gap: 0.35rem;
    }

    form.tab-nav[data-tabbed-dispatch] > button {
        width: 100%;
        justify-content: flex-start;
        top: 0;
        margin-bottom: 0;
        padding-bottom: var(--spacing-sm);
        border: 1px solid var(--border);
        border-radius: var(--radius-sm);
    }

    form.tab-nav[data-tabbed-dispatch] > button.active {
        /* Stacked tabs have no panel edge to seam with, so COLOUR is the only
           available active signal here. --primary is the deep brand shade the
           owner rejected on 2026-07-28 ("a dark green tab does NOT indicate
           ACTIVE to me, a bright green one does"); the desktop fix is the seam,
           but on phones it has to be the accent. Paired text token per §4. */
        background: var(--primary-accent);
        border-color: var(--primary-accent);
        color: var(--primary-accent-text);
        /* Owner decision 2026-07-28: tabs keep their SOURCE ORDER on mobile.
           Reordering the active tab to the bottom (flex `order`) would seam it
           to the panel, but the stack would visibly reshuffle on every tap and
           visual order would stop matching DOM/focus order (WCAG 2.4.3/1.3.2).
           Instead the active tab carries a second, NON-COLOUR cue: a heavy
           left rail. Fill + rail together mean the state does not depend on
           colour perception alone (WCAG 1.4.1). */
        border-left: 4px solid var(--primary-dark);
    }
}

/* ────────────────────────────────────────────────────────────────────
   Form action rows (STANDARD): flex with gaps — buttons never overlap
   or crowd. Order convention: PRIMARY action first, secondary actions
   next, utilities (Print) last.
   ──────────────────────────────────────────────────────────────── */
.form-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-sm);
    align-items: center;
    margin-top: var(--spacing-md);
}

/* ═══ POS RECEIPT (component; prints via the .forPrint contract) ═══════
   The receipt is PAPER: deliberately theme-independent, so literal
   black-on-white is CORRECT here (the one sanctioned exception - a
   receipt must render identically on every site and theme, and the old
   var(--white)/var(--black) tokens resolved to NOTHING). On screen it
   is a white card; in print, print-styles.css's .forPrint contract
   prints ONLY this subtree. [POS redesign chat 2026-08-12] */
.receipt-container { max-width: 400px; margin: 0 auto; }
.receipt-actions { margin-bottom: var(--spacing-lg); display: flex; gap: var(--spacing-md); }
.receipt {
    background: #fff;
    color: #000;
    padding: var(--spacing-lg);
    font-family: 'Courier New', monospace;
    font-size: 12px;
    line-height: 1.4;
}
.receipt h2, .receipt h4, .receipt p, .receipt pre, .receipt small { color: #000; background: transparent; }
.receipt table { width: 100%; margin: 0; border-collapse: collapse; background: transparent; box-shadow: none; }
.receipt tr, .receipt th, .receipt td {
    padding: 2px 0;
    border: none;
    background: transparent;
    color: #000;
    font-size: 11px;
}
.receipt th { text-align: left; }
.receipt-header { text-align: center; margin-bottom: var(--spacing-md); border-bottom: 1px dashed #000; padding-bottom: var(--spacing-md); }
.receipt-header h2 { margin: 0; font-size: 18px; }
.receipt-address { margin: var(--spacing-sm) 0 0 0; font-family: inherit; font-size: 11px; white-space: pre-line; }
.receipt-logo { max-width: 60%; max-height: 80px; margin-bottom: var(--spacing-sm); }
.receipt-contact { margin: 2px 0; font-size: 11px; }
.receipt-info { margin-bottom: var(--spacing-md); border-bottom: 1px dashed #000; padding-bottom: var(--spacing-md); }
.receipt-line-items th { border-bottom: 1px solid #000; font-weight: bold; padding: 4px 2px; }
.receipt-line-items td { padding: 4px 2px; }
.receipt-line-items tbody tr:last-child td { border-bottom: 1px dashed #000; padding-bottom: var(--spacing-sm); }
.discount-note { color: #555; font-size: 10px; }
.receipt-totals { margin-bottom: var(--spacing-md); border-bottom: 1px dashed #000; padding-bottom: var(--spacing-md); }
.totals-table .grand-total td { padding-top: var(--spacing-sm); font-size: 14px; border-top: 1px solid #000; }
.receipt-payments h4 { margin: 0 0 var(--spacing-sm) 0; font-size: 12px; }
.payments-table { margin-bottom: var(--spacing-md); }
.payments-table .total-tips td { border-top: 1px dashed #000; padding-top: var(--spacing-sm); }
.receipt-footer { text-align: center; margin-top: var(--spacing-md); font-size: 11px; }
.receipt-voided { text-align: center; font-size: 18px; font-weight: bold; color: #c00; margin-top: var(--spacing-md); padding: var(--spacing-md); border: 2px solid #c00; }
@media print {
    .receipt-container { max-width: none; margin: 0; padding: 0; }
    .receipt { padding: 0; box-shadow: none; }
}

/* ===== GIFT CERTIFICATE (owner design 2026-09-29) =====
   Printable certificate shown under the receipt, one per certificate line
   (receipt.php). Sized for the SAME strip the receipt prints on: 2.75in wide
   by 8.5in long, four cut from a landscape letter sheet. It is paper, so it
   is ink on white in every theme. Three stacked groups (head / body / foot)
   spread down the strip. "To" and "From" are blank rules for handwriting.
   The print-only overrides that beat the generic .forPrint rules live in
   print-styles.css; keep the two in step. */
.gc-print { margin-top: var(--spacing-lg); }
.gift-certificate {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    box-sizing: border-box;
    width: 100%;
    max-width: 2.75in;
    min-height: 7in;
    margin: 0 auto;
    padding: 0.14in 0.12in;
    border: 4px double #000;
    background: #fff;
    color: #000;
    font-family: Georgia, 'Times New Roman', serif;
    font-size: 9pt;
    line-height: 1.3;
    text-align: center;
}
.gift-certificate h3,
.gift-certificate h4,
.gift-certificate p,
.gift-certificate dl,
.gift-certificate dt,
.gift-certificate dd { margin: 0; padding: 0; color: #000; background: transparent; }
.gift-certificate .receipt-logo { max-width: 1.6in; max-height: 0.8in; margin: 0 auto 0.06in; }
.gift-certificate .receipt-logo--invert { filter: brightness(0); }
.gift-certificate h4 { font-size: 9pt; font-weight: normal; letter-spacing: 0.06em; }
.gift-certificate .gc-foot h4 { margin-top: 0.1in; }
.gift-certificate h3 { font-size: 15pt; font-weight: normal; letter-spacing: 0.14em; line-height: 1.15; text-transform: uppercase; }
.gift-certificate h3 span { display: block; white-space: nowrap; }   /* GIFT / CERTIFICATE, never split inside a word */
.gift-certificate hr { width: 40%; margin: 0.1in auto; border: 0; border-top: 1px solid #000; }
.gift-certificate .gc-amount { font-size: 26pt; font-weight: bold; line-height: 1; }
.gift-certificate .gc-write { display: flex; align-items: flex-end; gap: 0.06in; margin-top: 0.34in; font-style: italic; text-align: left; }
.gift-certificate .gc-write::after { content: ''; flex: 1; border-bottom: 1px solid #000; }
.gift-certificate dt { margin-top: 0.07in; font-size: 6.5pt; letter-spacing: 0.1em; text-transform: uppercase; }
.gift-certificate dd { font-size: 9pt; }
.gift-certificate .gc-code { font-family: 'Courier New', monospace; font-size: 11pt; font-weight: bold; letter-spacing: 0.04em; }
.gift-certificate .gc-void { margin-top: 0.1in; padding: 0.04in; border: 2px solid #000; font-weight: bold; letter-spacing: 0.3em; }
.gift-certificate .gc-terms { margin-top: 0.1in; font-size: 6.5pt; font-style: italic; line-height: 1.25; }
.gift-certificate .gc-contact { margin-top: 0.06in; font-size: 6.5pt; line-height: 1.25; white-space: pre-line; }
/* The code as a QR (owner Q49): drawn by qr-tools.js, sized for a phone camera. */
.gift-certificate .gc-qr { width: 1in; margin: 0.08in auto 0; }
.gift-certificate .gc-qr svg { display: block; width: 100%; height: auto; }

/* ========================================================================
   IMAGE LIGHTBOX - generic full-size view pane (initImageLightbox, universal.js)
   Opt-in: a container carrying data-lightbox makes every <img> inside it
   clickable; the image opens full size in this in-page overlay pane (no new
   window). Click anywhere or Esc closes. Backdrop + close colors are
   backdrop-paired literals (same class as shadows), not theme colors.
   ======================================================================== */
[data-lightbox] img {
    cursor: zoom-in;
}
.image-lightbox {
    position: fixed;
    inset: 0;
    z-index: 10000; /* above .calc-widget (9999) */
    display: none;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-lg);
    background: rgba(0, 0, 0, 0.85);
    cursor: zoom-out;
}
.image-lightbox.open {
    display: flex;
}
.image-lightbox img {
    max-width: 92vw;
    max-height: 92vh;
    border-radius: var(--radius-md, 0.5rem);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
}
.image-lightbox-close {
    position: absolute;
    top: var(--spacing-md);
    right: var(--spacing-md);
    padding: var(--spacing-xs);
    font-size: 2rem;
    line-height: 1;
    background: transparent;
    border: 0;
    color: #fff; /* on the black backdrop - theme-independent */
    cursor: pointer;
}
/* Gallery chrome (multi-page receipts, 2026-09-24): prev/next + counter show
   ONLY when the opened container holds 2+ images — classic single-image
   data-lightbox consumers render pixel-unchanged. */
.image-lightbox-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    padding: var(--spacing-xs) var(--spacing-sm);
    font-size: 3rem;
    line-height: 1;
    background: transparent;
    border: 0;
    color: #fff; /* on the black backdrop - theme-independent */
    cursor: pointer;
}
.image-lightbox-prev { left: var(--spacing-md); }
.image-lightbox-next { right: var(--spacing-md); }
.image-lightbox-counter {
    position: absolute;
    bottom: var(--spacing-md);
    left: 50%;
    transform: translateX(-50%);
    font-size: 0.9rem;
    color: #fff; /* on the black backdrop - theme-independent */
}
/* Multi-page receipt strip (renderReceiptPages, 2026-09-24): thumbs flow
   inline with page badges; medium/large stack pages in reading order. */
.receipt-pages {
    display: inline-flex;
    flex-wrap: wrap;
    gap: var(--spacing-xs);
    align-items: flex-start;
    vertical-align: middle;
}
.receipt-pages--stack {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-sm);
}
.receipt-page {
    position: relative;
    display: inline-block;
}
.receipt-page-badge {
    position: absolute;
    top: 2px;
    left: 2px;
    z-index: 1;
    padding: 0 0.35em;
    border-radius: var(--radius-sm, 0.25rem);
    font-size: 0.7rem;
    line-height: 1.4;
    background: rgba(0, 0, 0, 0.65); /* over the image - theme-independent */
    color: #fff;
    pointer-events: none;
}
/* Books/admin element helpers (2026-09-25 inline-style retirement):
   semantic classes for shapes the books screens carried as style=. */
form.form-inline { display: inline; }           /* action form beside sibling buttons */
.form-text--warning { color: var(--warning); }  /* --color-warning never existed; --warning is the semantic home (fix 2026-09-25) */
:where(.field-row, .form-row-inline).gap-lg { gap: 2rem; }  /* summary/totals rows */
tr.row-voided, .pos-sales-results summary.row-voided { opacity: 0.6; }
.heading-gap { margin-top: 2rem; }              /* heading after a form block */
.entry-divider {                                 /* divided list entry */
    margin-bottom: 1rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--color-border);
}
.button.gap-below { margin-bottom: 1rem; }
/* ── FOLD LISTS: framework component (the Story Workspace's tree, made the
   standard for every fold list, Q125 2026-10-07; framework-frontend.md §6C) ──
   A list whose groups and rows open and close IN PLACE:
     .fold-list                  the list (<ul>)
     details.fold-group          a group, remembered open/closed (data-state-key)
     li.fold-item                a row: details.fold-row (data-open-route /
                                 -target / -post: its header loads the row's
                                 editor into its inline-edit slot on first
                                 open), then div.fold-controls
     summary.fold-line           any fold's header line: the far-left +/-
     .fold-controls              controls laid over the header's right end but
                                 OUTSIDE the summary, so a tap on them never
                                 toggles the fold (the toggle-hit-zone rule)
   Phone first: the header keeps to its first line and the controls take a
   second header line under it; from 641px they ride on the header line,
   which keeps --fold-controls-w free for them. Behaviour is universal.js:
   setupStatefulDetails, the load-on-open folds (loadDetailsFold),
   enforceSingleInlineEditor with closeEmptiedFold / openFilledFold.
   Nested rows: a row's own rows ride in a details.fold-group INSIDE its
   li.fold-item, after the controls (inside the parent's card, indented on
   the group's darker body); the list filter keeps a row shown while a row
   inside it matches. */
.fold-list { display: flex; flex-direction: column; gap: 0.25rem; }
.fold-group { margin: 0; }
.fold-group > .fold-line {
    cursor: pointer;
    font-weight: 600;
    flex-wrap: wrap;
    min-height: var(--fold-head);
    padding: 0 1rem;
    align-items: center;
}
.fold-item {
    --fold-head: 2.25rem;
    position: relative;
    display: block;
    padding: 0;
    background: var(--surface-light);
    border-radius: 4px;
}
.fold-item:hover { background: var(--surface-lighter); }
.fold-row { margin: 0; background: none; border-radius: 0; overflow: visible; }
.fold-row > .fold-line {
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: 0 0.5rem;
    min-height: var(--fold-head);
    padding: 0 0.6rem;
    background: none;
    color: inherit;
    font-weight: inherit;
}
.fold-row > .fold-line:hover { background: none; }
.fold-row > .inline-edit-slot { padding: 0 0.6rem; background: none; color: inherit; }
.fold-row > .inline-edit-slot:not(:empty) { padding-bottom: 0.6rem; }
/* The open/close marker: a +/- on the FAR LEFT of the header, before
   anything, in place of the element baseline's right-side summary::after
   cue. + closed, - open. A row grid's fold carries the same marker on its
   first cell (a ::before of the summary itself would take a grid column). */
.fold-line::before,
.row-grid details > summary > :first-child::before {
    content: "+";
    flex: 0 0 auto;
    display: inline-block;
    width: 1em;
    margin-right: 0.5rem;
    font-family: monospace;
    font-weight: 700;
    opacity: 0.6;
    text-align: center;
}
[open] > .fold-line::before,
.row-grid details[open] > summary > :first-child::before { content: "\2212"; }
.fold-line::after,
[open] > .fold-line::after { content: none; }
/* Phone: a group's or row's ::after is the empty second header line its
   controls sit on. Both selectors: the open state must out-specify the
   rule above. */
:is(.fold-group, .fold-row) > .fold-line::after,
:is(.fold-group, .fold-row)[open] > .fold-line::after {
    content: "";
    order: 2;
    flex: 0 0 100%;
    height: var(--fold-head);
    margin: 0;
}
.fold-controls {
    position: absolute;
    top: var(--fold-head);
    right: 0.6rem;
    height: var(--fold-head);
    display: flex;
    align-items: center;
    gap: 0.5rem;
    z-index: 2;
}
@media (min-width: 641px) {
    :is(.fold-group, .fold-row) > .fold-line { flex-wrap: nowrap; padding-right: var(--fold-controls-w); }
    :is(.fold-group, .fold-row) > .fold-line::after,
    :is(.fold-group, .fold-row)[open] > .fold-line::after { content: none; }
    .fold-controls { top: 0; }
}
/* Nested rows: the group inside a row is a small plain line, its count
   beside the marker, with no controls line (the row's own controls stay on
   the row's header); its rows indented on the group's darker body. */
.fold-item > .fold-group { margin: 0 0.6rem 0.4rem 1.5rem; }
.fold-item > .fold-group > .fold-line {
    justify-content: flex-start;
    min-height: 2rem;
    padding: 0 0.6rem;
    font-size: var(--font-size-sm);
    font-weight: inherit;
    opacity: 0.85;
}
.fold-item > .fold-group > .fold-line::after,
.fold-item > .fold-group[open] > .fold-line::after { content: none; }

/* ── COLLAPSIBLE ROW GRID — framework component (Web Store lane 2026-08-25) ──
   A table-like list where a heading row can expand to child rows, with every
   row (header, item, child) aligned to ONE shared column template set via
   --row-cols on the container. <details>/<summary> deliberately RESET here:
   the site's element baseline styles them as content cards, but inside a row
   grid the rows are table rows, not cards. The component also pins its own
   text alignment — host regions (e.g. the storefront) may center text, and
   grid columns must not inherit that. Open state survives AJAX re-renders via
   data-state-key (setupStatefulDetails). First consumer: storefront
   inventory. Reuse: container gets .row-grid + --row-cols; rows get .row-line
   (.row-head header / .row-sub child / .row-plain non-collapsible). */
.row-grid { --row-cols: repeat(4, minmax(0, 1fr)); border: 1px solid var(--border-light); border-radius: var(--radius-sm); overflow: hidden; }
.row-grid .row-line { display: grid; grid-template-columns: var(--row-cols); gap: 0 var(--spacing-sm); align-items: center; padding: var(--spacing-xs) var(--spacing-sm); border-top: 1px solid var(--border-light); }
.row-grid .row-line > * { text-align: left; min-width: 0; }
.row-grid .row-line > .numeric { text-align: right; }
.row-grid > .row-line:first-child { border-top: 0; }
.row-grid .row-head { font-weight: 600; background: var(--surface-lighter); }
.row-grid .row-sub { background: var(--bg-layer-lighten); }
/* Reset the element-level details/summary card baseline inside the grid. */
.row-grid details { margin: 0; padding: 0; border-radius: 0; background: none; overflow: visible; }
.row-grid details > summary { width: auto; font-weight: inherit; background: none; color: inherit; cursor: pointer; list-style: none; }
.row-grid details > summary:hover { background: var(--surface-lighter); }
.row-grid details > summary::after { content: none; }
.row-grid details > summary::-webkit-details-marker { display: none; }
.row-grid details > :not(summary) { padding: 0; background: none; color: inherit; }
/* Disclosure cue: the fold lists' +/- on the first cell (FOLD LISTS above);
   plain rows indent to align. */
.row-grid .row-plain > :first-child { padding-left: calc(1em + 0.5rem); }

/* Storefront inventory consumer: 6 columns; Category/Brand yield on narrow. */
.inventory-results.row-grid { --row-cols: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(5.5rem, auto) minmax(4.5rem, auto); }
.inventory-results .inv-variant > span:first-child { grid-column: 1 / 5; padding-left: 2.25rem; }
@media (max-width: 700px) {
    .inventory-results.row-grid { --row-cols: minmax(0, 2fr) minmax(0, 1fr) minmax(5rem, auto) minmax(4rem, auto); }
    .inventory-results .inv-cat, .inventory-results .inv-brand { display: none; }
    .inventory-results .inv-variant > span:first-child { grid-column: 1 / 3; }
}

/* POS sale rows (renderPosSaleRows(), storefront/sales/helpers.php; the till's
   daily summaries and Admin POS Sales, 2026-09-30, client first). Mobile-first:
   on a phone the row is client + invoice, then stylist + time, then the
   labelled figures two per line; wider, the four head cells share one line
   and the figures one row under them. The opened detail spans the row.
   Voided rows dim with tr.row-voided (one rule). */
.pos-sales-results.row-grid { --row-cols: minmax(0, 1fr) auto; }
.pos-sales-results .ps-figures { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--spacing-xs) var(--spacing-sm); margin-top: var(--spacing-xs); }
/* Each label + figure is one chip (the totals strip's surface), so the pair
   reads as one thing (owner, 2026-09-30). */
.pos-sales-results .ps-figures > span { display: flex; justify-content: space-between; align-items: baseline; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); background: var(--bg-layer-darken); border-radius: var(--radius-sm); }
.pos-sales-results .ps-figures small { opacity: 0.75; }
.pos-sales-results .ps-detail { grid-column: 1 / -1; }
.pos-sales-results .ps-detail table { margin: var(--spacing-xs) 0; }
/* Row type hints (owner 2026-09-30): a left accent and a faint tint by token
   meaning (sale success, refund warning, void danger), mixed the way the
   .badge-* rules mix; the row's badge carries the word, so the type is never
   colour only. A void from before the reversal rule keeps .row-voided. */
.pos-sales-results summary.ps-sale   { border-inline-start: 3px solid var(--success); background: color-mix(in srgb, var(--success) 8%, var(--surface-base)); }
.pos-sales-results summary.ps-refund { border-inline-start: 3px solid var(--warning); background: color-mix(in srgb, var(--warning) 8%, var(--surface-base)); }
.pos-sales-results summary.ps-void   { border-inline-start: 3px solid var(--error);   background: color-mix(in srgb, var(--error) 8%, var(--surface-base)); }
/* The client of a finished sale (owner Q48): the till's own widget on the row. */
.pos-sales-results .pos-sale-client { margin-top: var(--spacing-sm); }
/* Refund on the till's sale row (owner Q47 2026-09-30): the lines to pick,
   then the money back by tender. */
.pos-sales-results .pos-refund { margin-top: var(--spacing-sm); }
.pos-sales-results .pos-refund > label { display: flex; align-items: center; gap: var(--spacing-sm); }
@media (pointer: coarse) {
    .pos-sales-results .pos-refund > label { min-height: var(--tap-min); }
}
@media (min-width: 701px) {
    .pos-sales-results.row-grid { --row-cols: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr) auto; }
    .pos-sales-results .ps-figures { grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); }
}

/* Universal Finder rows (renderFinder, finderRenderRecordRows, 2026-10-04;
   every finder's rows since Q125): the row grid above, any column count.
   Column 1 is 2fr and every further cell an auto 1fr column (one row, cells
   flowing across), so each finder's own columns align without a per-screen
   template. The opened row is a plain block: the record's editor in its
   slot, or (custom rowActions) its children's compact table, then the
   actions. Phone: the POS sales rows' layout, two cells per line, no header. */
.finder-results.row-grid { --row-cols: minmax(0, 2fr); }
.finder-results > details > summary.row-line,
.finder-results > .row-head { grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); }
.finder-results .row-head > :first-child { padding-left: calc(1em + 0.5rem); }
.finder-results .row-sub { display: block; }
.finder-results > details > .inline-edit-slot { padding: 0 var(--spacing-sm) var(--spacing-sm); }
.finder-results .row-sub table { margin: var(--spacing-xs) 0; }
.finder-results .row-count { opacity: 0.7; }
@media (max-width: 700px) {
    .finder-results.row-grid { --row-cols: minmax(0, 1fr) auto; }
    .finder-results > details > summary.row-line { grid-auto-flow: row; }
    .finder-results > .row-head { display: none; }
}

/* Till reports (storefront/reports.php, 2026-09-29; moved from the page's own
   style block). Element-first inside the one container class: the header,
   the report-type form (the only direct-child form), each report <article>
   with its <section>s. The figures use the shared
   .entry-totals-strip; print hiding uses .no-print on the controls. No width
   cap: the till fills #mainContent like every page (owner look 2026-09-30). */
.reports-interface > header { margin-bottom: var(--spacing-lg); }
.reports-interface > form {
    background: var(--bg-layer-lighten);
    padding: var(--spacing-md);
    border-radius: var(--radius-sm);
    margin-bottom: var(--spacing-lg);
}
.reports-interface h4 {
    margin-bottom: var(--spacing-lg);
    padding-bottom: var(--spacing-sm);
    border-bottom: 2px solid var(--primary-accent);
}
.reports-interface .entry-totals-strip { margin-bottom: var(--spacing-xl); }
.reports-interface section { margin-bottom: var(--spacing-xl); overflow-x: auto; }
.reports-interface section h5 { margin-bottom: var(--spacing-md); color: var(--text-on-dark-accent); }
.reports-interface section table { margin: 0; }

/* Storefront (pages/storefront.php, owner look 2026-09-30): ONE compact bar,
   the operator's badge then the three sections as the house .tab-nav strip
   (scrolls sideways, never wraps; the active tab is lit). One line where it
   fits; on a phone the badge sits above the strip so all three tabs show.
   No page chrome of its own: the till fills #mainContent like every page. */
.storefront-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--spacing-xs) var(--spacing-sm); margin-bottom: var(--spacing-md); }
.storefront-bar > .badge { flex-shrink: 0; font-size: var(--font-size-sm); }
.storefront-bar > .tab-nav { flex: 1 1 16rem; min-width: 0; margin-bottom: 0; }
/* Touch: the tabs meet the 44px minimum here (the generic .tab-nav button
   38px rule sits later in this file than the touch standard and wins). */
@media (pointer: coarse) {
    .storefront-bar > .tab-nav button { min-height: var(--tap-min); }
}

/* The QR scanner (qr-tools.js, owner Q49): a modal dialog with the camera
   picture, one line of status and Cancel. */
dialog.qr-scanner { width: min(28rem, 92vw); padding: var(--spacing-md); border: 1px solid var(--border-light); border-radius: var(--radius-sm); background: var(--surface-base); color: var(--text-on-dark); }
dialog.qr-scanner::backdrop { background: rgba(0, 0, 0, 0.6); }
dialog.qr-scanner video { display: block; width: 100%; border-radius: var(--radius-sm); background: #000; }
dialog.qr-scanner p { margin: var(--spacing-sm) 0; }

/* ═══ POP-OUT: THE content dialog (universal.js initPopouts, Q70 2026-10-03)
   One lazily created <dialog class="popout no-print">: a heading with its
   close button, then a body filled by loadContent. A centred panel on a
   desktop screen, a full-screen sheet at 640px wide or less. No padding on
   the dialog itself (a click on the dialog element is the backdrop). */
dialog.popout {
    width: min(48rem, 94vw);
    max-height: 85vh;
    padding: 0;
    border: 1px solid var(--border-light);
    border-radius: var(--radius-sm);
    background: var(--surface-base);
    color: var(--text-on-dark);
}

dialog.popout[open] {
    display: flex;
    flex-direction: column;
}

dialog.popout::backdrop {
    background: rgba(0, 0, 0, 0.6);
}

.popout-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--spacing-sm);
    padding: var(--spacing-sm) var(--spacing-md);
    border-bottom: 1px solid var(--border-light);
}

.popout-head h3 {
    margin: 0;
    font-size: var(--font-size-md);
}

.popout-close {
    min-width: var(--tap-min);
    min-height: var(--tap-min);
    padding: 0;
    font-size: 1.5rem;
    line-height: 1;
}

.popout-body {
    min-height: 4rem;   /* room for the .loading spinner while it fills */
    overflow: auto;
    padding: var(--spacing-md);
}

@media (max-width: 640px) {
    dialog.popout {
        width: 100vw;
        max-width: 100vw;
        height: 100dvh;
        max-height: 100dvh;
        margin: 0;
        border: 0;
        border-radius: 0;
    }
}

/* Storefront gift certificate lookup (sections/storefront/gift-certificates.php,
   2026-09-30): the search panel is the container's direct-child form. */
.gc-lookup > form {
    background: var(--bg-layer-lighten);
    padding: var(--spacing-md);
    border-radius: var(--radius-sm);
    margin-bottom: var(--spacing-lg);
}
.gc-lookup section { margin-bottom: var(--spacing-lg); }

/* Storefront inventory lookup (sections/storefront/inventory.php; moved from
   the section's own style block 2026-09-30, width cap dropped). */
.inventory-lookup .inventory-header { margin-bottom: var(--spacing-lg); }
.inventory-lookup .inventory-header h3 { margin-bottom: var(--spacing-xs); }
.inventory-lookup .inventory-search { background: var(--bg-layer-lighten); padding: var(--spacing-md); border-radius: var(--radius-sm); margin-bottom: var(--spacing-lg); }
.inventory-lookup .inventory-results { margin-bottom: var(--spacing-lg); }
.inventory-lookup .item-details { color: var(--text-on-dark-secondary); font-size: var(--font-size-sm); }
.inventory-lookup .item-type { display: inline-block; padding: 2px 8px; border-radius: var(--radius-sm); font-size: var(--font-size-sm); font-weight: 500; }
.inventory-lookup .item-type-product { background: var(--primary-transparent); color: var(--primary-accent); }
.inventory-lookup .item-type-service { background: var(--secondary-transparent); color: var(--text-on-dark-secondary); }
.inventory-lookup .item-type-material { background: var(--tertiary-transparent); color: var(--tertiary-accent); }
.inventory-lookup .stock-level { font-weight: 600; }
.inventory-lookup .stock-low { color: var(--warning); }
.inventory-lookup .stock-out { color: var(--error); font-weight: bold; }
.inventory-lookup .stock-na { color: var(--text-on-dark-secondary); font-style: italic; }
.inventory-lookup .results-note { color: var(--text-on-dark-secondary); font-size: var(--font-size-sm); font-style: italic; }
.inventory-lookup .placeholder { color: var(--text-on-dark-secondary); font-style: italic; padding: var(--spacing-lg); text-align: center; }
.inventory-lookup .inventory-stats { background: var(--surface-light); padding: var(--spacing-md); border-radius: var(--radius-sm); }
.inventory-lookup .inventory-stats h4 { margin-top: 0; margin-bottom: var(--spacing-md); }
.inventory-lookup .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: var(--spacing-md); }
.inventory-lookup .stat-item { text-align: center; padding: var(--spacing-md); background: var(--surface-base); border-radius: var(--radius-sm); }
.inventory-lookup .stat-value { display: block; font-size: var(--font-size-h2); font-weight: bold; color: var(--primary-accent); }
.inventory-lookup .stat-label { display: block; font-size: var(--font-size-sm); color: var(--text-on-dark-secondary); }
.inventory-lookup .stat-warning .stat-value { color: var(--warning); }
.inventory-lookup .stat-danger .stat-value { color: var(--error); }
@media (max-width: 768px) {
    .inventory-lookup .inventory-results { overflow-x: auto; }
    .inventory-lookup .stats-grid { grid-template-columns: repeat(2, 1fr); }
}

/* AutoFetch options must escape the table scroll containers — the same
   contract as the .action-disclosure[open] rule above: an overflow-x:auto
   box clips vertically too, trapping the options dropdown inside entry-line
   tables (opening stock lines, 2026-08-25). Un-clip while a picker inside
   has its options list open; scroll returns when it closes. */
.table-container:has(.autofetch-option),
.js-table-scroll-wrap:has(.autofetch-option),
.table-responsive:has(.autofetch-option) {
    overflow: visible;
}

/* AutoFetch un-clip v2 (2026-08-25): the ELEMENT table rule is the outer
   clip — tables carry overflow:hidden for corner radius, so a dropdown in a
   one-row entry table had zero room. Same focus contract as the scroll-wrap
   escapes (gate = options PRESENT, not input focus: focus drops on the
   selection mousedown and re-clipping mid-click ate the click — the list
   empties on close, so presence is the correct lifetime). And options get
   a readable floor width: left+right pinning made
   them column-width slivers inside narrow entry-table cells. */
table:has(.autofetch-option),
.row-grid:has(.autofetch-option) { overflow: visible; }   /* a picker on a report row (Q48) */
.autoFetchOptions { min-width: min(24rem, 85vw); }

/* ==========================================================================
   Variant attribute fine-print list (2026-08-31) - emitted by
   renderVariantAttrList() under variant names (admin stock-view x5,
   FanCraft showcase cards): one attribute per line, indented, fine print.
   ========================================================================== */
ul.variant-attrs {
    margin: 0.15rem 0 0;
    padding: 0 0 0 1.1em;
    list-style: none;
    font-size: 0.82em;
    opacity: 0.85;
}
ul.variant-attrs li { margin: 0; padding: 0; }

/* ==========================================================================
   PUBLIC-FORM GUARD + FEEDBACK & POLLS (framework, identical x5)
   ────────────────────────────────────────────────────────────────────────
   .hp-trap: the honeypot wrapper publicFormGuardFields() emits. Off-screen,
   never display:none (bots skip hidden fields, humans never see this).
   .feedback-*: the one presenter every feedback form / poll renders through
   (sections/feedback/helpers.php). Element-first; token colours only.
   ========================================================================== */
.hp-trap {
    position: absolute;
    left: -9999px;
    width: 0;
    height: 0;
    overflow: hidden;
}

.feedback-banner {
    margin: 0 0 var(--spacing-md);
}

.feedback-banner img {
    display: block;
    width: 100%;
    height: auto;
    border-radius: var(--radius-md, 0.5rem);
}

.feedback-intro {
    margin-bottom: var(--spacing-md);
}

.feedback-question legend {
    font-size: 1.1em;
    font-weight: 600;
}

.feedback-choice {
    display: block;
    padding: 0.35rem 0;
    cursor: pointer;
}

.feedback-choice input {
    margin-right: 0.5rem;
}

.feedback-identity .form-text {
    margin-bottom: var(--spacing-sm);
}

.feedback-thanks {
    padding: var(--spacing-md);
    border-left: 4px solid var(--success);
    background: color-mix(in srgb, var(--success) 12%, transparent);
    border-radius: var(--radius-md, 0.5rem);
}

.feedback-thanks h3 {
    margin-top: 0;
}

.feedback-home .feedback-entry + .feedback-entry,
.feedback-home > div + div {
    margin-top: var(--spacing-lg);
}

/* Response quality tier chips (admin report) */
.fb-tier {
    display: inline-block;
    padding: 1px 0.5rem;
    border-radius: 999px;
    font-size: var(--font-size-xs);
    font-weight: 700;
    letter-spacing: 0.04em;
    border: 1px solid currentColor;
    background: color-mix(in srgb, currentColor 18%, transparent);
    white-space: nowrap;
}

.fb-tier--member     { color: var(--success); }
.fb-tier--identified { color: var(--primary-accent); }
.fb-tier--anonymous  { color: var(--surface-text); opacity: 0.75; }

/* Vote-or-write-in (answerType=openvote): the approved list is a wrapping
   grid that survives a long list (scroll box + quick filter above 12), the
   write-in box sits under it. Admin review rows share the row look. */
.feedback-votes-lead { margin: 0 0 var(--spacing-xs); font-weight: 600; }

.feedback-vote-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: var(--spacing-xs) var(--spacing-md);
    max-height: 50vh;
    overflow: auto;
    padding: var(--spacing-sm);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-lighter, rgba(255, 255, 255, 0.04));
}

.feedback-vote-grid .feedback-choice { margin: 0; }

.feedback-writein { margin-top: var(--spacing-md); }
.feedback-writein small { font-weight: normal; opacity: 0.8; }

.fb-review { margin-top: var(--spacing-lg); }
.fb-review-row {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--spacing-sm) var(--spacing-md);
    padding: var(--spacing-sm) 0;
    border-bottom: 1px solid var(--border);
}
.fb-review-row .fb-review-text { flex: 1 1 220px; }
.fb-review-row .fb-review-text strong { display: block; }
.fb-review-row .form-group { margin: 0; min-width: 160px; }
.fb-review-row .fb-review-actions { display: flex; flex-wrap: wrap; gap: var(--spacing-xs); }

/* Daily Tasks: one collapsible per feedback entry, nested inside the step */
.fb-task-entry { margin: var(--spacing-sm) 0; }
.fb-task-entry > summary { font-weight: normal; }
.fb-task-actions { margin: var(--spacing-sm) 0; }
.fb-task-done > summary { font-size: var(--font-size-sm); cursor: pointer; }


/* ═══════════════════════════════════════════════════════════════════════════
   SITE TRAFFIC admin (framework-traffic-module.md §5). The visit list is the
   EAD chapter/scene shape on the element details/summary baseline: one
   <details.traffic-day> per day, one .traffic-row per visit, the drill-down
   rendered into the row's own inline slot. Stat/fact lists are <dl> grids.
   ═══════════════════════════════════════════════════════════════════════════ */
.traffic-stats, .traffic-facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    gap: var(--spacing-sm);
    margin: var(--spacing-sm) 0 var(--spacing-md);
}
.traffic-stats > div, .traffic-facts > div { min-width: 0; }
.traffic-stats dt, .traffic-facts dt { font-size: var(--font-size-xs); opacity: 0.75; margin: 0; }
.traffic-stats dd { font-size: 1.4rem; font-weight: 600; margin: 0; }
.traffic-facts dd { margin: 0; overflow-wrap: anywhere; }
.traffic-facts { grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); }
.traffic-ua { font-size: var(--font-size-sm); }

.traffic-tables { display: flex; flex-wrap: wrap; gap: var(--spacing-md); align-items: flex-start; margin-bottom: var(--spacing-md); }
.traffic-tables table { flex: 1 1 18rem; margin: 0; }

.traffic-day > summary { display: flex; flex-wrap: wrap; gap: var(--spacing-xs) var(--spacing-sm); align-items: baseline; }
.traffic-day-title { font-weight: 600; }
.traffic-day-meta { font-size: var(--font-size-sm); opacity: 0.8; }

.traffic-visits { list-style: none; margin: 0; padding: 0 var(--spacing-sm) var(--spacing-sm); }
.traffic-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--spacing-xs) var(--spacing-sm);
    padding: var(--spacing-xs) 0;
    border-bottom: 1px solid var(--border-light);
}
.traffic-row:last-child { border-bottom: 0; }
.traffic-time { flex: 0 0 5rem; font-variant-numeric: tabular-nums; opacity: 0.8; }
.traffic-who { flex: 1 1 12rem; font-weight: 600; }
.traffic-who small, .traffic-detail small { font-weight: normal; opacity: 0.75; }
.traffic-sum { flex: 2 1 14rem; font-size: var(--font-size-sm); }
.traffic-row .inline-edit-slot { flex: 1 0 100%; }

/* Role / classification chips are the framework .badge set (no traffic-specific chip). */

.traffic-detail { padding: var(--spacing-sm); margin: var(--spacing-xs) 0; border: 1px solid var(--border-light); border-radius: var(--radius-md); }
.traffic-hits td:last-child, .traffic-hits th:last-child { text-align: right; font-variant-numeric: tabular-nums; }
.traffic-hits tr.is-ping td, .traffic-hits tr.is-sync td { opacity: 0.6; }
.traffic-detail-actions { display: flex; flex-wrap: wrap; gap: var(--spacing-xs); margin-top: var(--spacing-sm); }

@media (max-width: 700px) {
    .traffic-time { flex-basis: 4rem; }
    .traffic-sum { flex-basis: 100%; }
}
.traffic-bounces { margin: 0 var(--spacing-sm) var(--spacing-sm); }
.traffic-bounces > summary { font-weight: normal; }
.traffic-row-empty { opacity: 0.7; font-style: italic; }

/* Profile page (pages/admin/profile.php): the account-status tiles. */
.profile-status-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1rem;
}

/* Checkbox with its label on one line (login "Remember me", report options). */
.checkbox-label { display: inline-flex; align-items: center; gap: 0.4rem; font-weight: normal; }
.checkbox-label input[type="checkbox"] { width: auto; }
