/* ===================================================================
 * 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,
.door-field {
    margin-bottom: var(--sp-4);
}

.core-input-group:last-child,
.door-field: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 var(--sp-4);
    padding: var(--sp-4);
    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 var(--sp-2);
    font-size: var(--fs-13);
    font-weight: var(--fw-semibold);
    color: var(--ink-muted);
    text-transform: uppercase;
    letter-spacing: var(--tracking-label);
}

/* 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. The room planner's unit
   popover and the saved-designs modals build their fields in JS without the
   group class, so their labels join by selector (efficiency #628). */
.core-input-group label,
.door-field label,
.core-segmented-label,
.core-handle-label,
.core-room-popover__label,
.csd-modal__field label {
    display: block;
    margin-bottom: var(--sp-1);
    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).

   The Theme's fields are this body too (efficiency #558): its own markup
   (the search form, the saved-designs modals) carries `.core-input`, and the
   WooCommerce form rows it cannot class — the checkout, order-pay and account
   pages — are comma-joined here, the way `.btn` joins WooCommerce's `.button`
   below. Their sheets keep only what genuinely differs: the checkout field's
   Stripe-copied padding (journey.css), the select2 box (woocommerce.css) and
   each surface's textarea minimum.

   The text size and side inset are main.css §1's field tokens, which the
   Theme's unclassed `:where(input…)` base reads too (inconsistency #382);
   `--field-fs` steps to 16px on phones there, so iOS never zooms a focused
   field (issue #607). */
.core-input,
.core-select,
.door-field input[type="number"],
.door-field select,
.woocommerce form .form-row input.input-text,
.woocommerce form .form-row textarea,
.woocommerce form .form-row select,
form .mmc .form-row input.input-text,
form .mmc .form-row textarea,
form .mmc .form-row 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 adds
       `.core-input--sm` below. */
    min-height: var(--h-control);
    padding: 8px var(--field-pad-inline);
    border: 1px solid var(--rule-interactive);
    border-radius: var(--r-sm);
    font-size: var(--field-fs);
    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,
.woocommerce form .form-row input.input-text:focus,
.woocommerce form .form-row textarea:focus,
.woocommerce form .form-row select:focus,
form .mmc .form-row input.input-text:focus,
form .mmc .form-row textarea:focus,
form .mmc .form-row select:focus {
    outline: none;
    border-color: var(--ink);
    box-shadow: var(--focus-ring);
}

/* A field that cannot take input steps back to a structural rule, Pebble and
   Ink Faint (Design.md §3.3) — one disabled look for every field, lifted from
   Desk's corner-radius input when it moved onto this body (inconsistency #364).
   Door's derived read-only inputs (width/height locked by a face frame, the
   auto last panel height) wear it too, though they stay `readonly` so the
   value still submits — one "can't edit" look, not four (inconsistency #394). */
.core-input:disabled,
.core-select:disabled,
.door-field input[type="number"]:disabled,
.door-field select:disabled,
.door-field input.door-input--locked,
.door-field input.door-auto-panel-height,
.woocommerce form .form-row input.input-text:disabled,
.woocommerce form .form-row textarea:disabled,
.woocommerce form .form-row select:disabled,
form .mmc .form-row input.input-text:disabled,
form .mmc .form-row textarea:disabled,
form .mmc .form-row select:disabled {
    border-color: var(--rule);
    background: var(--paper-soft);
    color: var(--ink-faint);
    cursor: not-allowed;
}

/* A multi-line field grows from its `rows`; the 44px floor above still holds.
   A surface that wants a taller minimum (the checkout's order notes) states
   only that. */
textarea.core-input,
.woocommerce form .form-row textarea,
form .mmc .form-row textarea {
    padding-block: var(--sp-2);
    font-family: inherit;
    line-height: var(--lh-body);
    resize: vertical;
}

/* The dense rung: §7.1's Small 30px, for a field that shares a row with 30px
   controls — the Sheet and Trim bulk-table rows (Design.md §5.4, "one height
   per row"). Box, focus and disabled stay the base field's; only the height,
   inset and text step down. A field anyone types into on a phone still wants
   16px text, so a layout that reflows these for touch (the bulk table's card
   rows) lifts them back to 44px / 16px itself. */
.core-input--sm {
    height: var(--h-btn-sm);
    min-height: var(--h-btn-sm);
    padding: 0 var(--sp-2);
    font-size: var(--fs-13);
}

/* Checkbox fields */
.core-field-checkbox label,
.door-field-checkbox label {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    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: var(--sp-1);
    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: var(--sp-1);
}

[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 30px
   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: 3px solid var(--accent-tint);
    outline-offset: 0;
    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.
       It overhangs the NEXT setting's label row, so it lets the pointer
       through: a pop-out that caught it would stay open over that row's
       "What's this?" and keep it out of reach. Nothing inside is
       interactive, so nothing is lost. */
    [data-has-info] .core-info {
        position: absolute;
        right: 0;
        z-index: var(--z-raised);
        pointer-events: none;
        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;
    }
}

/* 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.

   • Button backing (a choice that posts nothing, e.g. the Style step's
     Without a style | With a style): each option is a
     `<button role="radio" aria-checked>` holding its <span>, and
     aria-checked stands in for :checked.

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

/* The hairline is drawn by each segment, not the pill (maintainer ruling,
   2026-10-14): the grey Rule Interactive edge goes round the white segments
   only, and the active segment's edge takes the accent so its fill runs to
   the control's outer edge. Neighbours overlap by 1px so a divider stays one
   hairline, and the active segment sits on top so its accent edge covers the
   grey one it shares. The container keeps the radius for the focus ring. */
.core-segmented {
    display: flex;
    width: 100%;
    border-radius: var(--r-sm);
    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;
    border: 1px solid var(--rule-interactive);
    background: var(--paper);
    overflow: hidden;
}

.core-segmented .core-segmented__option + .core-segmented__option {
    margin-left: -1px;
}

/* -of-type, not -child: the binary variant's first child is its checkbox. */
.core-segmented .core-segmented__option:first-of-type {
    border-top-left-radius: var(--r-sm);
    border-bottom-left-radius: var(--r-sm);
}

.core-segmented .core-segmented__option:last-of-type {
    border-top-right-radius: var(--r-sm);
    border-bottom-right-radius: var(--r-sm);
}

/* 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 segment's own 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,
button.core-segmented__option:hover:not([aria-checked="true"]) > span {
    background: var(--paper-sunk);
    color: var(--ink);
}

/* The button backing: no chrome beyond the shared segment hairline — the
   span paints the segment. */
.core-segmented button.core-segmented__option {
    font-family: inherit;
    text-align: inherit;
}

.core-segmented button.core-segmented__option:focus-visible {
    outline: none;
}

/* 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,
button.core-segmented__option[aria-checked="true"] > span {
    background: var(--accent);
    color: var(--accent-fg);
    font-weight: var(--fw-regular);
}

/* …and its edge: accent, not grey, raised over the neighbour it overlaps. */
.core-segmented .core-segmented__option:has(input[type="radio"]:checked),
.core-segmented button.core-segmented__option[aria-checked="true"] {
    border-color: var(--accent);
    background: var(--accent);
    z-index: 1;
}

/* 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),
.core-segmented:has(button.core-segmented__option:focus-visible) {
    box-shadow: var(--focus-ring);
}

/* 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);
}

/* …with the same accent edge, 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 {
    border-color: var(--accent);
    background: var(--accent);
    z-index: 1;
    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 Theme's `.btn` family is this family too (efficiency #558). Its body
   used to be a second full copy in design-base.css; its classes are now
   comma-joined onto the rules below rather than migrated, because the markup
   is not all ours to rewrite — a static block's saved `.btn` markup lives in
   post content, where changing `save.js` would invalidate every saved block —
   and WooCommerce's own `.button` elements (shop loop, account pages, notice
   actions, the order-pay endpoint's #place_order), which no class can be
   added to, came with it. `.btn` is the `.core-btn--inline` form: auto width,
   one line. New markup uses `.core-btn`; `.btn` is the alias, not a second
   family (Design.md §7.1).

   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,
.btn,
.woocommerce :is(a, button, input).button,
.woocommerce #respond input#submit,
.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,
.btn:active,
.woocommerce :is(a, button, input).button:active,
.woocommerce #respond input#submit:active,
.driver-popover-close-btn:active,
.driver-popover-footer .driver-popover-navigation-btns button:active {
    transform: translateY(1px);
}

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

.core-btn-primary,
.btn--primary,
.woocommerce :is(a, button, input).button,
.woocommerce #respond input#submit,
.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);
}

/* The colour is restated for an `<a>` button, which would otherwise take the
   page's link hover colour. */
.core-btn-primary:hover,
.btn--primary:hover,
.woocommerce :is(a, button, input).button:hover,
.woocommerce #respond input#submit: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);
    color: var(--paper);
}

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

