/* ===================================================================
 * Core control layer — the one definition of a field, and of a button.
 *
 * Text/number inputs, selects, their labels and grouping wrappers, the
 * segmented pill, and the `.core-btn` family. Split out of
 * core-configurator.css (efficiencies #228 for the fields, #229 for the
 * buttons) so the controls have a home that is not tied to the
 * configurator shell: the room planner emits the same control markup
 * but is a different surface, and its sheets had each grown their own
 * verbatim copy of this box model (three of them, plus this file) —
 * which is how one border decision in the 2026-08-10 Rule Interactive
 * migration had to be made four times.
 *
 * Registered as the `core-controls` handle beside `core-tokens` and
 * `core-modal`, the other standalone shared primitives. Declared as a
 * dep by `core-configurator` and by `core-room`, so both surfaces get
 * it first and their own sheets still win on any selector they share.
 * A surface that renders Core control markup declares this handle;
 * it must never restate these values locally.
 *
 * Colour, radius, type and motion tokens come from the theme.
 * =================================================================== */

/* --- Form Fields --- */

.core-input-group {
    margin-bottom: 15px;
}

.core-input-group:last-child {
    margin-bottom: 0;
}

/* --- Labelled subsection ---
   Core_Control_Renderer's `section` descriptor: a fieldset around a run of
   controls, for a step that asks the same question twice about two different
   things — the front's Frame and Panel catalogues, each a type → material →
   thickness cascade. The legend is what lets the inner labels shorten to
   "Material type" / "Material" / "Thickness" without going ambiguous, so it is
   carrying meaning here rather than decorating.

   `min-width: 0` is load-bearing: a fieldset defaults to
   `min-inline-size: min-content`, which ignores the 360px control rail and lets
   one long option name push the whole column wider. The border is `--rule`, not
   `--rule-interactive` — a subsection frame is structure, not something the
   customer operates. */
.core-control-section {
    margin: 0 0 15px;
    padding: 14px 16px 16px;
    min-width: 0;
    border: 1px solid var(--rule);
    border-radius: var(--r-md);
}

.core-control-section:last-child {
    margin-bottom: 0;
}

/* A quiet group name, not a second heading competing with the step title. The
   uppercase is presentational: the authored string stays sentence case, per the
   microcopy guide.

   The bulk tables' "Project details" fieldset joins here rather than restating
   these values in core-bulk-table.css — the `.door-field` precedent from
   efficiency #223. That sheet reaches this one through
   core-bulk-table → core-configurator → core-controls, so these print first and
   it keeps only the fieldset's own box (padding/margin) locally. */
