/**
 * FRAMEWORK - UNIVERSAL CSS FOUNDATION
 * Rebuilt with zero-redundancy philosophy
 * Version: Restructured 2025
 */

/* ===== IMPORT OPTIONAL FEATURE FILES ===== */
@import 'alerts.css?v=2'; /* ?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=3'; /* bump when expanding-blocks.css changes — @imports cache independently (see alerts.css note above) */
@import 'content-highlights.css';
@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;

    /* 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);
}

/* ========================================================================
   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%;
}

/* ── .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;
}

.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__output {
    height: 60vh;
    min-height: 18rem;
    overflow-y: auto;
    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;
}

/* ────────────────────────────────────────────────────────────────────
   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);
}

/* 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);
}

.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);
    }

    .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-success { background: #d4edda; color: #155724; }
.badge-danger  { background: #f8d7da; color: #721c24; }
.badge-warning { background: #fff3cd; color: #856404; }
.badge-info    { background: #d1ecf1; color: #0c5460; }
.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); }

/* ────────────────────────────────────────────────────────────────────────
   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;
}

.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);
}

/* ── 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;
}
.currency-calc-trigger {
    position: absolute;
    right: 0.25rem;
    top: 50%;
    transform: translateY(-50%);
    background: transparent;
    border: 0;
    padding: 0.25rem;
    margin: 0;
    cursor: pointer;
    color: inherit;
    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: 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 {
    /* 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 {
    font-size: var(--font-size-xs);
    opacity: 0.8;
    line-height: 1.2;
    text-align: center;
}

/* ===== 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;
}

/* ═══ 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;
}

.pivot-scroll {
    overflow-x: auto;
    max-width: 100%;
}

/* ── 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. Generalizes the
   .pivot-fixed pattern; apply the class to the <table>. */
.sticky-first-col th:first-child,
.sticky-first-col td:first-child {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--surface-base);
}

.sticky-first-col thead th:first-child {
    background: var(--surface-lighter);
    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));
}

.pivot-table {
    font-size: var(--font-size-sm, 0.875rem);
}

.pivot-table th,
.pivot-table td {
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

.pivot-table .pivot-fixed {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--bg-base, #fff);
    max-width: 15rem;
    white-space: normal;
}

.pivot-table .pivot-year {
    text-align: center;
}

.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;
    }

    .stack-table,
    .stack-table tbody,
    .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);
}