.core-btn-secondary,
.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,
.btn--secondary:hover,
.driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn:hover {
    border-color: var(--ink);
}

.core-btn-secondary:disabled,
.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);
}

/* --- 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,
.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 30px action bars, and §5.4 gives a row one height, so they take the row's
   rung rather than this one. The Theme's CTAs are its consumers, all on
   `.core-btn--lg` (inconsistencies #488, #493). `.btn--lg` stays joined for
   one reason: a CTA band saved before #493 keeps the old classes in post
   content until an editor re-saves it (deprecated.js migrates it then). */
.core-btn--lg,
.btn--lg {
    min-height: var(--h-btn-lg);
    padding: 0 var(--sp-6);
    font-size: var(--fs-16);
}

/* --- Joined segment (Design.md §11.6, §7.14) -------------------------
   One section of a joined grey control: flat Pebble at the fixed 30px rung,
   13px type, a Paper hairline between neighbouring sections and only the
   group's outer corners rounded. The box is set outright with a line-height
   of 1 so no label or glyph can change a strip's height. Printed after `--sm`
   so its box wins at equal specificity. Five hosts wear it and keep only
   placement and their own engaged states locally (efficiency #625): the
   preview's view pair, tools strip and ↻ chip, the style pill, the inline
   confirm, and the stepper's Back. A view switch or pressed tool keeps the
   Coal invert (§11.6), never the accent fill — this is not `.core-segmented`. */