.core-control-section legend,
.core-bulk-project-meta legend {
    padding: 0 6px;
    font-size: var(--fs-13);
    font-weight: var(--fw-semibold);
    color: var(--ink-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Door's fields are typed rather than classed (`.door-field label`) — its
   appearance markup is Door's, rendered on a surface where Door's own sheet
   isn't loaded. It joins the Core block rather than re-declaring these
   values (efficiency #223). */
/* The segmented control's group title and the handle card's label wear the
   same body — one definition of a field label. */
.core-input-group label,
.door-field label,
.core-segmented-label,
.core-handle-label {
    display: block;
    margin-bottom: 5px;
    font-weight: var(--fw-regular);
    color: var(--ink-soft);
    font-size: var(--fs-13);
}

/* Core's padding is canonical — Door had drifted to 8px 10px (#223). The
   planner's free-type boxes carry `.core-input` / `.core-select` on the
   element for the same reason: one box model, one place (#228). */
.core-input,
.core-select,
.door-field input[type="number"],
.door-field select {
    width: 100%;
    /* §7.2 / §5.4 — every field is 44px, the configurator rail included
       (ruled 2026-09-27). A min-height so a wrapping select never clips;
       the one sanctioned smaller rung is a dense table row, which sizes its
       own controls (core-bulk-table.css). */
    min-height: var(--h-control);
    padding: 8px 12px;
    border: 1px solid var(--rule-interactive);
    border-radius: var(--r-sm);
    font-size: var(--fs-14);
    background: var(--paper);
    transition: border-color var(--dur-base) var(--ease);
    box-sizing: border-box;
}

.core-input:focus,
.core-select:focus,
.door-field input[type="number"]:focus,
.door-field select:focus {
    outline: none;
    border-color: var(--ink);
    box-shadow: var(--focus-ring);
}

/* Checkbox fields */
.core-field-checkbox label,
.door-field-checkbox label {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: var(--fs-13);
    cursor: pointer;
}

/* Radio groups */
.core-radio-group {
    display: flex;
    gap: 16px;
}

.core-radio-group label {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: var(--fs-13);
    cursor: pointer;
    font-weight: var(--fw-regular);
}

/* Help text */
.core-help-text {
    display: block;
    margin-top: 5px;
    font-size: var(--fs-12);
    color: var(--ink-soft);
}

/* A live reading under a control ("Auto fits 9 shelves at this height") —
   one step quieter than help text, because it restates what a choice will
   do rather than a limit the customer must respect. */
.core-help-text--hint {
    font-size: var(--fs-11);
    color: var(--ink-muted);
}

/* The D24 Matching reminder — a resolved VALUE, not a hint, so it reads at
   full ink weight rather than in the muted help colour beside it. */
.core-eb-match-name {
    color: var(--ink);
    font-weight: var(--fw-semibold);
}

/* =========================================================
   "What's this?" — setting help (Design.md §7.23)
   Markup: Core_Info_Help; behaviour: core-info-help.js.
   The group carries `data-has-info`; its label and the trigger sit as
   SIBLINGS in `.core-label-row` (never the button inside a <label>), and
   the `.core-info` panel follows the row. `hidden` is the only open/closed
   state — nothing here sets `display` on `.core-info`, only where an open
   panel sits.

   Every selector is anchored on `[data-has-info]` (or is the uniquely named
   `.core-sr-only`): the Theme loads this sheet on EVERY page
   (Theme/inc/enqueue.php), and none of this may reach a surface that is
   not a configurator.

   Default = touch / no hover: the words are always visible and the panel
   opens INLINE between the label row and the control. The hover query
   turns it into a floating pop-out the group reveals on hover or focus.
   Reduced motion is the Theme's global reset (Design.md §6.2).
   ========================================================= */

/* The plugin's own visually-hidden utility — not the Theme's `.sr-only`,
   because the plugin ships separately and must not lean on a Theme class. */
.core-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* The pop-out's anchor: neither `.core-input-group` nor `.door-field` is
   positioned, and the pop-out right-aligns to its own group. */
[data-has-info] {
    position: relative;
}

/* The row owns the 5px gap the label used to carry, so the row is exactly
   as tall as the label was. */
[data-has-info] .core-label-row {
    display: flex;
    align-items: baseline;
    gap: var(--sp-3);
    margin-bottom: 5px;
}

[data-has-info] .core-label-row > label,
[data-has-info] .core-label-row > .core-segmented-label {
    flex: 1 1 auto;
    min-width: 0;
    margin-bottom: 0;
}

/* An inline disclosure, deliberately not underlined and not on the 32px
   rung — §7.18's text-button rule is for action buttons (§7.23). */
[data-has-info] .core-info-trigger {
    position: relative;
    flex: 0 0 auto;
    margin: 0 0 0 auto;
    padding: 0;
    min-height: 0;
    border: 0;
    background: none;
    box-shadow: none;
    font-family: inherit;
    font-size: var(--fs-12);
    font-weight: var(--fw-regular);
    line-height: 1.2;
    letter-spacing: normal;
    color: var(--ink-muted);
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    transition: color var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}

[data-has-info] .core-info-trigger:hover,
[data-has-info] .core-info-trigger[aria-expanded="true"] {
    color: var(--ink);
}

/* The site's standard keyboard ring for a text control (design-base.css,
   `.core-switch`). */
[data-has-info] .core-info-trigger:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: var(--r-xs);
}

/* 44px touch target (§5.4's floor) from an invisible hit area, so the row's
   visual height never grows. It grows UPWARD from just inside the row's 5px
   bottom margin — never down over the control beneath (a centred box reached
   ~15px into it) — and leftward from the right edge; the words are wider
   than 44px, so it never covers the label. Upward it spends the 15px gap
   above the group. Switched off in the hover query below. */
[data-has-info] .core-info-trigger::after {
    content: "";
    position: absolute;
    right: 0;
    bottom: -4px;
    width: max(100%, 44px);
    height: 44px;
}

/* Inline panel (touch): Pebble, in normal flow under the label row. */
[data-has-info] .core-info {
    box-sizing: border-box;
    margin: 0 0 var(--sp-2);
    padding: var(--sp-3);
    background: var(--paper-soft);
    border: 1px solid var(--rule);
    border-radius: var(--r-md);
}

[data-has-info] .core-info__img {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: contain;
    margin: 0 0 var(--sp-2);
    background: var(--paper-soft);
}

[data-has-info] .core-info__title {
    margin: 0 0 var(--sp-1);
    font-size: var(--fs-13);
    font-weight: var(--fw-semibold);
    color: var(--ink);
}

[data-has-info] .core-info__text {
    margin: 0;
    font-size: var(--fs-13);
    line-height: 1.5;
    color: var(--ink-soft);
}

@media (hover: hover) and (pointer: fine) {
    /* Hidden until the setting is hovered or holds focus, or while open.
       Still in the tab order: focusing it opens the pop-out. */
    [data-has-info] .core-info-trigger {
        opacity: 0;
    }

    [data-has-info]:hover .core-info-trigger,
    [data-has-info]:focus-within .core-info-trigger,
    [data-has-info] .core-info-trigger[aria-expanded="true"] {
        opacity: 1;
    }

    [data-has-info] .core-info-trigger::after {
        content: none;
    }

    /* Floating pop-out, Raised (rung 2). `top` stays auto, so it sits at its
       static position — straight under the label row — and nothing reflows. */
    [data-has-info] .core-info {
        position: absolute;
        right: 0;
        z-index: var(--z-raised);
        width: 250px;
        max-width: 100%;
        margin: var(--sp-1) 0 0;
        background: var(--paper);
        box-shadow: var(--shadow-2);
    }

    /* core-info-help.js adds this when the pop-out would run off the bottom
       of its scroll box (.config-step): it sits above the label row instead. */
    [data-has-info] .core-info.core-info--above {
        bottom: 100%;
        margin: 0 0 var(--sp-1);
    }

    [data-has-info] .core-info__img {
        aspect-ratio: 4 / 3;
    }
}

@media (prefers-contrast: high) {
    .core-input:focus,
    .core-select:focus {
        border-color: var(--ink);
    }
}

/* Reduced motion is handled globally — Theme/assets/css/main.css's
   `*, *::before, *::after` reset under `prefers-reduced-motion: reduce`
   zeroes every animation/transition duration with `!important`. */

/* =========================================
   Segmented Control — one pill control, two backings
   The single canonical segmented control. Every visual rule below is
   shared; only the checked-state selectors differ by input type.

   • Radio backing (N options, e.g. Overlay/Inset, Hinge side, shelf
     count): one hidden radio nested inside each option label.
       <div class="core-segmented" role="radiogroup" aria-label="...">
         <label class="core-segmented__option">
           <input type="radio" name="..." value="a" checked><span>A</span>
         </label>
         <label class="core-segmented__option">
           <input type="radio" name="..." value="b"><span>B</span>
         </label>
       </div>

   • Checkbox backing (binary Yes/No, e.g. Edge Banding, Face Frame):
     see the ".core-segmented--binary" block further down — one hidden
     checkbox drives the --off / --on segments so the .checked contract
     every reader depends on is preserved.

   The native input is visually hidden but still focusable and
   form-submittable, so existing change handlers keep working.
   ========================================= */

.core-segmented {
    display: flex;
    width: 100%;
    border: 1px solid var(--rule-interactive);
    border-radius: var(--r-sm);
    background: var(--paper);
    overflow: hidden;
    margin: 0;
    padding: 0;
    font-family: inherit;
}

/* Scope via .core-segmented to beat host-plugin label rules
   (e.g. `.door-field label { margin-bottom: 4px; }` in door-common.css,
   which would otherwise leave a strip of container background below
   the active segment). */
.core-segmented .core-segmented__option {
    flex: 1 1 0;
    margin: 0;
    padding: 0;
    display: block;
    position: relative;
    cursor: pointer;
    min-width: 0;
    font-size: inherit;
    font-weight: inherit;
    color: inherit;
}

.core-segmented .core-segmented__option + .core-segmented__option {
    border-left: 1px solid var(--rule-interactive);
}

/* Visually hide the native input (radio nested in each option, or the
   single sibling checkbox of the binary variant) but keep it focusable */
.core-segmented__option input[type="radio"],
.core-segmented input[type="checkbox"] {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    border: 0;
    opacity: 0;
    white-space: nowrap;
}

.core-segmented__option span {
    /* §7.9 — the control sits on the 44px field rung (ruled 2026-09-29), the
       same as the inputs it shares the rail with. The span carries the height
       inside the container's 1px border, so the pill measures 44px whole; a
       min-height rather than a height so a label that wraps grows the row
       instead of clipping. Both backings paint through this span. */
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: calc(var(--h-control) - 2px);
    box-sizing: border-box;
    padding: 4px 12px;
    text-align: center;
    font-size: var(--fs-14);
    font-weight: var(--fw-regular);
    color: var(--ink-muted);
    background: var(--paper);
    transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
    user-select: none;
    -webkit-user-select: none;
}

.core-segmented__option:hover input[type="radio"]:not(:checked) + span {
    background: var(--paper-sunk);
    color: var(--ink);
}

/* Active segment: solid accent fill (maintainer ruling, 2026-08-10 — reverses
   the Coal-invert treatment #794 introduced; the black fill was rejected on
   look). --accent-fg carries the label and is per-site: Paper everywhere
   except Cabinet, whose Corn is light enough that Paper reads at 2.05:1 and
   which therefore flips to Coal (7.98:1) in main.css.
   KNOWN AA SHORTFALL, accepted with the ruling: Desk tops out at 4.12:1 and
   Door at 4.49:1 against a 4.5:1 floor — no text colour passes on either
   (Design.md §3.5 has the full measurement). Waived and pinned in
   tests/js/Theme/design-contrast.test.js; the other six sites pass. */
.core-segmented__option input[type="radio"]:checked + span {
    background: var(--accent);
    color: var(--accent-fg);
    font-weight: var(--fw-regular);
}

/* Keyboard focus ring — wraps the WHOLE control, not the segment (§7.9), and
   is shared with the checkbox backing below. :has(:focus-visible) (not
   :focus-within) so the ring only shows for keyboard focus — a mouse click on
   a segment won't trigger it. One rule for both backings: the pill is the
   focusable unit, and arrow-keying a radio group moves the fill, not the
   ring. */
.core-segmented:has(input:focus-visible) {
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-tint);
}

/* Disabled state (future-proofing) */
.core-segmented__option input[type="radio"]:disabled + span {
    opacity: 0.5;
    cursor: not-allowed;
}

/* =========================================================
   Checkbox-backed binary variant (Yes/No)
   The same .core-segmented pill, but a single hidden
   <input type="checkbox"> drives two --off / --on option
   segments, so the data-field / value="yes" / .checked
   contract every reader (readFaceFrameControls, the carcass UI
   handlers, the server extract's checkbox_truthy) depends on is
   preserved. All the container / option / hidden-input / pill
   styling is inherited from the shared rules above — only the
   checked-state selectors below are checkbox-specific.
   Markup (Core_Control_Renderer::render_checkbox):
     <div class="core-input-group">
       <span class="core-segmented-label">Add a face frame</span>
       <div class="core-segmented core-segmented--binary">
         <input type="checkbox" id="…" data-field="…" value="yes">
         <label for="…" class="core-segmented__option core-segmented__option--off"><span>No</span></label>
         <label for="…" class="core-segmented__option core-segmented__option--on"><span>Yes</span></label>
       </div>
     </div>
   The active segment sets pointer-events:none so a click always
   lands on the inactive segment's <label for>, flipping the
   checkbox to that state (correct segmented behaviour from one
   checkbox, no JS).
   ========================================================= */

/* Group title for the binary variant. The radio variant's title is a
   bare <label> styled by `.core-input-group label`; the checkbox
   variant's is a <span> (the aria-labelledby target — a <label for>
   would compete with the segment labels), so it can't ride that
   selector — it joins the field-label rule at the top of this sheet
   by class instead. */

/* Active segment: the SAME accent fill the radio backing takes above — one
   control, one active state. See that rule for the ruling and the AA waiver. */
.core-segmented input[type="checkbox"]:checked ~ .core-segmented__option--on span,
.core-segmented input[type="checkbox"]:not(:checked) ~ .core-segmented__option--off span {
    background: var(--accent);
    color: var(--accent-fg);
    font-weight: var(--fw-regular);
}

/* …and unclickable, so a click can only ever land on the other
   segment's <label for>, which flips the checkbox to it. */
.core-segmented input[type="checkbox"]:checked ~ .core-segmented__option--on,
.core-segmented input[type="checkbox"]:not(:checked) ~ .core-segmented__option--off {
    pointer-events: none;
}

.core-segmented:hover input[type="checkbox"]:checked ~ .core-segmented__option--off span,
.core-segmented:hover input[type="checkbox"]:not(:checked) ~ .core-segmented__option--on span {
    background: var(--paper-sunk);
    color: var(--ink);
}

/* Keyboard focus ring: shared with the radio backing — see the
   `.core-segmented:has(input:focus-visible)` rule above. */

.core-segmented input[type="checkbox"]:disabled ~ .core-segmented__option span {
    opacity: 0.5;
    cursor: not-allowed;
}

/* =========================================================
   Buttons — Design.md §7.1
   The one button family in the plugin: 44px tall, 20px side
   padding, 14px text, 4px radius, 1px border, and a 1px
   press depression. Primary is Coal, never accent — accent
   means focus and state, and a second meaning of "press me"
   would leave two competing signals and no winner (§3.6).

   Moved here from core-configurator.css (efficiency #229)
   because the family was only reachable behind the
   `core-configurator` handle. That is precisely why three
   surfaces with no configurator shell — the saved-designs
   list, the trade application form and the materials index —
   each hand-rolled their own button body: `.core-btn` was
   not available to them. A surface that renders a button
   declares `core-controls` and adds a variant; it must never
   restate the box model locally.

   Two deliberate deviations from §7.1, both documented
   rather than accidental:

   - The base is FULL WIDTH, not auto. §7.1 treats full width
     as a size variant, but the plugin's buttons overwhelmingly
     sit in the 360px control rail where full width IS the
     default, and ~50 markup sites already rely on it. Variants
     opt out with `width: auto` rather than the base opting in.
   - Height is a `min-height`, not a fixed `height`. Same 44px
     for a single-line label, but a long label in a 360px rail
     wraps instead of clipping. The theme's `.btn` can use a
     fixed height because it sits in full-page-width contexts.
   ========================================================= */

/* The driver.js coach-mark popover's Back / Next / Done buttons and its corner
   × are joined into the family here (efficiency #229). They are vendor markup —
   driver.js builds the popover itself, so no class can be added. The library's
   own `driver.css` is not loaded (2026-09-29), so nothing competes with these
   rules; placement stays in `core-tutorial.css`, which carries no button body. */
.core-btn,
.driver-popover-close-btn,
.driver-popover-footer .driver-popover-navigation-btns button {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    width: 100%;
    min-height: var(--h-control);
    padding: 0 var(--sp-5);
    border: 1px solid transparent;
    border-radius: var(--r-sm);
    font-family: inherit;
    font-size: var(--fs-14);
    font-weight: var(--fw-regular);
    /* Declared, not inherited: a `<button>` does not inherit letter-spacing
       from the page, so leaving this off resolves to `normal` and every label
       sits 2–7px wider than the same text in the theme's `.btn`. Same reason
       `box-sizing` and `text-decoration` are declared below — the plugin is a
       separate release unit and must not rely on a host theme's inheritance. */
    letter-spacing: var(--tracking-normal);
    text-align: center;
    cursor: pointer;
    /* Several consumers render the family on an `<a>` (the saved-designs Edit
       and Load links, the materials index Configure link). Declared on the base
       so an anchor button never has to restate it locally. */
    text-decoration: none;
    /* Declared rather than inherited from the theme's `*` reset, for the same
       reason `.core-input` above declares it: the plugin is a separate release
       unit and a full-width button with 20px padding and a 1px border would
       overflow its 360px rail on a theme with no reset. */
    box-sizing: border-box;
    transition: background var(--dur-fast) var(--ease),
                color var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease),
                transform var(--dur-fast) var(--ease);
}

