/* demo.css — minimal shared styles for V5 demos.
 *
 * V5 emits semantic class names (qx-text, qx-button, qx-container, …); this
 * file gives them enough visual weight to read in a real browser. A real
 * deployment supplies its own stylesheet — these are demo defaults, not the
 * V5 framework's opinion. */

:root {
    color-scheme: light;
    --qx-bg:        #fafaf8;
    --qx-fg:        #1a1a1a;
    --qx-muted:     #777;
    --qx-accent:    #2563eb;
    --qx-accent-fg: #fff;
    --qx-border:    #d9d9d6;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
    background: var(--qx-bg);
    color:      var(--qx-fg);
    margin: 0;
    padding: 2rem;
    line-height: 1.5;
}

h1, h2, h3, h4, h5, h6 { margin: 0; }

.qx-container          { display: flex; flex-direction: column; }
.qx-container--row     { flex-direction: row; align-items: center; }
.qx-container--column  { flex-direction: column; }

.qx-text               { font-size: 1rem; }
.qx-text--xs           { font-size: 0.75rem; }
.qx-text--sm           { font-size: 0.875rem; }
.qx-text--base         { font-size: 1rem; }
.qx-text--lg           { font-size: 1.25rem; }
.qx-text--xl           { font-size: 1.5rem; }
.qx-text--2xl          { font-size: 2rem; }
.qx-text--3xl          { font-size: 2.5rem; }
.qx-text--light        { font-weight: 300; }
.qx-text--normal       { font-weight: 400; }
.qx-text--medium       { font-weight: 500; }
.qx-text--semibold     { font-weight: 600; }
.qx-text--bold         { font-weight: 700; }
.qx-align-start        { text-align: left; }
.qx-align-center       { text-align: center; }
.qx-align-end          { text-align: right; }