.core-btn--seg {
    display: inline-flex;
    width: auto;
    height: var(--h-btn-sm);
    min-height: 0;
    padding: 0 var(--sp-3);
    border: 0;
    border-radius: 0;
    background: var(--paper-soft);
    color: var(--ink);
    font-size: var(--fs-13);
    line-height: 1;
    white-space: nowrap;
}

.core-btn--seg + .core-btn--seg {
    border-left: 1px solid var(--paper);
}

.core-btn--seg:first-child {
    border-top-left-radius: var(--r-sm);
    border-bottom-left-radius: var(--r-sm);
}

.core-btn--seg:last-child {
    border-top-right-radius: var(--r-sm);
    border-bottom-right-radius: var(--r-sm);
}

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

.core-btn--seg:focus-visible {
    outline: 3px solid var(--accent-tint);
    outline-offset: -3px;
}

/* After `:hover`, so a disabled segment keeps its rest fill under the
   pointer. */
.core-btn--seg:disabled {
    background: var(--paper-soft);
    color: var(--ink-muted);
}

/* --- Grey utility (Design.md §7.18) ----------------------------------
   An action bar's secondary utilities — the stepper's Save · Open · Help and
   the planner's Save & share · Undo · Redo: a Stone chip standing on its own,
   all four corners rounded. Size comes from `--sm`; the bar sets the 14px. */