/* §6.3 — a button depresses by exactly 1px on press, and that is the whole
   feedback. The pre-#229 family lifted 1px on HOVER instead, which read as
   the control backing away from the cursor and left `:active` cancelling a
   transform rather than expressing one. */
.core-btn:active,
.driver-popover-close-btn:active,
.driver-popover-footer .driver-popover-navigation-btns button:active {
    transform: translateY(1px);
}

.core-btn:disabled {
    cursor: not-allowed;
    transform: none;
}

.core-btn-primary,
.driver-popover-footer .driver-popover-navigation-btns :is(.driver-popover-next-btn, .driver-popover-done-btn) {
    background: var(--ink);
    color: var(--paper);
    border-color: var(--ink);
}

.core-btn-primary:hover,
.driver-popover-footer .driver-popover-navigation-btns :is(.driver-popover-next-btn, .driver-popover-done-btn):hover {
    background: oklch(from var(--ink) calc(l + 0.08) c h);
}

.core-btn-primary:disabled {
    background: var(--rule-strong);
    border-color: var(--rule-strong);
}

.core-btn-secondary,
.driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn {
    background: var(--paper);
    color: var(--ink);
    border-color: var(--rule-interactive);
}

/* §7.1 — the border darkens to Ink on hover, and that is all: no fill, the
   same as the Theme's `.btn--secondary`. */