.qx-button {
    appearance: none;
    border: 1px solid var(--qx-border);
    background: white;
    color: var(--qx-fg);
    padding: 0.5rem 1rem;
    font: inherit;
    border-radius: 6px;
    cursor: pointer;
    transition: background 0.1s ease;
}
.qx-button:hover                 { background: #f0f0ee; }
.qx-button:active                { transform: translateY(1px); }
.qx-button--primary              { background: var(--qx-accent); color: var(--qx-accent-fg); border-color: var(--qx-accent); }
.qx-button--primary:hover        { background: #1d4ed8; }
.qx-button--success              { background: #16a34a; color: white; border-color: #16a34a; }
.qx-button--success:hover        { background: #15803d; }
.qx-button--outline-secondary    { background: white; color: var(--qx-fg); border-color: var(--qx-border); }
.qx-button--outline-secondary:hover { background: #f4f4f2; }
.qx-button--sm                   { padding: 0.25rem 0.5rem; font-size: 0.875rem; }
.qx-button--lg                   { padding: 0.75rem 1.5rem; font-size: 1.125rem; }

/* demo chrome */
.demo-shell        { max-width: 720px; margin: 0 auto; }
.demo-header       { border-bottom: 1px solid var(--qx-border); padding-bottom: 1rem; margin-bottom: 2rem; }
.demo-header h1    { font-size: 1.5rem; font-weight: 600; }
.demo-header p     { color: var(--qx-muted); margin: 0.25rem 0 0; }
.demo-back         { color: var(--qx-accent); text-decoration: none; font-size: 0.875rem; }
.demo-back:hover   { text-decoration: underline; }

/* CSS-only tooltip rendering. V5's `ui.tooltip` capability sets
 * `data-bs-toggle="tooltip"` + `data-bs-title` + `data-bs-placement` on
 * tooltip-bearing elements (so a real Bootstrap.Tooltip JS can adopt
 * them). These rules render a styled popover from the same attributes
 * with zero JS — dark background, four placement variants, hover-only,
 * positioned via the nearest positioned ancestor (which we establish on
 * the tooltip-bearing element itself). */
[data-bs-toggle="tooltip"] { position: relative; }
[data-bs-toggle="tooltip"]::after {
    content: attr(data-bs-title);
    position: absolute;
    background: #1f2937;
    color: #fff;
    padding: 0.35rem 0.55rem;
    border-radius: 4px;
    font-size: 0.75rem;
    font-weight: 400;
    line-height: 1.3;
    white-space: nowrap;
    max-width: 18rem;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.12s ease 0.05s;
    z-index: 9999;
    /* Default — placement="top" (and the unscoped fallback) */
    bottom: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%);
}
[data-bs-toggle="tooltip"]:hover::after,
[data-bs-toggle="tooltip"]:focus::after        { opacity: 1; }

[data-bs-toggle="tooltip"][data-bs-placement="bottom"]::after {
    bottom: auto; top: calc(100% + 6px);
}
[data-bs-toggle="tooltip"][data-bs-placement="right"]::after {
    left: calc(100% + 6px); right: auto;
    bottom: auto; top: 50%;
    transform: translateY(-50%);
}
[data-bs-toggle="tooltip"][data-bs-placement="left"]::after {
    left: auto; right: calc(100% + 6px);
    bottom: auto; top: 50%;
    transform: translateY(-50%);
}

/* ── capability blocks: picker-row, text-input, validation ───────────────
 * Centralised here so every demo (especially the pure-V5 set) inherits
 * consistent styling without per-demo inline <style> blocks. Ported from
 * the older v4-adapter demos (03-picker-row, 04-form, 07-validation-port).
 */

/* picker-row — props.input.basic */
.qx-picker-row {
    display: inline-flex;
    gap: 0;
    border: 1px solid var(--qx-border);
    border-radius: 6px;
    overflow: hidden;
}
.qx-picker-row__option {
    appearance: none;
    border: none;
    border-right: 1px solid var(--qx-border);
    background: white;
    color: var(--qx-fg);
    padding: 0.5rem 1rem;
    font: inherit;
    cursor: pointer;
    transition: background 0.1s ease;
}
.qx-picker-row__option:last-child   { border-right: none; }
.qx-picker-row__option:hover        { background: #f0f0ee; }
.qx-picker-row__option--selected    { background: var(--qx-accent); color: var(--qx-accent-fg); }
.qx-picker-row__option--selected:hover { background: #1d4ed8; }

/* text-input — props.input.basic */
.qx-text-input {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}
.qx-text-input__label {
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--qx-fg);
}
.qx-text-input__field {
    appearance: none;
    border: 1px solid var(--qx-border);
    background: white;
    color: var(--qx-fg);
    padding: 0.5rem 0.625rem;
    font: inherit;
    border-radius: 6px;
    transition: border-color 0.1s ease, box-shadow 0.1s ease;
    width: 100%;
    box-sizing: border-box;
}
.qx-text-input__field:focus {
    outline: none;
    border-color: var(--qx-accent);
    box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}

/* validation feedback — forms.validation
 *
 * The propHandler sets data-qx-invalid="true" on the field's wrapper
 * after first blur if rules fail. Surface that with a red ring + error
 * text below. Pristine fields (data-qx-invalid absent) render clean.
 */
[data-qx-invalid="true"] .qx-text-input__field {
    border-color: #c0392b;
    box-shadow: 0 0 0 3px rgba(192, 57, 43, 0.12);
}
[data-qx-validation-error] {
    color: #c0392b;
    font-size: 0.8125rem;
    margin-top: 0.2rem;
}

/* datepicker / colorpicker — props.input.basic
 *
 * Same field-shape as text-input; pick up the .qx-text-input__field
 * rules by adding the class. (The blocks emit their own root classes
 * for variant styling; field-level rules live above.)
 */
.qx-datepicker__field, .qx-colorpicker__field {
    appearance: none;
    border: 1px solid var(--qx-border);
    background: white;
    color: var(--qx-fg);
    padding: 0.5rem 0.625rem;
    font: inherit;
    border-radius: 6px;
}

/* form block — props.input.form
 *
 * The dedicated `form` block (not used by pure-V5 demos yet — they
 * compose form-shaped layouts from container + text-input + button).
 * Included for parity demos that DO use it.
 */
.qx-form                       { display: flex; flex-direction: column; gap: 0.875rem; }
.qx-form__row                  { display: flex; flex-direction: column; gap: 0.25rem; }
.qx-form__row--boolean         { flex-direction: row; align-items: center; gap: 0.5rem; }
.qx-form__label                { font-size: 0.875rem; font-weight: 500; color: var(--qx-fg); }
.qx-form__required             { color: #c0392b; }
.qx-form__field {
    appearance: none;
    border: 1px solid var(--qx-border);
    background: white;
    color: var(--qx-fg);
    padding: 0.5rem 0.625rem;
    font: inherit;
    border-radius: 6px;
    transition: border-color 0.1s ease, box-shadow 0.1s ease;
}
.qx-form__field:focus          { outline: none; border-color: var(--qx-accent); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); }
textarea.qx-form__field        { resize: vertical; min-height: 4em; font-family: inherit; }
.qx-form__check                { width: 1rem; height: 1rem; accent-color: var(--qx-accent); }
.qx-form__hint                 { font-size: 0.75rem; color: var(--qx-muted); }

/* Validation feedback. The form's submit-time error summary (.qx-form__errors,
 * role=alert) and per-field live errors (.qx-form__field-error, shown on blur)
 * both render through the one validation engine. Invalid fields get a red ring
 * via [data-qx-invalid="true"]. Hidden until there's something to say. */
.qx-form__errors {
    margin: 0;
    padding: 0.6rem 0.75rem;
    border: 1px solid #f0c2c2;
    border-radius: 6px;
    background: #fdf0f0;
    color: #9a2c2c;
    font-size: 0.85rem;
}
.qx-form__errors[hidden]                 { display: none; }
.qx-form__field-error {
    font-size: 0.75rem;
    color: #c0392b;
    min-height: 0;
}
.qx-form__field-error:empty              { display: none; }
.qx-form__field[data-qx-invalid="true"],
.qx-form__row[data-qx-invalid="true"] .qx-form__field {
    border-color: #c0392b;
}
.qx-form__field[data-qx-invalid="true"]:focus {
    box-shadow: 0 0 0 3px rgba(192, 57, 43, 0.15);
}

/* effect.basic — Phase E7
 *
 * `effect.speechBubble` — a viewport-positioned overlay that anchors
 * itself to a target block via [data-qx-id]. The block sets its own
 * top/left via JS; the CSS only handles the visual + the tail
 * triangle's per-direction placement.
 */
.qx-speech-bubble {
    background:   var(--qx-fg);
    color:        var(--qx-bg);
    padding:      0.5rem 0.75rem;
    border-radius: 8px;
    font-size:    0.875rem;
    line-height:  1.4;
    max-width:    220px;
    box-shadow:   0 4px 16px rgba(0, 0, 0, 0.18);
    /* The bubble itself is non-interactive — overrides the inline
     * pointer-events: none from the inline style if anyone changes it. */
    pointer-events: none;
}
.qx-speech-bubble__body         { white-space: pre-wrap; word-break: break-word; }
.qx-speech-bubble__tail {
    position: absolute;
    width: 0; height: 0;
    border: 6px solid transparent;
}
.qx-speech-bubble--above .qx-speech-bubble__tail {
    top: 100%; left: 50%; transform: translateX(-50%);
    border-top-color: var(--qx-fg);
}
.qx-speech-bubble--below .qx-speech-bubble__tail {
    bottom: 100%; left: 50%; transform: translateX(-50%);
    border-bottom-color: var(--qx-fg);
}
.qx-speech-bubble--left .qx-speech-bubble__tail {
    top: 50%; left: 100%; transform: translateY(-50%);
    border-left-color: var(--qx-fg);
}
.qx-speech-bubble--right .qx-speech-bubble__tail {
    top: 50%; right: 100%; transform: translateY(-50%);
    border-right-color: var(--qx-fg);
}

/* ── table (capability data.table) ──────────────────────────────────────
 * A semantic <table> rendered by the `table` block. System-agnostic base
 * styling; sites layer their own look via the author `class` (e.g. seaside
 * adds `.tariff-table`). On narrow screens each row collapses into a stacked
 * "card": the <thead> is hidden and each <td> shows its column label from the
 * data-label attribute the block emits. This keeps the real <table> element
 * (and its accessibility) intact while reflowing the layout for phones. */
.qx-table {
    width: 100%;
    border-collapse: collapse;
    text-align: left;
}
.qx-table caption {
    caption-side: top;
    text-align: left;
    font-weight: 600;
    color: var(--qx-fg);
    padding: 0 0 0.5rem;
}
.qx-table th,
.qx-table td {
    padding: 0.6rem 0.75rem;
    border-bottom: 1px solid var(--qx-border, #e5e7eb);
}
.qx-table thead th {
    font-weight: 600;
    color: var(--qx-fg);
    border-bottom-width: 2px;
    white-space: nowrap;
}
.qx-table tbody tr:hover { background: color-mix(in srgb, var(--qx-accent, #2563eb) 6%, transparent); }
.qx-table .qx-table-empty td {
    color: var(--qx-muted, #6b7280);
    font-style: italic;
    text-align: center;
}

/* Responsive: stack rows as cards under 640px. The <table> stays a table in
 * the DOM (a11y/SEO preserved); only the visual box model changes. */
@media (max-width: 640px) {
    .qx-table thead {
        position: absolute;
        width: 1px; height: 1px;
        overflow: hidden; clip: rect(0 0 0 0);
        white-space: nowrap;
    }
    .qx-table tbody tr {
        display: block;
        margin-bottom: 0.75rem;
        border: 1px solid var(--qx-border, #e5e7eb);
        border-radius: 8px;
        padding: 0.25rem 0.5rem;
    }
    .qx-table td {
        display: flex;
        justify-content: space-between;
        gap: 1rem;
        border-bottom: 1px solid color-mix(in srgb, var(--qx-border, #e5e7eb) 60%, transparent);
        text-align: right;
    }
    .qx-table tbody tr td:last-child { border-bottom: 0; }
    .qx-table td::before {
        content: attr(data-label);
        font-weight: 600;
        color: var(--qx-muted, #6b7280);
        text-align: left;
    }
}

/* ── table: pills, group banners, link cells (capability data.table) ────── */
.qx-pill {
    display: inline-block;
    padding: 0.15em 0.6em;
    border-radius: 999px;
    font-size: 0.85em;
    font-weight: 600;
    line-height: 1.4;
    white-space: nowrap;
    background: color-mix(in srgb, var(--qx-fg, #334155) 10%, transparent);
    color: var(--qx-fg, #334155);
}
/* Status tones (statusType slug). Tasteful defaults; sites can override. */
.qx-pill--full-week   { background: #dcfce7; color: #166534; }   /* green  */
.qx-pill--short-break { background: #fef3c7; color: #92400e; }   /* amber  */
.qx-pill--sold-out    { background: #f1f5f9; color: #64748b; }   /* grey   */

/* Group banner row — one per group (e.g. a season), spans all columns. */
.qx-table-group th {
    background: color-mix(in srgb, var(--qx-accent, #2563eb) 8%, transparent);
    color: var(--qx-fg, #0f172a);
    font-weight: 700;
    text-align: left;
    padding: 0.55rem 0.75rem;
    border-bottom: 2px solid var(--qx-border, #e5e7eb);
}

/* Link cell — an anchor inside a <td>. */
.qx-table-link { color: var(--qx-accent, #2563eb); font-weight: 600; text-decoration: none; }
.qx-table-link:hover { text-decoration: underline; }

/* Mobile stacked-card: group banners stay full-width separators. */
@media (max-width: 640px) {
    .qx-table-group th { display: block; }
}

/* ── Ambient atmosphere (ambient-effect) ─────────────────────────────────
   The presets in defs/ambient/preset/*.json name these keyframes; before
   this block they were named by data with nothing to answer to, so rain,
   snow and bubbles were unreachable from config (NOTEBOARD C20).
   Each particle carries its own duration/delay inline; the shared part —
   the paths themselves — lives here, once, rather than being injected by
   script at runtime as V4 did (a stylesheet cannot be injected server-side,
   and the markup ships pre-rendered).
   A travelling particle is a FULL-HEIGHT COLUMN with its mark at the top, so
   the translateY percentages below resolve against the container rather than
   against a 14px glyph — which is what made rain fall in the top fifth of a
   panel, and what made the px-based presets right for exactly one panel size.
   --qx-ambient-opacity is set per particle so one preset can hold its peak
   opacity while another fades. */
.qx-ambient { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
.qx-ambient__p    { will-change: transform, opacity; }
.qx-ambient__mark { pointer-events: none; }

@keyframes qx-ambient-rise {
    0%   { transform: translateY(105%) translateX(0);   opacity: 0; }
    18%  { opacity: var(--qx-ambient-opacity, 0.7); }
    100% { transform: translateY(-15%) translateX(28px); opacity: 0; }
}
@keyframes qx-ambient-fall {
    0%   { transform: translateY(-15%) translateX(0);     opacity: 0; }
    14%  { opacity: var(--qx-ambient-opacity, 1); }
    100% { transform: translateY(105%) translateX(-32px); opacity: 0; }
}
@keyframes qx-ambient-fall-spin {
    0%   { transform: translateY(-15%) rotate(0deg);   opacity: 0; }
    14%  { opacity: var(--qx-ambient-opacity, 1); }
    100% { transform: translateY(105%) rotate(540deg); opacity: 0; }
}
@keyframes qx-ambient-drift {
    0%   { transform: translate(0, 0); opacity: 0; }
    45%  { opacity: var(--qx-ambient-opacity, 1); }
    100% { transform: translate(140px, -70px); opacity: 0; }
}
@keyframes qx-ambient-twinkle {
    0%, 100% { transform: scale(0.55); opacity: 0.25; }
    50%      { transform: scale(1.05); opacity: var(--qx-ambient-opacity, 1); }
}
@keyframes qx-ambient-rain {
    0%   { transform: translateY(-15%); opacity: 0; }
    10%  { opacity: var(--qx-ambient-opacity, 1); }
    100% { transform: translateY(105%); opacity: var(--qx-ambient-opacity, 1); }
}

/* Weather that respects the setting. A full-screen scatter of moving
   particles is exactly what this preference is for. */
@media (prefers-reduced-motion: reduce) {
    .qx-ambient__p { animation: none !important; opacity: var(--qx-ambient-opacity, 0.6) !important; }
    /* A travelling particle parks at its start edge with no animation, which
       would stack every mark on one line; nudge them back into the frame. */
    .qx-ambient__p > .qx-ambient__mark { top: 8%; }
}

/* ── data-select — the drop-down / autocomplete picker (props.input.basic, 2026-10-19) ──
   One control, three modes. The list is positioned under the control and scrolls, so
   it never widens the page on a phone. Sites theme it through the qx-select tree. */
.qx-select { position: relative; display: block; width: 100%; max-width: 100%; font: inherit; }
.qx-select__label { display: block; font-size: .8rem; color: #555; margin: 0 0 .25rem; }
.qx-select__control { position: relative; display: flex; align-items: stretch; min-width: 0; }
.qx-select__input {
    flex: 1 1 auto; min-width: 0; width: 100%; box-sizing: border-box;
    font: inherit; line-height: 1.3; padding: .5rem 4rem .5rem .7rem;
    border: 1px solid var(--qx-border, #cfcfcb); border-radius: 6px; background: #fff; color: var(--qx-fg, #111);
}
.qx-select--dropdown .qx-select__input { cursor: pointer; }
.qx-select__input:focus { outline: 2px solid var(--qx-accent, #2563eb); outline-offset: 1px; }
.qx-select__clear, .qx-select__toggle {
    position: absolute; top: 0; bottom: 0; width: 2rem; margin: 0; padding: 0; border: 0; background: transparent;
    color: #666; font: inherit; cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
}
.qx-select__toggle { right: 0; }
.qx-select__clear  { right: 2rem; font-size: 1.1em; }
.qx-select__clear[hidden] { display: none; }
.qx-select__list {
    position: absolute; left: 0; right: 0; top: 100%; z-index: 30; margin: .25rem 0 0; padding: .25rem 0;
    max-height: min(40vh, 22rem); overflow-y: auto; overflow-x: hidden;
    background: #fff; border: 1px solid var(--qx-border, #cfcfcb); border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,.12);
}
.qx-select__list[hidden] { display: none; }
.qx-select__option { display: block; padding: .45rem .7rem; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.qx-select__option--active { background: #eef2ff; }
.qx-select__option--selected { font-weight: 600; }
.qx-select__option--selected::after { content: " ✓"; color: var(--qx-accent, #2563eb); font-weight: 400; }
.qx-select__option mark { background: transparent; color: inherit; font-weight: 700; text-decoration: underline; text-decoration-color: var(--qx-accent, #2563eb); }
.qx-select__toggle::after { content: ""; width: 0; height: 0; border: 5px solid transparent; border-top: 6px solid #666; margin-top: 5px; }
.qx-select__indent { display: inline-block; vertical-align: middle; width: calc(var(--qx-depth, 1) * .9em); height: .9em; position: relative; margin-right: .35em; }
.qx-select__indent::before { content: ""; position: absolute; right: 0; top: 0; width: .55em; height: .55em; border-left: 1px solid #aaa; border-bottom: 1px solid #aaa; border-bottom-left-radius: 2px; }
.qx-select__desc { display: block; font-size: .8em; color: #777; white-space: normal; }
.qx-select__group { padding: .5rem .7rem .2rem; font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: #888; }
.qx-select__empty, .qx-select__more { padding: .45rem .7rem; color: #888; font-size: .9em; }
.qx-select--sm .qx-select__input { padding: .3rem 3.6rem .3rem .55rem; font-size: .9em; }
.qx-select--lg .qx-select__input { padding: .7rem 4.2rem .7rem .85rem; font-size: 1.08em; }
.qx-select--disabled .qx-select__input { background: #f3f3f1; color: #888; cursor: not-allowed; }

/* ── toggle — an on/off switch or tick box bound to a state key (props.input.basic, 2026-10-19) ──
   The whole row is the tap target (min 44px). Sites theme it through the custom properties
   below or the qx-toggle tree. */
.qx-toggle {
    --qx-toggle-on: #2f6fdf;
    --qx-toggle-off: #c9ccd3;
    --qx-toggle-thumb: #fff;
    --qx-toggle-focus: rgba(47, 111, 223, .45);
    display: inline-flex; flex-direction: column; gap: .15rem; max-width: 100%; font: inherit;
}
.qx-toggle__control {
    display: inline-flex; align-items: center; gap: .6rem; min-height: 44px; padding: .25rem .1rem;
    background: none; border: 0; margin: 0; color: inherit; font: inherit; text-align: start;
    cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.qx-toggle__control:focus-visible { outline: 3px solid var(--qx-toggle-focus); outline-offset: 2px; border-radius: 8px; }
.qx-toggle__track {
    position: relative; flex: none; width: 2.6em; height: 1.5em; border-radius: 999px;
    background: var(--qx-toggle-off); transition: background-color .18s ease;
}
.qx-toggle__thumb {
    position: absolute; top: .15em; left: .15em; width: 1.2em; height: 1.2em; border-radius: 50%;
    background: var(--qx-toggle-thumb); box-shadow: 0 1px 2px rgba(0,0,0,.25); transition: transform .18s ease;
}
.qx-toggle--on .qx-toggle__track { background: var(--qx-toggle-on); }
.qx-toggle--on .qx-toggle__thumb { transform: translateX(1.1em); }
.qx-toggle__label { line-height: 1.3; }
.qx-toggle__state { font-size: .85em; opacity: .75; min-width: 2em; }
.qx-toggle__desc { font-size: .8rem; color: #666; padding-inline-start: 3.2em; }
.qx-toggle--label-start .qx-toggle__desc { padding-inline-start: 0; }
.qx-toggle--sm { font-size: .85em; }
.qx-toggle--lg { font-size: 1.15em; }
.qx-toggle--disabled .qx-toggle__control { cursor: not-allowed; opacity: .55; }
/* the checkbox variant: the native input carries focus and state; the box is drawn */
.qx-toggle__input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }
.qx-toggle__box {
    flex: none; width: 1.25em; height: 1.25em; border-radius: 4px; border: 2px solid var(--qx-toggle-off);
    background: var(--qx-toggle-thumb); position: relative; transition: background-color .15s ease, border-color .15s ease;
}
.qx-toggle--checkbox.qx-toggle--on .qx-toggle__box { background: var(--qx-toggle-on); border-color: var(--qx-toggle-on); }
.qx-toggle--checkbox.qx-toggle--on .qx-toggle__box::after {
    content: ""; position: absolute; left: .32em; top: .08em; width: .3em; height: .6em;
    border: solid var(--qx-toggle-thumb); border-width: 0 .14em .14em 0; transform: rotate(45deg);
}
.qx-toggle__input:focus-visible + .qx-toggle__box { outline: 3px solid var(--qx-toggle-focus); outline-offset: 2px; }
.qx-toggle--checkbox .qx-toggle__control { position: relative; }
@media (prefers-reduced-motion: reduce) {
    .qx-toggle__track, .qx-toggle__thumb, .qx-toggle__box { transition: none; }
}

/* ── hover card — a floating detail box on any block (ui.hover-card `hoverCard:`, 2026-10-20) ──
   ONE shared layer at the end of <body>; each open card is a fixed-position child of it, placed
   by the trigger's rect. Neutral defaults only — a site styles its cards through the author's
   `class` (`.qx-hover-card.jx-who-card { … }`) or the custom properties below. */
.qx-hover-card-layer { position: fixed; top: 0; left: 0; width: 0; height: 0; overflow: visible; z-index: 1050; pointer-events: none; }
.qx-hover-card {
    --qx-hover-card-bg: #fff;
    --qx-hover-card-fg: #1a1a1a;
    --qx-hover-card-border: rgba(0, 0, 0, .14);
    --qx-hover-card-shadow: 0 6px 20px rgba(15, 23, 42, .14);
    --qx-hover-card-radius: 8px;
    position: fixed; box-sizing: border-box; pointer-events: auto;
    max-width: min(320px, calc(100vw - 16px)); padding: .6rem .8rem;
    background: var(--qx-hover-card-bg); color: var(--qx-hover-card-fg);
    border: 1px solid var(--qx-hover-card-border); border-radius: var(--qx-hover-card-radius);
    box-shadow: var(--qx-hover-card-shadow); font-size: .92rem; line-height: 1.4; text-align: start;
    opacity: 0; transition: opacity 120ms ease;
}
.qx-hover-card--open { opacity: 1; }
.qx-hover-card--closing { pointer-events: none; }
.qx-hover-card__body { display: flex; flex-direction: column; gap: .25rem; }
.qx-hover-card__line { white-space: pre-wrap; }
.qx-hover-card-trigger { cursor: default; }
/* the arrow is a rotated square sharing the card's surface and border; the handler sets
   --qx-hc-arrow-x / --qx-hc-arrow-y so it points at the trigger's centre */
.qx-hover-card__arrow {
    position: absolute; width: 10px; height: 10px; background: var(--qx-hover-card-bg);
    border: 1px solid var(--qx-hover-card-border); transform: rotate(45deg); pointer-events: none;
}
.qx-hover-card[data-placement="bottom"] .qx-hover-card__arrow { top: -6px; left: calc(var(--qx-hc-arrow-x, 50%) - 5px); border-right: 0; border-bottom: 0; }
.qx-hover-card[data-placement="top"]    .qx-hover-card__arrow { bottom: -6px; left: calc(var(--qx-hc-arrow-x, 50%) - 5px); border-left: 0; border-top: 0; }
.qx-hover-card[data-placement="right"]  .qx-hover-card__arrow { left: -6px; top: calc(var(--qx-hc-arrow-y, 50%) - 5px); border-top: 0; border-right: 0; }
.qx-hover-card[data-placement="left"]   .qx-hover-card__arrow { right: -6px; top: calc(var(--qx-hc-arrow-y, 50%) - 5px); border-bottom: 0; border-left: 0; }
@media (prefers-reduced-motion: reduce) {
    .qx-hover-card { transition: none; }
}

/* ── swatch-select ──────────────────────────────────────────────────────────
   A palette as a radiogroup. The engine ships the LOOK of the control (shape,
   ring, tick, wrapping, thumb size) and never the colours: each swatch's own
   colour arrives as --swatch from the option's data. Selection is a ring AND a
   tick, never the colour alone — that is what makes it usable without colour
   vision, and is why these rules exist rather than a page writing its own. */
.qx-swatch-select__label { display: block; font-size: .85rem; font-weight: 600; margin-bottom: .35rem; }
.qx-swatches { display: flex; flex-wrap: wrap; gap: .5rem; }
.qx-swatches--grid { display: grid; grid-template-columns: repeat(var(--qx-swatch-columns, 4), minmax(0, 1fr)); }
.qx-swatch { position: relative; display: inline-flex; align-items: center; justify-content: center;
             width: var(--qx-swatch-size, 44px); height: var(--qx-swatch-size, 44px);
             border-radius: 10px; cursor: pointer; box-sizing: border-box;
             background: var(--swatch, #e5e7eb); border: 1px solid rgba(0, 0, 0, .18); }
.qx-swatches--sm .qx-swatch { --qx-swatch-size: 32px; border-radius: 8px; }
.qx-swatches--lg .qx-swatch { --qx-swatch-size: 56px; border-radius: 12px; }
/* The name is for assistive tech and for the title tooltip; the swatch itself is the
   visual. Clipped rather than hidden, so it is read but does not size the box. */
.qx-swatch__name { position: absolute; width: 1px; height: 1px; overflow: hidden;
                   clip-path: inset(50%); white-space: nowrap; }
/* SELECTED: a ring plus a tick. Two independent signals, neither of them hue. */
.qx-swatch--selected { box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--qx-accent, #2563eb); }
.qx-swatch--selected .qx-swatch__tick { width: .7em; height: .4em; border-left: 2.5px solid #fff;
    border-bottom: 2.5px solid #fff; transform: rotate(-45deg) translateY(-15%);
    filter: drop-shadow(0 0 1px rgba(0, 0, 0, .55)); }
.qx-swatch:focus-visible { outline: 3px solid var(--qx-accent, #2563eb); outline-offset: 3px; }
/* NO COLOUR OF ITS OWN — a diagonal line, so it reads as a deliberate absence rather
   than as a swatch that failed to load. */
.qx-swatch--none, .qx-swatch--unset { background: #fff; }
.qx-swatch--none::after, .qx-swatch--unset::after { content: ""; position: absolute; inset: 0;
    background: linear-gradient(to top right, transparent calc(50% - 1px), rgba(0,0,0,.45) 50%, transparent calc(50% + 1px)); }
/* A leading option that DOES carry an inherited colour shows it, with a dashed edge to
   say "this is not mine" — inheritance looks different from ownership. */
.qx-swatch--none:not(.qx-swatch--unset) { background: var(--swatch, #fff); border-style: dashed; border-width: 2px; }
.qx-swatch--none:not(.qx-swatch--unset)::after { content: none; }
.qx-swatches--disabled .qx-swatch { cursor: not-allowed; opacity: .55; }

/* data-select: a per-option cue from the option's own data (optionStyleField). The engine
   draws it as a dot because a dot works in a dense list; the value is a custom property,
   so a page that wants a bar or a border overrides these two rules and nothing else. */
.qx-select__option--cued::before, .qx-select--cued .qx-select__control::before {
    content: ""; display: inline-block; width: .7em; height: .7em; border-radius: 50%;
    margin-right: .45em; vertical-align: middle;
    background: var(--qx-option-cue, transparent); border: 1px solid rgba(0, 0, 0, .18); }

/* ── speech bubble: the THOUGHT kind, and an ATTACHED bubble (G10, 2026-10-22) ──────────
   v4 had `kind: speech | thought` and the v5 port dropped it. A thought bubble is not a
   speech bubble with different words: its tail is a trail of puffs, so it is drawn rather
   than pointed. The tail element is the ::after both kinds share. */
.qx-speech-bubble--thought                       { border-radius: 1.2rem; }
.qx-speech-bubble--thought::after,
.qx-speech-bubble--thought::before               { content: ""; position: absolute; border-radius: 50%;
                                                   background: inherit; border: inherit; }
.qx-speech-bubble--thought.qx-speech-bubble--tail-down::after,
.qx-speech-bubble--thought.qx-speech-bubble--tail-down::before { border-top: none; }
.qx-speech-bubble--thought.qx-speech-bubble--tail-down::after  { width: 0.75rem; height: 0.75rem;
                                                   left: 1.5rem; bottom: -0.5rem; }
.qx-speech-bubble--thought.qx-speech-bubble--tail-down::before { width: 0.45rem; height: 0.45rem;
                                                   left: 0.9rem; bottom: -1.1rem; }
.qx-speech-bubble--thought.qx-speech-bubble--tail-up::after     { width: 0.75rem; height: 0.75rem;
                                                   left: 1.5rem; top: -0.5rem; }
.qx-speech-bubble--thought.qx-speech-bubble--tail-up::before    { width: 0.45rem; height: 0.45rem;
                                                   left: 0.9rem; top: -1.1rem; }
/* An attached bubble is positioned by core/anchor-position.js (position: fixed, set inline),
   so it must not also be laid out by its parent. `max-width` keeps a long line from spanning
   the viewport; the arrow offset comes from --qx-bubble-arrow-x/-y. */
.qx-speech-bubble--attached                      { max-width: min(22rem, calc(100vw - 1rem)); }
.qx-speech-bubble--attached[data-placement="top"],
.qx-speech-bubble--attached[data-placement="bottom"] { --qx-bubble-arrow-x: 50%; }