.core-btn--utility {
    border: 0;
    border-radius: var(--r-sm);
    background: var(--paper-sunk);
    color: var(--ink);
}

.core-btn--utility:hover {
    background: color-mix(in oklch, var(--paper-sunk) 85%, var(--ink));
    color: var(--ink);
}

.core-btn--utility:disabled {
    background: var(--paper-sunk);
    opacity: .4;
}

/* 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
   30px-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 30px. 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 30px 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 30px 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,
.btn,
.woocommerce :is(a, button, input).button,
.woocommerce #respond input#submit,
.driver-popover-footer .driver-popover-navigation-btns button {
    display: inline-flex;
    width: auto;
    white-space: nowrap;
}

/* `.btn`'s way back to full width — the base's default, which the `.btn`
   alias opts out of above. */
.btn--full {
    width: 100%;
}

/* §7.1 Text — no chrome, a 1px underline 3px below the baseline, for an
   inline link that acts ("Edit this part", "Change", "Customise"). Standalone
   rather than a colour variant of the base: on its own it is an inline run of
   text at its context's size, and on a `.core-btn` / `.btn` it keeps that
   control's box and 14px label. The colour is the context's, so the same
   link reads on a dark band. A host sets only its own size. The line-height
   is inherited too, so a `<button>` host never falls back to the UA's
   `normal` on a surface without the Theme's `font: inherit` reset. */
.core-btn--text,
.btn--text {
    padding: 0;
    background: none;
    border: none;
    font-family: inherit;
    font-weight: var(--fw-regular);
    line-height: inherit;
    text-transform: none;
    letter-spacing: 0;
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 3px;
    cursor: pointer;
}

/* The back control — "← Products" in the mobile mega menu, "‹ All units" at
   the head of the planner's unit popover, "‹ Back" in its price breakdown
   (efficiency #558; it was one control at three sizes). Ghost at the Small
   rung: no chrome, 13px Ink Soft stepping up to Ink on hover. Standalone, like
   `--text`, because it never sits in the control rail. The 30px box keeps a
   popover header compact; the invisible ::after carries the hit area to the
   44px touch floor (§13) without growing the row. The glyph is the label's. */
.core-btn--back {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    min-height: var(--h-btn-sm);
    margin: 0;
    padding: 0 var(--sp-3) 0 var(--sp-1);
    background: none;
    border: 0;
    border-radius: var(--r-sm);
    font-family: inherit;
    font-size: var(--fs-13);
    font-weight: var(--fw-regular);
    letter-spacing: var(--tracking-normal);
    line-height: 1.2;
    color: var(--ink-soft);
    text-decoration: none;
    cursor: pointer;
    transition: color var(--dur-fast) var(--ease);
}

.core-btn--back::after {
    content: "";
    position: absolute;
    inset: calc((var(--h-btn-sm) - var(--h-control)) / 2) 0;
}

.core-btn--back:hover {
    color: var(--ink);
}

/* The two chromeless controls take the site's text-control ring (the
   `.core-info-trigger` / `.core-switch` one) — written once here rather than
   per host (efficiency #628; the planner's popover and price breakdown had
   three copies). */
.core-btn--back:focus-visible,
.core-btn--text:focus-visible,
.btn--text:focus-visible {
    outline: 3px solid var(--accent-tint);
    outline-offset: 0;
    border-radius: var(--r-xs);
}

/* --- Label (Design.md §4.4) ---------------------------------------------
   The Label role: 11px Semibold, uppercase, tracked open, Ink Muted — a group
   heading, a column head, an eyebrow. One class for both units (efficiency
   #558): the room catalogue's group headings, the finishings popover's, the
   materials index's rate heads and the Theme's eyebrows all carry it. The
   authored string stays sentence case; the uppercase is presentational.

   The Theme rules joined onto the four type properties take the role without
   the class, because WooCommerce prints their markup or their own sheet keeps
   the colour and box (efficiency #499). Every one of those sheets prints after
   this one, so a joined rule's own colour still wins. Deliberately NOT
   joined: rules inside a media query (`.mmj-foot__label`,
   `.mm-mega__bar-title`), near-misses at another size or weight (`.badge`,
   `.eyebrow`, the WooCommerce tabs and sale badge), and the labels set at
   Regular weight. The Theme's block sheets take the same four through
   `@mixin label-role` in _shared.scss. */