.core-btn-secondary:hover,
.driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn:hover {
    border-color: var(--ink);
}

.core-btn-secondary:disabled {
    background: var(--paper);
    color: var(--ink-muted);
    border-color: var(--rule-interactive);
}

/* §7.1 Danger — Paper fill with Error text and border, inverting to a solid
   Error fill on hover. Destructive actions only; a delete that looks like every
   other secondary button is the one case where "everything else" is wrong. */
.core-btn--danger {
    background: var(--paper);
    color: var(--err);
    border-color: var(--err);
}

.core-btn--danger:hover {
    background: var(--err);
    color: var(--paper);
    border-color: var(--err);
}

/* §7.1 Ghost — no fill and no border, for the quiet half of a button pair
   (Back beside Continue, Cancel beside Add to basket). Tighter side padding
   than the filled variants, since there is no border to sit inside. */
.core-btn--ghost {
    background: transparent;
    color: var(--ink);
    padding: 0 var(--sp-3);
}

.core-btn--ghost:hover {
    background: var(--paper-soft);
}

/* Pins Primary to Coal under forced-contrast, where an accent fill would be
   the tempting "make it louder" answer and is exactly the wrong one (§3.6). */
@media (prefers-contrast: high) {
    .core-btn-primary {
        background: var(--ink);
    }
}