.core-label,
form .mmc .form-row label,
.mmo-stats__label,
.woocommerce form .form-row label,
.ft__heading {
    font-size: var(--fs-11);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
}

.core-label {
    font-family: var(--font-sans);
    color: var(--ink-muted);
}

/* --- Disclosure -----------------------------------------------------------
   A `<summary>` that draws its own affordance drops the UA marker — written
   once here rather than per host (efficiency #558; it was five copies).
   `.accordion__trigger` is joined because the accordion block's summary is
   static saved markup that cannot take a new class.

   `.core-chevron` is the 14px stroke chevron the basket's "Product details"
   toggle and the materials index both draw, placed after its label. Each
   host turns it 180° from its own open state (`[open]`, `aria-expanded`,
   `.is-open`), which differs per surface; the body and its motion are here. */
.core-summary,
.accordion__trigger {
    list-style: none;
}

.core-summary::-webkit-details-marker,
.accordion__trigger::-webkit-details-marker {
    display: none;
}

.core-chevron {
    flex: 0 0 auto;
    color: var(--ink);
    transition: transform var(--dur-base) var(--ease);
}

/* --- Chip (Design.md §7.7) ----------------------------------------------
   The 30px small rung, 13px, 4px radius, flat Pebble with no visible border
   (ruled 2026-10-14 — the §7.13 standalone chip's look); hover steps to Stone,
   and selected inverts completely to Coal + Paper. The border stays as a
   transparent 1px so the selected Coal box is the same size as the rest. The one
   chip recipe (efficiency #558): the Theme's category filters (image gallery,
   product card grid) and the products grid's "chip" style wear it; the grid's
   default "tab" style is a tab, not a chip, and stays its own. Selected is
   `.is-active` (the filters' single-select state) or `aria-pressed`. */
.core-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    height: var(--h-btn-sm);
    padding: 0 var(--sp-3);
    background: var(--paper-soft);
    border: 1px solid transparent;
    border-radius: var(--r-sm);
    font-family: inherit;
    font-size: var(--fs-13);
    font-weight: var(--fw-regular);
    letter-spacing: var(--tracking-normal);
    color: var(--ink);
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--dur-fast) var(--ease),
                color var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease);
}

.core-chip:hover {
    background: var(--paper-sunk);
}

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

/* --- 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 30px 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: 3px solid var(--accent-tint);
    outline-offset: 0;
}

/* =========================================================
   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 30px 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 Accent Tint (mouse clicks stay clean). */
.core-switch:focus-visible {
    outline: 3px solid var(--accent-tint);
    outline-offset: 0;
}

/* On — accent track, knob slides right, label goes full-ink. Hovering an on
   switch keeps the raw accent (one accent value, never darkened — Design.md
   §3.5); this rule outranks the off-state hover above by source order. */
.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"]::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 shared
   sub-step bar (`.core-substeps__tab`, below), 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 sits 1px low (bottom: -1px), over the bar's own 1px bottom rule, so
   it replaces that rule rather than stacking on it — every bar that wears
   this class draws that rule. It stays inside the bar's border box, 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: -1px;
    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: 3px solid var(--accent-tint);
    outline-offset: -3px;
}

/* =========================================================
   Sub-steps — one step's worth of tabs over its panes
   Worn by the room planner's sub-step bars (Shape & Size / Features /
   Finishes, Fronts / Carcass) and the configurators' Door Frame / Door
   Panel bar (Core_Control_Renderer::render_substeps()), so the two can't
   drift. Each tab also wears `.core-tab` above for the underline, hover and
   focus ring; only the sub-step sizing and divider colour live here: 1px
   `--rule` between tabs, not `--rule-strong` — a sub-step bar is one step's
   worth of chrome, not the whole card's. The room rail adds its own side
   inset (core-room-rail.css).
   ========================================================= */