/* --- Size and width modifiers (Design.md §7.1) -----------------------
   §7.1's sanctioned sizes are Large 52 / Default 44 / Small 32 (2px radius) /
   Icon / Full width. The plugin base is already Default and full width (see
   the header note), so the two the consumers actually reach for are Small and
   the full-width opt-out. Orthogonal on purpose: `--sm` says how big, `--inline`
   says how wide, and a colour variant says which. A surface that wants a 36px
   or 38px button does not get one — it picks the rung above or below, which is
   the whole point of a ladder. */

.core-btn--sm,
.driver-popover-footer .driver-popover-navigation-btns button {
    min-height: var(--h-btn-sm);
    padding: 0 var(--sp-3);
    font-size: var(--fs-13);
    border-radius: var(--r-xs);
}

/* The 52px top rung — for a primary action that stands alone. No plugin
   surface uses it today: the stepper's and the planner's "Add to basket" sit
   in 32px action bars, and §5.4 gives a row one height, so they take the row's
   rung rather than this one. */
.core-btn--lg {
    min-height: var(--h-btn-lg);
    padding: 0 var(--sp-6);
    font-size: var(--fs-16);
}

/* Square glyph-only form — modal and popover close buttons, the planner's
   canvas zoom pair. Shrink-to-fit like `--inline` (a square button must never
   stretch to its container), with the side padding dropped so the glyph
   centres in a box as wide as the base is tall. Pairs with `--sm` for the
   32px-tall rung; the width stays 44px there, matching the theme family this
   replaced rather than re-deciding the proportion.

   Not to be confused with `--close` below: `--icon` is a bordered control on
   the size ladder, for a glyph action that stands on its own (the planner's
   zoom pair, its full-size modal closes). `--close` is the small round ghost
   "×" that lives inside a card or a table cell. */
.core-btn--icon {
    display: inline-flex;
    width: var(--h-control);
    padding: 0;
}

/* The "×" dismiss glyph — Ghost's treatment at glyph size, on the same --r-sm
   square as every other control (§7.28, ruled 2026-09-29; it was a circle).
   Seven surfaces had each hand-rolled this body: Core's popup close, Desk's
   corner-rounding, cable-hole and edge-detail removes, the saved-designs modal
   close, and Sheet/Trim's bulk-table remove-row (efficiency #229). Two of them
   carried hardcoded hexes and one carried the whole body as an inline `style=`
   attribute with `onmouseover` hover handlers.

   Deliberately OFF §7.1's size ladder, which starts at Small 32px. A dismiss
   sits INSIDE the surface it closes — pinned in a popup's 8px padding, or in a
   modal header beside the title — where a 32px control would crowd its own
   host. 24px is the floor a pointer target may sit at, so it is the one rung
   below Small rather than a free number. A remove-row × is the exception: it
   shares its row with other controls, so it takes the row's height (Design.md
   §5.4) — core-bulk-table.css sizes Sheet/Trim's to the 32px row.

   Colour is the only thing a host may add, and only one pair does: Sheet's and
   Trim's remove-row tint their hover Error, because removing a priced row is
   not the same act as closing a popover. */
.core-btn--close,
.driver-popover-close-btn {
    display: inline-flex;
    width: 24px;
    min-height: 24px;
    padding: 0;
    background: transparent;
    border-color: transparent;
    border-radius: var(--r-sm);
    color: var(--ink-muted);
    font-size: var(--fs-16);
    line-height: 1;
}

.core-btn--close:hover,
.driver-popover-close-btn:hover {
    background: var(--paper-sunk);
    color: var(--ink-soft);
}

/* Shrink-to-fit, for buttons sitting in a row of siblings rather than filling
   the 360px control rail.

   `display: inline-flex` does the real work here and `width: auto` alone will
   not replace it. The base is a block-level flex box, and on a block-level box
   `width: auto` means *fill the containing block*, not *shrink to content* — so
   an `<a class="core-btn--inline">` in ordinary block flow stretches the full
   width of its parent. (A `<button>` hides the bug: form controls resolve
   `width: auto` to their intrinsic size, so the button consumers looked
   correct while the anchor ones only looked correct because they happened to
   sit in a flex row or a `justify-items: end` grid.) `inline-flex` is
   shrink-to-fit in every context and is blockified back to `flex` when the
   element is a flex or grid item, so it is safe in all of them.

   `nowrap` matches the theme's `.btn`: the base allows wrapping because a long
   label in a fixed 360px rail must wrap rather than clip, but a button sized to
   its own content has no reason to break its label. */
.core-btn--inline,
.driver-popover-footer .driver-popover-navigation-btns button {
    display: inline-flex;
    width: auto;
    white-space: nowrap;
}

/* --- Pinned preview overlay controls (Design.md §11.6) ---------------
   The reset-view button and Door's 2D reset-zoom: small
   controls floated over the preview canvas. Translucent Paper so the model
   reads through behind them, the 32px small rung of the ladder, and their
   own stacking context. Only the corner each one pins to is local — that
   genuinely differs per control, so it stays with the consumer.

   Pressed state inverts to Coal, never an accent fill behind text (§3.5). */

.core-btn--overlay {
    position: absolute;
    width: auto;
    min-height: var(--h-btn-sm);
    padding: 0 var(--sp-3);
    background: color-mix(in oklch, var(--paper) 92%, transparent);
    border-color: var(--rule-interactive);
    color: var(--ink-soft);
    font-size: var(--fs-12);
    line-height: 1.2;
    z-index: var(--z-raised);
}

.core-btn--overlay:hover {
    background: var(--paper);
    border-color: var(--ink);
    color: var(--ink);
}

.core-btn--overlay.active,
.core-btn--overlay[aria-pressed="true"] {
    background: var(--ink);
    border-color: var(--ink);
    color: var(--paper);
}

.core-btn--overlay:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

/* Square icon form of the overlay control — the reset-view glyph. */
.core-btn--overlay-icon {
    width: var(--h-btn-sm);
    padding: 0;
    font-size: var(--fs-14);
}

/* --- Drawer-heights control ---
   Core_Control_Renderer's `drawer_heights` descriptor + core-drawer-heights.js:
   a proportional strip of the drawer stack with a draggable divider between
   each adjacent pair of zones. Each zone is typed in DIRECTLY — the mount
   relocates its mm input into the zone beside the name, so there is no
   duplicate row of fields under the strip. The strip is operable (drag +
   arrow keys on each divider), so its frame is Rule Interactive; the zone
   fills stay flat Paper — no alternating banding — so the divider handles
   read as the thing to grab. Zone flex-grow is set inline from the live mm
   values. */
.core-drawer-heights {
    margin: 0 0 15px;
}

.core-drawer-heights__strip {
    position: relative;
    display: flex;
    flex-direction: column;
    height: 220px;
    border: 1px solid var(--rule-interactive);
    border-radius: var(--r-sm);
    background: var(--paper);
    overflow: hidden;
    margin-bottom: 12px;
}

.core-drawer-heights__zone {
    flex: 1 1 0;
    min-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 0 12px;
    background: var(--paper);
}

/* The inline zone input — part of the strip primitive, so its box model
   lives here rather than borrowing `.core-input`: a 44px control cannot fit
   a zone that may legitimately be ~33px tall (a 100mm drawer in a 220px
   strip), so this is the one deliberately compact field in the plugin.
   Native spinners are dropped for the same reason (§7.3's stepper rule). */
.core-drawer-heights__zone-input {
    width: 64px;
    height: 26px;
    padding: 0 6px;
    border: 1px solid var(--rule-interactive);
    border-radius: var(--r-xs);
    background: var(--paper);
    font-size: var(--fs-13);
    text-align: right;
    box-sizing: border-box;
    transition: border-color var(--dur-base) var(--ease);
    appearance: textfield;
    -moz-appearance: textfield;
}

.core-drawer-heights__zone-input::-webkit-outer-spin-button,
.core-drawer-heights__zone-input::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

.core-drawer-heights__zone-input:focus {
    outline: none;
    border-color: var(--ink);
    box-shadow: var(--focus-ring);
}

/* Static unit suffix beside the inline input (§7.2's unit-in-the-field,
   sized for the strip). */
.core-drawer-heights__unit {
    font-size: var(--fs-12);
    color: var(--ink-muted);
    pointer-events: none;
}

/* Distribution presets (Even / Graduated) under the strip. Placement only —
   the pill body comes from the shared `.core-segmented` rules above. Neither
   segment is checked once the customer drags a divider or types a height, so
   the pill reads as "no preset" rather than lying about the stack. */
.core-drawer-heights__presets {
    margin-bottom: 12px;
}

.core-drawer-heights__zone-label {
    font-size: var(--fs-13);
    color: var(--ink-soft);
    pointer-events: none;
    white-space: nowrap;
}

.core-drawer-heights__zone.is-short .core-drawer-heights__zone-label {
    color: var(--err);
}

/* The divider is a full-width horizontal grab line centred on the zone
   boundary. The visible rule is thin; the hit target is the padded box. */
.core-drawer-heights__divider {
    position: absolute;
    left: 0;
    right: 0;
    height: 18px;
    margin-top: -9px;
    cursor: row-resize;
    display: flex;
    align-items: center;
    justify-content: center;
    /* On the divider only — a finger on a zone still scrolls the page. */
    touch-action: none;
}

.core-drawer-heights__divider::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    border-top: 1px solid var(--rule-interactive);
}

.core-drawer-heights__grip {
    position: relative;
    width: 44px;
    height: 8px;
    border: 1px solid var(--rule-interactive);
    border-radius: var(--r-sm);
    background: var(--paper);
    transition: border-color var(--dur-fast) var(--ease);
}

.core-drawer-heights__divider:hover .core-drawer-heights__grip,
.core-drawer-heights__divider.is-dragging .core-drawer-heights__grip {
    border-color: var(--ink);
}

.core-drawer-heights__divider:focus-visible {
    outline: none;
}