.core-substeps {
    display: flex;
    border-bottom: 1px solid var(--rule-strong);
}
.core-substeps__tab {
    flex: 1;
    min-height: var(--h-btn-sm);
    font-size: var(--fs-13);
    color: var(--ink);
    transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.core-substeps__tab + .core-substeps__tab {
    border-left: 1px solid var(--rule);
}
.core-substeps__tab[aria-current="step"] {
    color: var(--accent);
}
.core-substeps-group > .core-substeps {
    margin-bottom: var(--sp-4);
}
@media (width < 640px) {
    .core-substeps {
        flex-direction: column;
    }
    .core-substeps__tab {
        text-align: left;
    }
    .core-substeps__tab + .core-substeps__tab {
        border-left: 0;
        border-top: 1px solid var(--rule);
    }
}

/* =========================================================
   Joined chip group — Design.md §7.13
   A row of links or buttons joined into one control: flat Pebble sections
   split by a 1px Paper line, only the group's outer corners rounded, no
   border or shadow, every section a fixed 30px with 13px type. On inverts to
   Coal with Paper text (§7.7). First consumer: My account's section pages
   (issue #586). The configurator's preview strip (`.core-preview-toggle`,
   core-configurator.css) draws the same look with its own positioning and
   tool states, and has not been moved onto this.
   ========================================================= */

.core-chips {
    display: inline-flex;
    margin: 0;
    padding: 0;
    list-style: none;
    border-radius: var(--r-sm);
    overflow: hidden;
}

.core-chips > li {
    display: flex;
}

.core-chips > li + li .core-chips__item,
.core-chips > .core-chips__item + .core-chips__item {
    border-left: 1px solid var(--paper);
}

.core-chips__item {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    height: var(--h-btn-sm);
    padding: 0 var(--sp-3);
    border: 0;
    background: var(--paper-soft);
    color: var(--ink);
    font-family: inherit;
    font-size: var(--fs-13);
    letter-spacing: var(--tracking-normal);
    line-height: 1;
    white-space: nowrap;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.core-chips__item:hover {
    background: var(--paper-sunk);
    color: var(--ink);
}

.core-chips__item.is-on,
.core-chips__item.is-on:hover {
    background: var(--ink);
    color: var(--paper);
}

.core-chips__item:focus-visible {
    outline: 3px solid var(--accent-tint);
    outline-offset: -3px;
}

/* =========================================================
   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, and so does the Style step's "Start without
   a style" card. `--picture` puts a 4:3 picture on top instead (the Style
   step's tiles, Table's leg and apron tiles, any future picture picker).
   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);
}

/* A card that makes something rather than picks it — the Style step's
   "Create a style" — is a link, never a radio: a dashed border says "new",
   and the link keeps the card's Ink through the Theme's link hover. Box,
   hover and focus are the card's (efficiency #626). */
.core-choice-card--create {
    box-sizing: border-box;
    border-style: dashed;
    text-decoration: none;
}

.core-choice-card--create:hover {
    color: var(--ink);
    text-decoration: none;
}

/* Picture on top (efficiency #606): the picture spans the card at 4:3 with
   the name and detail left-aligned beneath. The chosen border is still 2px,
   so the bottom padding gives back the extra 1px and nothing moves. */
.core-choice-card--picture {
    box-sizing: border-box;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    width: 100%;
    overflow: hidden;
    padding: 0 0 var(--sp-2);
}

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

.core-choice-card__pic {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    aspect-ratio: 4 / 3;
    overflow: hidden;
    background: var(--paper-soft);
    border-bottom: 1px solid var(--rule);
}

.core-choice-card--picture .core-choice-card__text {
    padding: var(--sp-2) var(--sp-2) 0;
}