.core-drawer-heights__divider:focus-visible .core-drawer-heights__grip {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

/* A stack whose minimums no longer fit the carcass (e.g. after a height
   change) — the strip flags it; the price call carries the actual message. */
.core-drawer-heights.is-overfull .core-drawer-heights__strip {
    border-color: var(--err);
}

/* --- Preview paused (WebGL context loss) ---------------------------------
   The panel CorePreview3D raises over a preview whose WebGL context has gone
   away (issue #337) — a lost context otherwise leaves the canvas holding its
   last frame with nothing on screen to say the model has stopped answering.

   It lives in THIS sheet rather than core-configurator.css because the panel
   belongs to every CorePreview3D consumer, and `core-controls` is the one
   frontend sheet all of them load: the product configurators enqueue it
   directly and the room planner's sheets declare it as a dep, while the
   configurator sheet is product-only. Its button is a `.core-btn`, defined
   above. */

.core-preview-paused {
    position: absolute;
    inset: 0;
    z-index: var(--z-overlay);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--sp-5);
    box-sizing: border-box;
    /* A wash rather than an opaque fill: the frozen last frame stays faintly
       visible behind the panel, so this reads as the same preview paused and
       not as a different screen. Derived from --core-preview-bg so it tracks
       whatever a product paints its preview surface. */
    background: color-mix(in oklch, var(--core-preview-bg) 88%, transparent);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
}

/* The panel is a flex container, which would otherwise win over [hidden]. */
.core-preview-paused[hidden] {
    display: none;
}

.core-preview-paused-panel {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-3);
    max-width: 320px;
    padding: var(--sp-5);
    box-sizing: border-box;
    text-align: center;
    background: var(--paper);
    border: 1px solid var(--rule);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-2);
}

/* §4.4 — a panel heading at this size is Semibold; it is not an <h*> because
   the preview sits mid-page and must not interrupt the document outline. */
.core-preview-paused-title {
    margin: 0;
    color: var(--ink);
    font-size: var(--fs-16);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-normal);
    line-height: var(--lh-snug);
}

.core-preview-paused-message {
    margin: 0;
    color: var(--ink-muted);
    font-size: var(--fs-13);
    letter-spacing: var(--tracking-normal);
    line-height: var(--lh-body);
}

/* =========================================================
   Switch — the one on/off switch (Design.md §7.9)
   A <button role="switch"> whose ::before is the 32×18 track (in flow,
   placed after the label with order: 2, so the gap always separates the
   two however long the label runs) and whose ::after is the 14px knob
   (absolute, flush over the track, which sits against the right padding
   edge — so it overlays without depending on the label width).

   Two surfaces wear it: the configurator's Advanced options switch
   (core-input-mode.js) and the Theme header's "Prices ex VAT" switch.
   They were two verbatim ~70-line copies until 2026-09-27; the Theme now
   loads this sheet for its header. Each keeps only its genuine
   divergences under its own class (the header's 32px min-height and its
   short/long label swap).

   ON is `aria-checked="true"` — or, for the VAT switch only,
   `<html data-vat="ex">`. That attribute is set by the Theme's head
   bootstrap before paint, whereas aria-checked only lands when the
   deferred vat-toggle.js runs, so keying the VAT switch's on state off
   the root stops an ex-VAT visitor seeing it flash off on a cached page.
   `data-vat` is already a shared Theme↔plugin contract (core-vat.js reads
   it), and `[data-vat-toggle]` is the switch's own JS hook.

   Reduced motion is handled globally by the Theme's main.css reset.
   ========================================================= */

.core-switch {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-1) var(--sp-2);
    border: 0;
    border-radius: var(--r-md);
    background: none;
    color: var(--ink-muted);
    font: inherit;
    font-size: var(--fs-12);
    font-weight: var(--fw-regular);
    letter-spacing: var(--tracking-tight);
    line-height: 1;
    white-space: nowrap;
    -webkit-appearance: none;
    appearance: none;
    cursor: pointer;
    transition: color var(--dur-base) var(--ease);
}

/* Track */
.core-switch::before {
    content: "";
    order: 2;
    flex: none;
    width: 32px;
    height: 18px;
    border-radius: var(--r-pill);
    background: var(--rule-strong);
    transition: background var(--dur-base) var(--ease);
}

/* Knob */
.core-switch::after {
    content: "";
    position: absolute;
    top: 50%;
    right: calc(var(--sp-2) + 16px);
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--paper);
    box-shadow: var(--shadow-1);
    transform: translateY(-50%);
    transition: transform var(--dur-base) var(--ease);
}

/* Hover — firm up the label and darken the off-state track slightly. */
.core-switch:hover {
    color: var(--ink);
}
.core-switch:hover::before {
    background: var(--ink-faint);
}

/* Keyboard focus ring in the brand accent (mouse clicks stay clean). */
.core-switch:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* On — accent track, knob slides right, label goes full-ink. */
.core-switch[aria-checked="true"],
:root[data-vat="ex"] .core-switch[data-vat-toggle] {
    color: var(--ink);
}
.core-switch[aria-checked="true"]::before,
:root[data-vat="ex"] .core-switch[data-vat-toggle]::before {
    background: var(--accent);
}
.core-switch[aria-checked="true"]:hover::before,
:root[data-vat="ex"] .core-switch[data-vat-toggle]:hover::before {
    background: color-mix(in oklch, var(--accent) 88%, var(--ink));
}
.core-switch[aria-checked="true"]::after,
:root[data-vat="ex"] .core-switch[data-vat-toggle]::after {
    transform: translateY(-50%) translateX(14px);
}

/* =========================================================
   Tab — the one step-tab (Design.md §7.15)
   Worn by the configurator's steps bar (`.steps-bar__step`) and the room
   planner's sub-step bar (`.core-room-substeps__tab`), which were two
   copies of the same underline/hover mechanism until 2026-09-27. Each bar
   keeps its own sizing, type and divider colour under its own class; this
   is only what makes a row of buttons read as tabs.

   A continuous 2px accent underline runs under every tab UP TO AND
   INCLUDING the current one. `:has(~ …)` reads "a later sibling is the
   current step", so it tracks aria-current exactly and needs no JS. The
   line is drawn INSIDE the tab (bottom: 0), never below it — a line
   protruding past the bar made the card scroll — and on every underlined
   tab after the first it starts 1px left, over the divider, so the run
   reads unbroken. Engines without :has() underline the current tab alone.
   ========================================================= */

.core-tab {
    position: relative;
    border: 0;
    background: transparent;
    cursor: pointer;
    font: inherit;
    color: inherit;
    transition: background var(--dur-fast) var(--ease);
}

.core-tab[aria-current="step"]::after,
.core-tab:has(~ .core-tab[aria-current="step"])::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 2px;
    background: var(--accent);
    pointer-events: none;
}

.core-tab + .core-tab[aria-current="step"]::after,
.core-tab + .core-tab:has(~ .core-tab[aria-current="step"])::after {
    left: -1px;
}

/* Clickable affordance on a non-current tab. */
.core-tab:hover:not([aria-current="step"]) {
    background: var(--paper-soft);
}

.core-tab:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

/* =========================================================
   Badge — Design.md §7.6
   22px, Label type, 2px radius, Pebble fill with a Rule border. Text is
   Ink Soft, not the Label role's Ink Muted: §3.3 measures Ink Muted at
   ≈4.3:1 on Pebble, under the 4.5:1 floor. The one badge for both units —
   the Theme's `.badge` and the room catalogue's bespoke copy (which had
   drifted to Semibold and 6px padding) folded into it on 2026-09-27.
   ========================================================= */

.core-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    height: var(--h-badge);
    padding: 0 var(--sp-2);
    box-sizing: border-box;
    font-size: var(--fs-11);
    font-weight: var(--fw-regular);
    letter-spacing: var(--tracking-label);
    line-height: 1;
    text-transform: uppercase;
    white-space: nowrap;
    border-radius: var(--r-xs);
    background: var(--paper-soft);
    color: var(--ink-soft);
    border: 1px solid var(--rule);
}

/* Accent: Accent Tint fill, the delivered accent as text, no border. Small
   text, so it reads weakly on the three light-accent sites (§3.5) — keep
   accent badge copy non-essential. */
.core-badge--accent {
    background: var(--accent-tint);
    color: var(--accent);
    border-color: transparent;
}

/* Dot: a 6px filled circle in the current text colour. */
.core-badge--dot::before {
    content: "";
    width: 6px;
    height: 6px;
    background: currentColor;
    border-radius: var(--r-pill);
}

/* =========================================================
   Table header — one quiet column header for every data table
   Regular weight, Ink Muted, a 1px Rule Strong line under the row, no fill
   (ruled 2026-09-27). The basket cut list and the Sheet / Trim bulk tables
   had drifted to two treatments — the bulk table's Semibold on a Pebble band
   over a 2px rule. Each table keeps its own cell padding, because the cut
   list sits flush left and the bulk table's cells hold controls.
   ========================================================= */

.core-table th {
    font-weight: var(--fw-regular);
    text-align: left;
    color: var(--ink-muted);
    border-bottom: 1px solid var(--rule-strong);
    white-space: nowrap;
}

/* =========================================================
   Choice card — one pick from a grid of named options
   Paper, a 1px Rule Interactive border, the small radius; hover darkens the
   border only, so no new text/background pairing. Picked is a 2px Ink border
   with the padding stepped in so nothing moves, and focus takes the accent
   border and ring. The zone editor's "Start from" presets wear it, with the
   fittings sketch as the glyph (the Style step's picture tiles keep their own
   box in core-style-step.css). The markup is a
   `<button role="radio" aria-checked>` inside a `role="radiogroup"`.
   ========================================================= */

.core-choice-card {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    min-width: 0;
    padding: var(--sp-3);
    border: 1px solid var(--rule-interactive);
    border-radius: var(--r-sm);
    background: var(--paper);
    color: var(--ink);
    font: inherit;
    text-align: left;
    cursor: pointer;
    transition: border-color var(--dur-fast) var(--ease);
}

.core-choice-card:hover {
    border-color: var(--ink-soft);
}

.core-choice-card[aria-checked="true"] {
    border: 2px solid var(--ink);
    padding: calc(var(--sp-3) - 1px);
}

.core-choice-card:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: var(--focus-ring);
}

/* The picture at the card's head — swatches, a sketch — never shrinks. */
.core-choice-card__glyph {
    display: flex;
    flex: none;
}

.core-choice-card__text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.core-choice-card__name {
    font-size: var(--fs-13);
    line-height: 1.3;
}

.core-choice-card__sub {
    font-family: var(--font-sans);
    font-size: var(--fs-11);
    letter-spacing: var(--tracking-normal);
    line-height: 1.35;
    color: var(--ink-muted);
}

/* =========================================================
   Stock status (D53, Design.md §7.19)
   One line under a configurator's thickness picker naming the
   chosen thickness's stock — mark + label, in both the segmented
   and the <select> shape (CoreStockStatus, via
   CoreSegmented.setOptions). The bulk tables carry the mark alone
   beside each row's select, under one key.
   Signal colours only, never the accent (§3.7); Warn is only ever
   the icon, never the text.
   ========================================================= */

.core-stock-line {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 6px 0 0;
    font-size: var(--fs-13);
    line-height: 1.4;
    color: var(--ink-soft);
}

.core-stock-icon {
    flex: none;
    display: block;
}

.core-stock-icon--in   { color: var(--ok); }
.core-stock-icon--lead { color: var(--warn); }
.core-stock-icon--long { color: var(--err); }

.core-stock-mark {
    display: inline-flex;
    vertical-align: middle;
    margin-left: 6px;
}

/* One line, right-aligned over the list (§7.19). */
.core-stock-key {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 4px 12px;
    margin: 0 0 8px;
    padding: 0;
    list-style: none;
    font-size: var(--fs-12);
    color: var(--ink-soft);
}

.core-stock-key__item {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
