/* ============================================================================
   COMPONENTS — the reusable pieces every page is built from.
   ========================================================================= */

/* ============================================================== STRUCTURE
   NOT EVERY GROUP IS A CARD. This is the central layout decision of the
   system, and it is what makes a long page readable.

   The old sheet had exactly one grouping device — .panel, a white box with a
   border — so a page was a stack of identical boxes and nothing ranked against
   anything. Product detail was six white boxes describing ONE product.

   Two devices now:
     .panel   — a bounded thing you ACT ON or that holds a DATASET: a form
                section, a table, the go-live card. Earns a surface, a border
                and a radius, because it has an edge in the user's model.
     .region  — a titled group that is only STRUCTURE: Overview, Description,
                Details. A heading and a hairline. No box, because there is no
                bounded thing there — just a part of the page.

   When adding a group, ask which it is. If the answer is "it's just the next
   part of this page", it is a .region. */

.panel {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    /* No shadow at rest — see the elevation opinion in tokens.css. */
}

.panel-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: var(--space-5) var(--space-5);
}

.region {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding-top: var(--space-5);
    border-top: 1px solid var(--border);
}

/* A heading row that carries a trailing action or count. */
.head-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
}

.muted {
    color: var(--text-muted);
    font-size: var(--text-xs);
}

.page-links {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
}

/* The way back out of a detail page. Above the h1 rather than beside it, because
   it is not an action on this record — it leaves. Muted, small, and NOT a
   button: a detail page should have exactly one visually loud control and this
   is never it. */
.page-back {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    font-size: var(--text-xs);
    color: var(--text-muted);
    text-decoration: none;
}

.page-back:hover {
    color: var(--accent-text);
    text-decoration: underline;
}

/* ================================================================ THE VOICE
   ── THE SIGNATURE ELEMENT ──────────────────────────────────────────────────

   Sailesman speaks in the first person throughout this product: "I'm live on
   your storefront", "I woke from a bad dream", "I'm still exploring the shop".
   That voice is the most distinctive thing about it — and before #519 those
   lines were rendered as <p class="notice notice-muted">, the exact same
   object as "Select at least one product first". The product's character and
   its validation errors looked identical.

   .says separates the two channels:
     .says    — the ASSISTANT speaking about its own state. Carries the mark.
     .notice  — the SYSTEM reporting on an operation. Bordered, tinted, boxed.

   It is a re-classification of existing copy, never a rewrite: the words, the
   role="status"/role="alert" attributes and the data-* hooks are untouched.

   THE MARK, and why lime.
   The brand ships a CTA lime (#dbf227) that #282 deliberately parked because
   the portal hardcodes white button labels and white-on-lime is ~1.1:1. Lime
   measures 1.25:1 against white, so as a FOREGROUND in light mode it is
   effectively invisible and can never carry meaning. As a FILL with the brand
   navy on it, it measures 11.99:1.

   So the mark is a lime chip carrying the Sailesman crystal in navy. Lime is
   asked to do the only job it can do — draw the eye — and the mark and the
   text carry the meaning in fully legible tokens.

   The chip supplies its own background, so it is IDENTICAL in light and dark.
   It is the one element in the portal that does not shift between themes:
   the constant that means "this is Sailesman talking".

   RESERVED: --voice / --voice-ink appear here and nowhere else. If lime shows
   up on a button or a badge, the signature is gone. */
/* Indent-plus-absolute-mark, NOT a grid. A grid container turns every child of
   a <p> into its own grid item, including inline elements — so a .says line
   containing a link ("...issue a fresh key on your <a>API keys</a> page")
   shattered into one column per text run. Padding keeps normal inline flow. */
.says {
    position: relative;
    padding-left: calc(20px + var(--space-3));
    max-width: var(--measure);
    font-size: var(--text-sm);
    line-height: var(--leading-relaxed);
    color: var(--text);
}

.says::before {
    content: "";
    position: absolute;
    left: 0;
    /* Optical alignment with the first line of --text-sm/--leading-relaxed. */
    top: 1px;
    width: 20px;
    height: 20px;
    border-radius: var(--radius-sm);
    background-color: var(--voice);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23042940' stroke-width='2.4' stroke-linejoin='round'%3E%3Cpath d='M12 3 L20 7.5 V16.5 L12 21 L4 16.5 V7.5 Z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: center;
    background-size: 13px 13px;
}

/* A .says that wraps several blocks (a line plus a recovery button) keeps its
   own internal rhythm. */
.says > * + * {
    margin-top: var(--space-3);
}

/* The one variant, and it exists because applying .says to real content found a
   case the single class could not carry: some of what the assistant says about
   itself is also something the owner has to act on ("I woke from a bad dream",
   "I couldn't reach your storefront", "copy this key now — it is shown once").
   Forcing a choice between the character and the urgency would lose one of
   them, so this keeps the mark and adds the danger inset. It is NOT a severity
   scale — there is no --says--info or --says--success. Either a line needs
   acting on or it does not. */
.says--attention {
    padding: var(--space-3) var(--space-4);
    padding-left: calc(20px + var(--space-3) + var(--space-4));
    border: 1px solid var(--danger-border);
    border-radius: var(--radius-md);
    background: var(--danger-bg);
    color: var(--danger-text);
}

/* The mark stays clear of the box's own padding. It keeps its own colours in
   there too — it is the one element in the portal that never re-themes. */
.says--attention::before {
    left: var(--space-4);
    top: calc(var(--space-3) + 1px);
}

/* ================================================================= NOTICES
   The SYSTEM channel: the outcome of something the user just did, or a
   condition of the page. Boxed and tinted, because a notice is a report the
   user should be able to dismiss mentally once read. */
.notice {
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
    /* Full strength: a notice is the system reporting an outcome the reader is
       meant to act on. It is boxed and tinted precisely to be READ, so its body
       is primary text — the legacy sheet set --text here for the same reason. */
    color: var(--text);
    font-size: var(--text-sm);
    line-height: var(--leading-relaxed);
    max-width: var(--measure);
}

/* .notice-muted is the DEFAULT treatment and therefore declares nothing of its
   own — the class stays in the markup so a neutral notice states its intent
   next to its -warn and -success siblings, rather than reading as one that
   forgot its variant. Deliberately not a no-op rule repeating .notice. */

.notice-warn {
    background: var(--danger-bg);
    border-color: var(--danger-border);
    color: var(--danger-text);
}

.notice-success {
    background: var(--success-bg);
    border-color: var(--success-border);
    color: var(--success-text);
}

/* The trial-ended legible-off notice (#554) is a multi-block notice — a lead sentence
   over a list of conditions and an action — so it keeps vertical rhythm between its parts,
   mirroring .says. Scoped to this notice: the site's other notices are single-block and a
   bare `.notice > * + *` would silently re-space any that aren't. */
[data-trial-notice] > * + * {
    margin-top: var(--space-3);
}

.validation-summary-errors {
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--danger-border);
    border-left: 3px solid var(--danger);
    border-radius: var(--radius-md);
    background: var(--danger-bg);
    color: var(--danger-text);
    font-size: var(--text-sm);
    max-width: var(--measure);
}

.validation-summary-errors ul {
    margin: 0;
    padding-left: var(--space-4);
}

/* ASP.NET's validation-summary tag helper ALWAYS emits its list, even with no
   errors — `<ul><li style="display:none"></li></ul>` — so neither :empty guard
   below can ever match, and the box rendered on every pristine form: 26x310px of
   --danger-bg with a 3px danger left border and no text in it. Invisible under
   the legacy sheet, which only ever set `color` on this class; it became visible
   the moment #519 gave the summary a real box, and it is on Login, Signup,
   ForgotPassword, API keys, AI providers and Account.

   Keying on the framework's own state class is the fix — it is the only thing in
   the markup that actually distinguishes "no errors", and it is set by the same
   tag helper that emits the list. The :empty rules stay for the hand-written
   summaries (the Configuration sections build their own). #519 Phase 4. */
.validation-summary-errors.validation-summary-valid,
.validation-summary-errors:empty,
.validation-summary-errors ul:empty {
    display: none;
}

/* ================================================================= BUTTONS */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    height: var(--control-height);
    padding: var(--space-1) var(--space-4);
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    line-height: var(--leading-tight);
    text-decoration: none;
    cursor: pointer;
    transition: background-color var(--motion-fast) var(--ease-out),
                border-color var(--motion-fast) var(--ease-out),
                color var(--motion-fast) var(--ease-out);
}

.btn:hover {
    text-decoration: none;
}

/* White-on-accent measures 7.91:1 light / 4.50:1 dark — the label colour is
   fixed white in both themes, which is why --accent's dark step is a DARKENED
   brand teal rather than the lightened --accent-text. */
.btn-primary {
    background: var(--accent);
    border-color: var(--accent);
    color: #ffffff;
}

.btn-primary:hover {
    /* --accent-fill-hover, not --accent-hover: white on the latter measures
       2.95:1 in dark. See the token's note in tokens.css. */
    background: var(--accent-fill-hover);
    border-color: var(--accent-fill-hover);
    color: #ffffff;
}

.btn-secondary {
    background: var(--surface);
    border-color: var(--border);
    color: var(--text);
}

.btn-secondary:hover {
    background: var(--surface-2);
    border-color: var(--text-muted);
    color: var(--text);
}

.btn:disabled,
.btn[aria-disabled="true"] {
    opacity: 0.55;
    cursor: not-allowed;
}

/* A link wearing .btn for LAYOUT only — it sits in a .form-actions row beside a
   real button and has to share its baseline. It carries no fill and no border so
   the row still reads as one primary action plus an escape hatch, never as two
   competing buttons. Underline on hover, because without a border the hover
   state has nothing else to say. */
.btn-link {
    padding-inline: var(--space-1);
    border-color: transparent;
    background: transparent;
    color: var(--accent-text);
}

.btn-link:hover:not(:disabled) {
    color: var(--accent-hover);
    text-decoration: underline;
}

/* Destructive action (#575). A tinted OUTLINE, not a solid --danger fill:
   --danger-text on --danger-bg is the legible red that white-on-#dc2626 (~4.5:1)
   can't guarantee, and both tokens are defined in each theme. Used for the
   knowledge source remove confirm. */
.btn-danger {
    background: var(--danger-bg);
    border-color: var(--danger-border);
    color: var(--danger-text);
}

.btn-danger:hover {
    background: var(--danger-tint);
    border-color: var(--danger);
    color: var(--danger-text);
}

/* Knowledge source lifecycle actions (#575) — the list row's action column: a single row
   of compact icon buttons (re-ingest, remove); the confirm disclosure and the run
   label/progress drop below that row. */
.knowledge-actions {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    align-items: flex-start;
}

.knowledge-actions__row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
}

.knowledge-actions .knowledge-action {
    display: inline-flex;
    margin: 0;
}

.knowledge-run-state {
    margin: 0;
}

/* Status column (#694): a dot in a state colour beside the word. The state is ALSO in the word,
   never colour alone (components.css state-indicator rule). The dot is inline-block so a long live
   poll message wraps under the word with the dot staying on the first line. Four tones over the
   shared token set — both themes already define each, so no one-theme-only token can slip in. */
.knowledge-status__dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-right: var(--space-2);
    border-radius: 50%;
    background: var(--text-muted);
}

.knowledge-status__dot--working { background: var(--accent); }
.knowledge-status__dot--ready { background: var(--success-text); }
.knowledge-status__dot--failed { background: var(--danger-text); }
.knowledge-status__dot--muted { background: var(--text-muted); }

/* The pages an owner ticked, under the source name (#694) — a quiet second line, the source link
   stays the lead. */
.knowledge-source-cell__pages {
    display: block;
    margin-top: var(--space-05);
    font-size: var(--text-xs);
    color: var(--text-muted);
}

/* Analyze wait line (#694): revealed by the submit script while a website is being looked at. No
   author display, so the UA [hidden] rule hides it until the script clears the attribute. */
.knowledge-analyzing {
    margin-top: var(--space-2);
}

/* Icon-only action button (also the remove disclosure's <summary>): a bordered square,
   its label carried by aria-label + title, so a row of them reads as one tidy control set. */
.knowledge-action-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--control-height);
    height: var(--control-height);
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    color: var(--text-muted);
    cursor: pointer;
    list-style: none; /* kill the <summary> disclosure marker */
    transition: background-color var(--motion-fast) var(--ease-out),
                color var(--motion-fast) var(--ease-out),
                border-color var(--motion-fast) var(--ease-out);
}

.knowledge-action-btn::-webkit-details-marker {
    display: none;
}

.knowledge-action-btn:hover {
    background: var(--surface-2);
    color: var(--text);
}

.knowledge-action-btn--danger:hover {
    background: var(--surface);
    border-color: var(--danger-text);
    color: var(--danger-text);
}

.knowledge-action-btn:disabled,
.knowledge-action-btn[aria-disabled="true"] {
    opacity: 0.45;
    cursor: not-allowed;
}

/* JS-free delete confirm: opening the disclosure drops the irreversible-delete copy and
   the confirm button that POSTs, in a --danger-bg card so the destructive step reads as one. */
.knowledge-remove__confirm {
    width: 16rem;
    margin-top: var(--space-2);
    padding: var(--space-3);
    border: 1px solid var(--danger-border);
    border-radius: var(--radius-sm);
    background: var(--danger-bg);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.knowledge-remove__lead {
    margin: 0;
    font-weight: 600;
}

.knowledge-remove__name {
    word-break: break-all;
}

/* The Knowledge surface stacks independent panels (add-a-website, add-a-PDF, the
   source list). The wrapper is a single child of .portal-main, so it has to own the
   page rhythm its parent would otherwise apply to each panel. */
.knowledge-surface {
    display: flex;
    flex-direction: column;
    gap: var(--space-8);
}

/* Pick-the-pages list (#574 add-a-website). Each candidate is a selectable row: the
   checkbox with the page title over its muted URL, bounded so a long list reads as one
   grouped set rather than loose form rows. The fieldset keeps its own gap for the
   legend/help/list block; the option rows live in __list so a large site's discovered
   pages scroll inside a fixed height instead of growing the page unbounded (#612). */
.knowledge-pages {
    gap: var(--space-2);
}

/* Page-picker toolbar (#694): select-all, a title/URL filter and a live count over the list. Revealed
   by the picker script only past a small page threshold; server-rendered hidden, so [hidden] must win
   over this flex display. */
.knowledge-pages__toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-1);
}

.knowledge-pages__toolbar[hidden] {
    display: none;
}

.knowledge-pages__all {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    white-space: nowrap;
    cursor: pointer;
}

.knowledge-pages__filter {
    flex: 1 1 12rem;
    min-width: 0;
}

.knowledge-pages__count {
    margin: 0;
    white-space: nowrap;
}

/* The filter hides non-matching rows without removing them (a filtered-out ticked page still posts).
   Double-class so it out-specifies `.knowledge-page-option { display: flex }` no matter the source
   order — a same-specificity single-class rule would lose the tie to the later base rule. */
.knowledge-page-option.knowledge-page-option--filtered {
    display: none;
}

.knowledge-pages__list {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    max-height: 24rem;
    overflow-y: auto;
    /* Small inset so the scrollbar and each row's edge don't butt against the clip box;
       overflow-y:auto makes overflow-x non-visible, so the box clips at its edge. */
    padding: var(--space-1);
}

.knowledge-page-option {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    cursor: pointer;
    transition: background-color var(--motion-fast) var(--ease-out),
                border-color var(--motion-fast) var(--ease-out);
}

.knowledge-page-option:hover {
    background: var(--surface-2);
    border-color: var(--text-muted);
}

.knowledge-page-option > input[type="checkbox"] {
    margin-top: 0.15em;
    flex: none;
}

.knowledge-page-option__text {
    display: flex;
    flex-direction: column;
    gap: var(--space-05);
    min-width: 0;
}

.knowledge-page-option__title {
    font-weight: var(--weight-medium);
    color: var(--text);
    word-break: break-word;
}

.knowledge-page-option__url {
    font-size: var(--text-xs);
    color: var(--text-muted);
    word-break: break-all;
}

/* Source-type picker (#580). Two selectable cards over the two add panes: the icon leads
   on the left and the real radio sits in the top-right corner, so "what it is" and
   "is it selected" don't stack on the same side. The label card is the affordance; the
   reveal + selected treatment are CSS-only, below. */
/* Library-first Knowledge (#613 follow-up): the sources list is the page; adding is an on-demand
   <details>. The summary is a button, the form expands inline. The server renders it open after a
   website Analyze round-trip so the candidate-pages step needs no drawer and shows no reopen flash. */
.knowledge-add {
    margin: var(--space-4) 0 var(--space-6);
}

.knowledge-add__toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    width: max-content;
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    color: var(--text);
    cursor: pointer;
    list-style: none; /* no default disclosure triangle */
    transition: background-color var(--motion-fast) var(--ease-out),
                color var(--motion-fast) var(--ease-out);
}

.knowledge-add__toggle::-webkit-details-marker {
    display: none;
}

.knowledge-add__toggle:hover {
    background: var(--surface-2);
}

/* Open: the button reads as active (the accent-tint pill used across the portal's active states). */
.knowledge-add[open] > .knowledge-add__toggle {
    background: var(--accent-tint);
    border-color: transparent;
    color: var(--accent-on-tint);
}

.knowledge-add__body {
    margin-top: var(--space-4);
}

/* A quiet, centred invite when the assistant hasn't learned anything yet (the add form sits open
   above it, so this is a nudge, not a dead end). */
.knowledge-empty {
    margin: 0;
    padding: var(--space-6) var(--space-4);
    text-align: center;
    color: var(--text-muted);
}

/* The add-area disclaimer is a light guidance line spanning both columns, not a boxed notice. */
.knowledge-add__note {
    margin: 0 0 var(--space-4);
    max-width: none;
}

/* Source picker (#613 follow-up), ported from operator-service's KnowledgeSources (ADR 0016:
   pattern, not code). Two selectable cards side by side, each with a radio, an icon in an
   accent-tinted square, a name + type badge, a description, and its form fields inside. The
   selected card takes an accent border; the shared submit routes to it. Teal tokens here. */
.source-picker {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
    gap: var(--space-4);
    margin-top: var(--space-4);
    /* It is a <fieldset> (it groups the source-type radios) — strip the UA border/padding/inline margin;
       min-inline-size:0 lets the grid tracks shrink instead of forcing the fieldset's min-content width. */
    border: 0;
    padding: 0;
    margin-inline: 0;
    min-inline-size: 0;
}

.source-card {
    position: relative;
    display: flex;
    flex-direction: column;
    padding: var(--space-5);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface);
    transition: border-color var(--motion-fast) var(--ease-out),
                box-shadow var(--motion-fast) var(--ease-out);
}

/* Highlight the selected card (its connector radio checked) — a CSS-only cue, no JS. Scoped to the
   radio in the select cell so the website card's ticked candidate-page checkboxes never match. */
.source-card:has(.source-card__select input:checked) {
    border-color: var(--accent-text);
    box-shadow: inset 0 0 0 1px var(--accent-text);
}

/* The card header is a <label> for the connector radio — clicking anywhere on it selects the card,
   a large native target. It lays out the radio + icon + name/desc row. */
.source-card__choose {
    display: grid;
    grid-template-columns: auto auto 1fr;
    align-items: center;
    gap: var(--space-1) var(--space-4);
    cursor: pointer;
}

/* Make the WHOLE card the click target for its radio, not just the header row: a transparent
   overlay stretched over the card (its containing block is the position:relative .source-card).
   The body lifts above it (below) so its fields and submit stay live. */
.source-card__choose::after {
    content: "";
    position: absolute;
    inset: 0;
    cursor: pointer;
}

/* The radio itself lifts above the overlay so it takes a direct click (the overlay only needs the
   card's dead space — the icon, name and padding still select via the label they sit inside). */
.source-card__select {
    position: relative;
    z-index: 1;
}

.source-card__select input {
    width: 1.05rem;
    height: 1.05rem;
    accent-color: var(--accent-text);
    cursor: pointer;
}

.source-card__icon {
    display: grid;
    place-items: center;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: var(--radius-md);
    background: var(--accent-tint);
    color: var(--accent-text);
}

.source-card__icon svg {
    width: 22px;
    height: 22px;
}

.source-card__head {
    display: flex;
    flex-direction: column;
    gap: var(--space-05);
}

.source-card__name {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-weight: var(--weight-semibold);
    color: var(--text);
}

.source-card__badge {
    padding: 0.1rem 0.5rem;
    border-radius: var(--radius-pill);
    background: var(--accent-tint);
    color: var(--accent-text);
    font-size: var(--text-micro);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
}

.source-card__desc {
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.source-card__body {
    /* Above the card-select overlay so the fields, dropzone and submit stay clickable. */
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin-top: var(--space-4);
    min-width: 0;
}

/* A label-wrapping field inside a card (operator's shape), scoped so it doesn't touch the
   portal's other .field usages. */
.source-card .field {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
}

.source-card .field input {
    width: 100%;
}

/* Each card's own submit sits at the foot of its body, sized to its label (never stretched by the
   flex-column body). */
.source-card__actions {
    display: flex;
    margin-top: var(--space-2);
}

/* An input with a leading icon (the URL field). */
.field-input {
    position: relative;
    display: block;
}

.field-input__icon {
    position: absolute;
    left: 0.7rem;
    top: 50%;
    transform: translateY(-50%);
    display: grid;
    place-items: center;
    pointer-events: none;
    color: var(--text-muted);
}

.field-input__icon svg {
    width: 16px;
    height: 16px;
}

/* Clear the leading icon. The shared input rule sets `padding` as a shorthand, so this must
   out-specify it (two classes + the type attribute) or the icon overlaps the placeholder. */
.source-card .field-input input[type="text"] {
    padding-left: 2.1rem;
}

/* Multi-file PDF dropzone (#580). A dashed drop area that IS a <label> wrapping the file
   input — so clicking anywhere opens the picker with no JavaScript and no raw file button
   showing; the input is screen-reader-only but focusable (:focus-within rings the box).
   Drag-over adds a solid-border shape cue. Chosen files render below as a summary + list. */
.knowledge-dropzone {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-4);
    border: 1.5px dashed var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
    text-align: center;
    cursor: pointer;
    transition: border-color var(--motion-fast) var(--ease-out),
                background-color var(--motion-fast) var(--ease-out);
}

.knowledge-dropzone:focus-within {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
}

/* Screen-reader-only, but focusable and clickable via its wrapping label — not
   display:none (which would drop it from the tab order and from the form's validation). */
.knowledge-dropzone__input {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

.knowledge-dropzone:hover {
    border-color: var(--text-muted);
}

.knowledge-dropzone__icon {
    width: 22px;
    height: 22px;
    color: var(--text-muted);
    transition: color var(--motion-fast) var(--ease-out);
}

.knowledge-dropzone:hover .knowledge-dropzone__icon,
.knowledge-dropzone.is-dragover .knowledge-dropzone__icon {
    color: var(--accent-text);
}

/* Drag-over: a shape change (dashed → solid) alongside the accent, so the state never
   rests on colour alone. The border is a foreground line, so --accent-text, not --accent. */
.knowledge-dropzone.is-dragover {
    border-style: solid;
    border-color: var(--accent-text);
    background: var(--surface);
}

.knowledge-dropzone__label {
    margin: 0;
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
}

.knowledge-dropzone__hint {
    margin: 0;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.knowledge-file-summary {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-top: var(--space-3);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
}

/* An author `display` beats the UA's `[hidden] { display: none }`, so the JS-toggled
   summary/list need this pairing or they show while `hidden` is set (the #336 trap). */
.knowledge-file-summary[hidden],
.knowledge-file-list[hidden] {
    display: none;
}

/* A text-style button that still meets the 24px hit-target floor (min-height + inline
   padding), so it isn't a 16px-tall tap target on touch. */
.knowledge-file-summary__clear {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    padding: 0 var(--space-2);
    border: none;
    background: none;
    font: inherit;
    font-weight: var(--weight-medium);
    color: var(--accent-text);
    text-decoration: underline;
    cursor: pointer;
}

.knowledge-file-summary__clear:hover {
    text-decoration: none;
}

.knowledge-file-list {
    list-style: none;
    margin: var(--space-2) 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.knowledge-file-list__item {
    display: flex;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
}

.knowledge-file-list__name {
    word-break: break-all;
}

.knowledge-file-list__size {
    flex: none;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

/* Live ingest progress (#580). The just-added-source region stacks its owner-words
   label over a thin bar; the list row shows the same bar under its run-state label. */
.knowledge-progress {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.knowledge-progress__label {
    margin: 0;
}

/* A thin determinate/indeterminate bar. Reset the UA chrome, then paint the track and
   the filled value from tokens in BOTH themes (an accent fill on a surface-2 track). An
   indeterminate bar (no value attribute — total not known yet) keeps the browser's own
   animation over the same track. */
.knowledge-progress-bar {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: 8px;
    border: none;
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    overflow: hidden;
}

.knowledge-progress-bar::-webkit-progress-bar {
    background: var(--surface-2);
    border-radius: var(--radius-sm);
}

.knowledge-progress-bar::-webkit-progress-value {
    background: var(--accent);
    border-radius: var(--radius-sm);
    transition: width var(--motion-base) var(--ease-out);
}

.knowledge-progress-bar::-moz-progress-bar {
    background: var(--accent);
    border-radius: var(--radius-sm);
}

/* Source page: the back link, the header (name, counts and run state beside the run actions) and
   the pages table. A page is a link (websites) or plain text (upload refs) beside its read state. */
.knowledge-back {
    margin: 0;
    font-size: var(--text-sm);
}

.knowledge-source-header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
    margin-block-end: var(--space-3);
}

.knowledge-source-header__main {
    flex: 1 1 20rem;
    min-width: 0;
}

.knowledge-source-heading {
    word-break: break-word;
}

.knowledge-read-page__link {
    word-break: break-all;
}

.knowledge-read-page__meta {
    color: var(--text-muted);
    white-space: nowrap;
}

/* The image count column: a bare number, right-aligned so the digits line up. */
.knowledge-read-page__count {
    text-align: right;
    white-space: nowrap;
}

/* A document of an upload source: its name over an optional disclosure of what was made of it. */
.knowledge-document__name {
    word-break: break-word;
}

.knowledge-document__about {
    margin-top: var(--space-1);
    font-size: var(--text-sm);
}

.knowledge-document__about > summary {
    color: var(--text-muted);
    cursor: pointer;
}

.knowledge-document__title {
    font-weight: var(--weight-semibold);
}

.knowledge-document__meta {
    color: var(--text-muted);
    white-space: nowrap;
}

/* One read page: the address under its title, the text the assistant read, the images found on it
   and the documents it links to. Every text here is storefront or model-written, so long unbroken
   words wrap instead of widening the panel. */
.knowledge-page__address {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--text-xs);
    word-break: break-all;
}

.knowledge-page__section > h3 {
    margin: 0;
}

/* Plain text whose line breaks are the paragraphs. A table row arrives as one line of cells
   separated by "|", so keeping the breaks keeps the rows apart. */
.knowledge-page__text {
    max-width: var(--measure);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* The collapsed text. The markup never carries this class, so the whole text shows until it is
   added. The fade is decorative and carries no text. */
.knowledge-page__text--clamped {
    position: relative;
    max-height: 18rem;
    overflow: hidden;
}

.knowledge-page__text--clamped::after {
    content: "";
    position: absolute;
    inset: auto 0 0;
    height: var(--space-12);
    background: linear-gradient(to bottom, transparent, var(--surface));
    pointer-events: none;
}

.knowledge-page__more {
    align-self: flex-start;
}

.knowledge-page__more[hidden] {
    display: none;
}

.knowledge-file-meta {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--text-xs);
}

.knowledge-file-about {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.knowledge-file-about[hidden] {
    display: none;
}

.knowledge-file-about__description {
    margin: 0;
    max-width: var(--measure);
    overflow-wrap: anywhere;
}

.knowledge-file-about__label {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
}

.knowledge-file-about .keyword-chips {
    margin: 0;
    padding: 0;
    list-style: none;
}

/* The image grid: a square thumbnail per image with its title and type under it. */
.knowledge-images {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
    gap: var(--space-4);
    margin: 0;
    padding: 0;
    list-style: none;
}

.knowledge-image {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
}

.knowledge-image__open {
    display: block;
    width: 100%;
    aspect-ratio: 1;
    padding: 0;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
    cursor: zoom-in;
}

.knowledge-image__open:hover {
    border-color: var(--accent-text);
}

.knowledge-image__thumb {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.knowledge-image__title {
    margin: 0;
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    overflow-wrap: anywhere;
}

/* The documents a page links to: a row each, the open action at its trailing edge. */
.knowledge-page-documents {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
}

.knowledge-page-document {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}

.knowledge-page-document__body {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
}

.knowledge-page-document__title {
    margin: 0;
    font-weight: var(--weight-semibold);
    overflow-wrap: anywhere;
}

.knowledge-page-document__open {
    flex: none;
}

/* The image viewer: a modal over the whole viewport. It floats over the page, so it takes the
   overlay shadow and a dimming scrim. */
.knowledge-lightbox {
    position: fixed;
    inset: 0;
    z-index: var(--z-overlay);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-6);
}

.knowledge-lightbox[hidden] {
    display: none;
}

.knowledge-lightbox__scrim {
    position: absolute;
    inset: 0;
    background: var(--scrim);
}

.knowledge-lightbox__dialog {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    width: min(56rem, 100%);
    max-height: 100%;
    overflow: auto;
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: var(--shadow-overlay);
}

.knowledge-lightbox__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}

.knowledge-lightbox__position {
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
}

.knowledge-lightbox__stage {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    min-height: 12rem;
    border-radius: var(--radius-md);
    background: var(--surface-2);
}

.knowledge-lightbox__image {
    display: block;
    min-width: 0;
    max-width: 100%;
    max-height: 60vh;
    object-fit: contain;
}

.knowledge-lightbox__nav {
    flex: none;
}

.knowledge-lightbox__nav[hidden] {
    display: none;
}

.knowledge-lightbox__detail {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.knowledge-lightbox__title {
    margin: 0;
    font-size: var(--text-md);
    overflow-wrap: anywhere;
}

/* The promote star. A toggle whose state is carried by fill AND by
   aria-pressed, so the state is never colour-alone. */
.btn-star {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--control-height);
    height: var(--control-height);
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    background: none;
    color: var(--text-muted);
    cursor: pointer;
    transition: color var(--motion-fast) var(--ease-out),
                background-color var(--motion-fast) var(--ease-out);
}

button.btn-star:hover {
    background: var(--surface-2);
    color: var(--text);
}

.btn-star--on {
    color: var(--accent-text);
}

/* ==================================================================== FORMS
   A settings page is the hardest surface in this portal, and the thing that
   makes it hard is that every field looks equally important. The system's
   answer: the LABEL carries the weight, the control is quiet, and the hint is
   demoted below both. */
.form-grid {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.form-fieldset {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    margin: 0;
    padding: 0;
    border: 0;
}

.form-fieldset > legend {
    padding: 0;
    margin-bottom: var(--space-3);
    font-family: var(--font-display);
    font-size: var(--text-md);
    font-weight: var(--weight-semibold);
    color: var(--text);
}

.field {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.field > label,
.field-label {
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    color: var(--text);
}

/* Explanatory prose. .field-hint sits under one control; .help explains a whole
   section.

   Both exist because the old sheet had neither, so permanent explanation was
   written as `notice notice-muted` — a boxed, tinted SYSTEM REPORT. That is why
   the configuration page read as a wall of alerts: a dozen standing sentences
   of help were each dressed as something that had just happened. Explanation is
   quiet, unboxed, and demoted below the thing it explains. */
.field-hint,
.help {
    font-size: var(--text-xs);
    line-height: var(--leading-relaxed);
    color: var(--text-muted);
    max-width: var(--measure);
}

.field-managed {
    font-size: var(--text-xs);
    line-height: var(--leading-relaxed);
    color: var(--text-muted);
    max-width: var(--measure);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
}

.field-managed code {
    padding: 0;
    background: none;
    color: var(--text);
}

/* Per-field validation, emitted by asp-validation-for. Same size as .field-hint
   and in the same slot under the control, but in danger ink — a rejected field
   should not also change the form's layout, so it must occupy the hint's place
   rather than push the next field down. Empty by default: the tag helper always
   renders the span, so an unstyled one would reserve a blank line under every
   input on the page.

   .onb-field__error joins this rule rather than restating it in the scene layer:
   onboarding-scene.js EMITS that class name when it builds a finding card, so the
   scene cannot simply write .field-validation-error in markup — but a rejected
   field is a rejected field, and a second copy of the declaration is how the two
   drift. */
.field-validation-error,
.onb-field__error {
    font-size: var(--text-xs);
    line-height: var(--leading-relaxed);
    color: var(--danger-text);
}

.field-validation-error:empty,
.onb-field__error:empty {
    display: none;
}

/* .onb-card__label input is the onboarding finding card's shop-name field, built by
   onboarding-scene.js and therefore classless in the same way — it is the same
   control as every field below and takes the same rule. */
.onb-card__label input,
.field input[type="text"],
.field input[type="email"],
.field input[type="password"],
.field input[type="search"],
.field input[type="url"],
.field input[type="number"],
.field select,
.field textarea,
.form-grid input[type="text"],
.form-grid input[type="email"],
.form-grid select,
.form-grid textarea,
.setting-row__control input[type="text"],
.setting-row__control input[type="email"],
.setting-row__control input[type="url"],
.setting-row__control input[type="number"],
.setting-row__control select,
.setting-row__control textarea,
/* The list-page filter controls join here so their border, surface and ink are
   the same rule as every other input and cannot drift from it. Each keeps its
   own layout deltas (the search field's icon and clear padding, the bar's
   auto widths, below at FILTER BAR); only the control look is shared. */
.search-form__input,
.knowledge-pages__filter,
.filter-period__dates input[type="date"],
.filter-bar select {
    width: 100%;
    /* The control height holds the row level; the block padding, size and line
       height are the ones .btn takes, so a field and a button centre alike. */
    height: var(--control-height);
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
    font-size: var(--text-sm);
    line-height: var(--leading-tight);
    transition: border-color var(--motion-fast) var(--ease-out);
}

/* The portal's own chevron, in the muted ink of the active theme. The trailing
   padding keeps a long option clear of it. */
.field select,
.form-grid select,
.setting-row__control select,
.filter-bar select,
.portal-shop-switcher {
    appearance: none;
    padding-inline-end: var(--space-8);
    background-image: var(--select-chevron);
    background-position: right var(--space-3) center;
    background-size: 10px 6px;
    background-repeat: no-repeat;
}

/* Forced colours repaints the control but not the colour baked into the chevron;
   the native arrow follows the system colours. */
@media (forced-colors: active) {
    .field select,
    .form-grid select,
    .setting-row__control select,
    .filter-bar select,
    .portal-shop-switcher {
        appearance: auto;
        background-image: none;
    }
}

.field input:hover,
.field select:hover,
.field textarea:hover,
.onb-card__label input:hover,
.setting-row__control input:hover,
.setting-row__control select:hover,
.setting-row__control textarea:hover,
.search-form__input:hover,
.knowledge-pages__filter:hover,
.filter-period__dates input[type="date"]:hover,
.filter-bar select:hover {
    border-color: var(--text-muted);
}

.field textarea,
.form-grid textarea,
.setting-row__control textarea {
    /* The one multi-line control: it grows with its content, not to the control height. */
    height: auto;
    resize: vertical;
    min-height: 5rem;
    line-height: var(--leading-normal);
}

.field input[type="color"],
.setting-row__control input[type="color"] {
    width: 64px;
    height: var(--control-height);
    padding: 2px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    cursor: pointer;
}

.check-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    font-size: var(--text-sm);
    color: var(--text);
    cursor: pointer;
}

.check-row input[type="checkbox"] {
    width: 16px;
    height: 16px;
    accent-color: var(--accent);
    cursor: pointer;
}

.form-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-3);
    padding-top: var(--space-2);
}

.form-actions .field-hint {
    margin: 0;
}

.form-inline {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
}

/* Accent presets. Swatches are a convenience over the colour input; each
   carries its hex as an accessible name, so the colour is never the only
   information. */
.swatch-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.swatch {
    width: var(--control-height);
    height: var(--control-height);
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: transform var(--motion-fast) var(--ease-out);
}

.swatch:hover {
    transform: scale(1.08);
}

/* A read-only prompt is not a disabled input — it is text you can select and
   copy, so it reads as an inset rather than a dead field. */
.assistant-prompt-readonly {
    width: 100%;
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
    /* Full strength: the prompt IS this control's content — text to read, select
       and copy. The inset says "read-only"; muting it would say "inactive". */
    color: var(--text);
    font-size: var(--text-xs);
    line-height: var(--leading-relaxed);
    resize: vertical;
}

/* =============================================================== DISCLOSURE */
.disclosure > summary {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    cursor: pointer;
    list-style: none;
}

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

.disclosure > summary::before {
    content: "";
    width: 0;
    height: 0;
    border-left: 5px solid currentColor;
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    transition: transform var(--motion-fast) var(--ease-out);
}

.disclosure[open] > summary::before {
    transform: rotate(90deg);
}

.disclosure > summary:hover {
    color: var(--accent-text);
}

.disclosure-summary {
    display: inline;
    font-size: var(--text-md);
}

.disclosure > *:not(summary) {
    margin-top: var(--space-4);
}

/* ==================================================================== TABLE
   Dense by design. Hairline row rules rather than zebra striping: striping
   adds a second surface colour to every page for no information, and in this
   warm palette it muddies. Header is an eyebrow, numbers are tabular. */
/* A wide table scrolls inside its own container — the page body must never
   scroll horizontally.

   EVERY .table-scroll in the markup carries role="region", an aria-label and
   tabindex="0". That is not decoration: a container that scrolls must be
   operable by keyboard (2.1.1), and at the 320px width WCAG 1.4.10 requires the
   portal to work at, these do scroll. Tabbing to a link inside the table is only
   an accidental workaround — two of the eight tables (the scrape-run history and
   the chat engagements) have nothing focusable in any row, so a keyboard user
   simply could not reach their cut-off columns. Applied to all eight rather than
   the two, so the property does not depend on whether a given table happens to
   contain a link today; naming each region also makes them screen-reader
   navigable, which is the compensation for the extra tab stop. #519 Phase 4. */
.table-scroll {
    overflow-x: auto;
}

/* Inside a panel the scroller bleeds to the panel's edge, so the scrollbar and
   the cut-off column sit at the boundary rather than floating inside the
   padding. The bleed is scoped to a direct panel-body child ON PURPOSE: a
   .region has no horizontal padding, so an unscoped negative margin would pull
   its table out past the content column. */
.panel-body > .table-scroll {
    margin: 0 calc(var(--space-5) * -1);
    padding: 0 var(--space-5);
}

.data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-sm);
    /* Column alignment across rows is what makes a table scannable. */
    font-variant-numeric: tabular-nums;
}

.data-table th {
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border);
    text-align: left;
    font-family: var(--font-body);
    font-size: var(--text-micro);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
    white-space: nowrap;
}

.data-table td {
    padding: var(--space-3);
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
    color: var(--text);
}

.data-table tbody tr:last-child td {
    border-bottom: 0;
}

/* A timestamp or identifier column. Held to its content because it is a LOOKUP —
   you check it once you have already found the row by its name — and demoted for
   the same reason. Promoted out of the catalog's page rules when the chat archive
   wanted the identical treatment; the .catalog-* names are kept in this selector
   list so Catalog's shipped markup did not have to be edited to follow. */
.date-cell,
.id-cell,
.catalog-id-cell,
.catalog-date-cell {
    white-space: nowrap;
    color: var(--text-muted);
}

/* Row-level controls. Each one is its own <form> (they post different handlers),
   so without this they stack as block elements and every row grows three lines
   tall. Wraps rather than scrolls: the actions column is the one place a narrow
   viewport may reflow. */
.table-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

.data-table tbody tr {
    transition: background-color var(--motion-fast) var(--ease-out);
}

.data-table tbody tr:hover {
    background: var(--surface-2);
}

/* A sortable header is a link inside the th, so it inherits the eyebrow
   treatment and only signals interactivity on hover. */
/* min-height is the 2.5.8 target-size floor (AA). A sort link's own box is only
   as tall as its 12.5px line, and the criterion's inline-text exception does not
   cover it: the exception is for links inside a sentence, and a column header is
   not a sentence. The header cell's padding does not count — it is the parent's
   box, not the target's. */
.sort-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-height: 24px;
    color: inherit;
}

.sort-link:hover {
    color: var(--accent-text);
    text-decoration: none;
}

.sort-indicator {
    color: var(--accent-text);
}

/* ================================================================== KV LIST
   Label/value pairs. A two-column grid rather than a table: these are facts
   about one thing, not rows of comparable records. */
.kv-list {
    display: grid;
    grid-template-columns: minmax(8rem, 14rem) minmax(0, 1fr);
    gap: 0;
    margin: 0;
}

.kv-row {
    display: contents;
}

.kv-list dt,
.kv-list dd {
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
    margin: 0;
}

.kv-list dt {
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    color: var(--text-muted);
    padding-right: var(--space-4);
}

.kv-list dd {
    color: var(--text);
    overflow-wrap: anywhere;
}

.kv-row:last-child dt,
.kv-row:last-child dd {
    border-bottom: 0;
}

@media (max-width: 640px) {
    .kv-list {
        grid-template-columns: minmax(0, 1fr);
    }

    .kv-list dt {
        padding-bottom: 0;
        border-bottom: 0;
    }

    .kv-list dd {
        padding-top: var(--space-1);
    }
}

/* ================================================================ KPI TILES
   The display numbers, and the one place Outfit is used for figures. Its
   geometric digits make a measurement read as data rather than as text.
   Table numbers deliberately stay on DM Sans + tabular-nums — see tokens.css. */
.kpi-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: var(--space-3);
}

.kpi-tile {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-4) var(--space-5);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
}

.kpi-tile__label {
    font-size: var(--text-micro);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
}

.kpi-tile__value {
    font-family: var(--font-display);
    font-size: var(--text-2xl);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-tight);
    letter-spacing: var(--tracking-tight);
    color: var(--text);
    font-variant-numeric: tabular-nums;
}

/* A change against the previous period. The sign or the word is the text, so colour only
   repeats it. Each verdict takes its status trio whole; a neutral change stays muted. */
.kpi-tile__delta {
    align-self: flex-start;
    display: inline-flex;
    padding: var(--space-05) var(--space-2);
    border: 1px solid transparent;
    border-radius: var(--radius-pill);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    font-variant-numeric: tabular-nums;
}

.kpi-tile__delta--neutral {
    border-color: var(--border);
    background: var(--surface-2);
    color: var(--text-muted);
}

.kpi-tile__delta--better {
    border-color: var(--success-border);
    background: var(--success-bg);
    color: var(--success-text);
}

.kpi-tile__delta--worse {
    border-color: var(--danger-border);
    background: var(--danger-bg);
    color: var(--danger-text);
}

/* A tile that filters the list under it. The border answers hover and marks the tile the list is
   filtered to; the global focus ring covers the keyboard. Tokens carry both themes. */
.kpi-tile--link {
    color: inherit;
    transition: border-color 120ms ease;
}

.kpi-tile--link:hover {
    border-color: var(--accent);
}

.kpi-tile--link.is-current {
    border-color: var(--accent);
    box-shadow: inset 0 0 0 1px var(--accent);
}

.kpi-tile__note {
    font-size: var(--text-xs);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* =================================================================== CHARTS
   WHY THERE ARE TWO CHARTS AND NOT ONE.

   The dashboard used to plot Turns and Errors as two series on one axis. Three
   independent rules each say that is wrong, and they converge:

   1. SCALE. Errors are a small fraction of turns, so on a shared axis the
      error series is a flat line pinned to the baseline — present but
      unreadable. Two measures of different magnitude are never a dual axis;
      they are two charts.
   2. CVD. The two brand colours available for the series — accent teal and
      danger red — measure ΔE 7.0 (light) and 5.2 (dark) under simulated
      protanopia, against a floor of 6 and a target of 8. The dark pair FAILS
      outright: a protanope could not reliably tell the two lines apart.
   3. SEMANTICS. Errors MEAN bad; turns are just volume. A series that means
      good/bad wears status colour, a series that is just identity wears
      categorical — and the two never mix inside one chart.

   Split into two single-series charts, all three dissolve: no adjacent pair
   exists to separate, each chart auto-scales to its own maximum so the error
   line is finally readable, and each colour does exactly one job. A single
   series also needs no legend box — the chart title names it, which is one
   fewer thing on the page.

   The split is markup-only: dashboard-charts.js already renders every
   svg[data-line] it finds, so no JS changed. */
.chart {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.chart__title {
    font-size: var(--text-md);
}

.chart__plot {
    display: block;
    width: 100%;
    height: auto;
    /* line.js draws the axes with stroke:currentColor at 0.2 opacity, so the
       inherited colour sets the axis weight. */
    color: var(--text);
    overflow: visible;
}

/* The series colour is applied here rather than through the JSON island so it
   is a theme-aware token instead of a hex baked into the page at render time.
   line.js emits exactly one <path> per series and <line>/<text> for the axes,
   so `path`/`circle` reach the data and nothing else. */
.chart__plot--volume path,
.chart__plot--volume circle {
    stroke: var(--chart-volume);
}

.chart__plot--volume circle {
    fill: var(--chart-volume);
}

.chart__plot--error path,
.chart__plot--error circle {
    stroke: var(--chart-error);
}

.chart__plot--error circle {
    fill: var(--chart-error);
}

/* Axis tick labels: recessive, and small enough not to compete with the data.
   CSS wins over the presentation attributes line.js sets. */
.chart__plot text {
    fill: var(--text-muted);
    font-family: var(--font-body);
    font-size: 10px;
}

/* ============================================================== FILTER BAR */
/* The one filter bar a list page filters with. Its groups, in order: the
   period, the narrowing controls, the search. A group wraps as one unit, the
   search takes the width that remains, and the trailing end holds the
   no-script Apply and Reset. Under the bar sits the count of what the list
   shows. */
.filter-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    margin-block-end: var(--space-3);
}

/* A bar that filters the whole view is a row of the page grid, outside any
   panel: full width, spaced by the grid's gap. */
.page-grid > .filter-bar {
    grid-column: 1 / -1;
    margin-block-end: 0;
}

/* A group moves to the next line whole; its own controls wrap only when the
   group alone is wider than the bar, which keeps a 320px viewport free of a
   sideways scroll. */
.filter-bar__group {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
}

/* The shared control rule makes a field fill its column; in a bar a select is
   as wide as its longest option. */
.filter-bar select {
    width: auto;
    max-width: 16rem;
}

/* The period's pills are one segmented set: one hairline between them, the
   chip radius on the outer corners only. Each pill keeps its own border and
   overlaps its neighbour's, so a set that has to wrap stays drawn. */
.pill-set {
    display: inline-flex;
    flex-wrap: wrap;
}

/* A pill and the checkbox-backed chip are the same chip. */
.pill-set__pill,
.filter-chip {
    display: inline-flex;
    align-items: center;
    height: var(--control-height);
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: var(--text-sm);
    line-height: var(--leading-tight);
    white-space: nowrap;
    text-decoration: none;
    cursor: pointer;
    transition: background-color var(--motion-fast) var(--ease-out),
                border-color var(--motion-fast) var(--ease-out);
}

.pill-set__pill:hover,
.filter-chip:hover {
    background: var(--surface-2);
    color: var(--text);
    text-decoration: none;
}

.pill-set__pill + .pill-set__pill {
    margin-inline-start: -1px;
}

.pill-set__pill:first-child {
    border-start-start-radius: var(--radius-sm);
    border-end-start-radius: var(--radius-sm);
}

/* A hidden last pill leaves the outer corners to the one before it. */
.pill-set__pill:last-child,
.pill-set__pill:has(+ .pill-set__pill[hidden]) {
    border-start-end-radius: var(--radius-sm);
    border-end-end-radius: var(--radius-sm);
}

.pill-set__pill[hidden] {
    display: none;
}

/* The lit state: the accent tint with the ink made for it, an accent hairline
   and the medium weight, so it does not rest on colour alone. Lifted so its
   hairline draws over the neighbour it overlaps. A linked chip is always lit,
   hovered too. */
.pill-set__pill[aria-current="true"],
.filter-chip:has(input:checked),
.filter-chip--linked,
.filter-chip--linked:hover {
    position: relative;
    z-index: 1;
    border-color: var(--accent-on-tint);
    background: var(--accent-tint);
    color: var(--accent-on-tint);
    font-weight: var(--weight-medium);
}

.pill-set__pill:focus-visible {
    position: relative;
    z-index: 2;
}

.filter-chip {
    gap: var(--space-2);
    border-radius: var(--radius-sm);
}

.filter-chip input[type="checkbox"] {
    width: 14px;
    height: 14px;
    margin: 0;
    accent-color: var(--accent);
    cursor: pointer;
}

/* A filter that arrived by link: not a toggle, so no pointer; the clear
   control sits inside the trailing edge. */
.filter-chip--linked {
    padding-inline-end: var(--space-1);
    cursor: default;
}

/* Capped so a long name narrows the chip, not the bar; the whole name is in
   the title and in hidden text. */
.filter-chip__name {
    min-width: 0;
    max-width: 16rem;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The chip's clear control, in the chip's own ink rather than muted, which
   the tint would not carry. */
.filter-chip__clear {
    color: inherit;
    font-weight: var(--weight-normal);
    cursor: pointer;
}

.filter-chip__clear:hover {
    background: var(--surface);
    text-decoration: none;
}

/* From and To, side by side with a muted "to" between them. */
.filter-period__dates {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

.filter-period__dates[hidden] {
    display: none;
}

.filter-period__dates input[type="date"] {
    width: auto;
}

.filter-period__to {
    font-size: var(--text-sm);
    color: var(--text-muted);
}

/* The search: a leading icon, the field, and a clear control inside the
   field's trailing edge. It is the control that grows. */
.search-form {
    position: relative;
    display: flex;
    align-items: center;
    flex: 1 1 14rem;
    min-width: 0;
}

.search-form__icon {
    position: absolute;
    left: var(--space-3);
    display: inline-flex;
    color: var(--text-muted);
    pointer-events: none;
}

/* The border, surface, ink, height and hover come from the shared control
   rule above. Only the search field's own layout lives here. */
.search-form__input {
    flex: 1 1 auto;
    min-width: 0;
    /* Leading padding clears the search icon; trailing padding keeps typed text
       off the clear control. Both are wider than a bare input's inline padding. */
    padding-inline: var(--space-8);
}

/* A clear control: a round target inside its control's trailing edge. */
.search-form__clear,
.filter-chip__clear {
    display: inline-flex;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border-radius: var(--radius-pill);
    color: var(--text-muted);
    font-size: var(--text-md);
    line-height: 1;
}

/* Inside the field's trailing edge, the conventional clear-this-input
   affordance. Sitting it outside reads as a stray glyph rather than a control
   belonging to the field. */
.search-form__clear {
    position: absolute;
    right: var(--space-1);
}

.search-form__clear:hover {
    background: var(--surface-2);
    color: var(--text);
    text-decoration: none;
}

/* The trailing end: Apply, Reset and Export. With nothing in it shown it takes
   no room, so it leaves no gap behind. */
.filter-bar__end {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-inline-start: auto;
}

.filter-bar__end:not(:has(> :not([hidden]))),
.filter-bar__end > [hidden] {
    display: none;
}

/* Export with no rows to export keeps the button's place and outline. Muted ink at the regular
   weight says it does nothing; no opacity, because the muted token clears AA on the surface only
   at full strength. The hover holds still for the same reason. */
.filter-bar__export[aria-disabled="true"],
.filter-bar__export[aria-disabled="true"]:hover {
    opacity: 1;
    background: var(--surface);
    border-color: var(--border);
    color: var(--text-muted);
    font-weight: var(--weight-normal);
    cursor: default;
}

.result-count {
    font-size: var(--text-xs);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* =================================================================== PAGER */
.pager {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-1);
}

.pager__btn,
.pager__page,
.pager__size {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--control-height);
    height: var(--control-height);
    padding: 0 var(--space-2);
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    color: var(--text);
}

a.pager__btn:hover,
a.pager__page:hover,
a.pager__size:hover {
    background: var(--surface-2);
    color: var(--text);
    text-decoration: none;
}

.pager__pages {
    display: inline-flex;
    gap: var(--space-1);
}

.pager__page.is-current {
    background: var(--accent);
    border-color: var(--accent);
    color: #ffffff;
}

/* A bound you cannot cross renders as a non-interactive span, never a dead
   link — the markup already does this; the styling has to agree. Muted ink at the
   regular weight sets it apart from the live ends; no opacity, because the muted
   token clears AA on the surface only at full strength. */
.pager__btn.is-disabled {
    color: var(--text-muted);
    font-weight: var(--weight-normal);
    cursor: default;
}

.pager__status {
    margin-left: auto;
    font-size: var(--text-xs);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* The optional page-size choice, after the status. The size in use is a bordered span, not a
   filled one, so it never reads as the current page. */
.pager__sizes {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
}

.pager__sizes-label {
    margin-inline-end: var(--space-1);
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.pager__size.is-current {
    border-color: var(--border);
    background: var(--surface-2);
    font-weight: var(--weight-semibold);
}

/* ================================================================== BADGES */
.status-badge {
    display: inline-flex;
    align-items: center;
    padding: var(--space-05) var(--space-3);
    border: 1px solid var(--accent-tint);
    border-radius: var(--radius-pill);
    background: var(--accent-tint);
    /* --accent-on-tint, not --accent-text: the ink sits on the tint. */
    color: var(--accent-on-tint);
    font-size: var(--text-micro);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
}

/* ================================================================ KEYWORDS */
.keyword-preview-groups {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.keyword-group {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.keyword-group__head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.keyword-group__title {
    font-family: var(--font-body);
    font-size: var(--text-micro);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
}

.keyword-group__count {
    font-size: var(--text-micro);
    font-weight: var(--weight-medium);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.keyword-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

/* Source is already carried by the group heading above the chips, so the chip
   variants differ only in border weight — colour is never the sole signal. */
.keyword-chip {
    display: inline-flex;
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    /* Full strength: the keyword is the DATA of this surface, not a label on it.
       The legacy sheet set --text here for the same reason. */
    color: var(--text);
    font-size: var(--text-xs);
}

.keyword-chip--manual {
    border-color: var(--accent-text);
}

.keyword-more {
    align-self: center;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

/* ================================================================= GALLERY */
.product-gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
    gap: var(--space-3);
}

.product-gallery img {
    width: 100%;
    aspect-ratio: 1;
    object-fit: contain;
    padding: var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
}

/* ============================================================ THEME TOGGLE */
.theme-toggle {
    display: inline-flex;
    gap: var(--space-05);
    padding: var(--space-05);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
}

.theme-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 26px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text-muted);
    cursor: pointer;
    transition: background-color var(--motion-fast) var(--ease-out),
                color var(--motion-fast) var(--ease-out);
}

.theme-btn:hover {
    color: var(--text);
    background: var(--surface-2);
}

.theme-btn[aria-pressed="true"] {
    background: var(--accent-tint);
    color: var(--accent-text);
}

/* ============================================================= KEY CALLOUT
   The one-time reveal of a freshly issued key. A .panel by structure — a
   bounded thing you act on — but it carries the success border because it IS
   the outcome of the action that just succeeded, and it appears only in that
   one moment. The urgency ("copy it now") is carried by the .says--attention
   line inside rather than by tinting the whole box red: nothing has gone wrong
   here, the owner simply has one chance.

   .key-callout is a TEST-COUPLED name (the widget E2E resolves the snippet
   through it) — restyle freely, but keep the class. */
.key-callout {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: var(--space-5);
    border: 1px solid var(--success-border);
    border-radius: var(--radius-lg);
    background: var(--surface);
}

/* The key itself, given room to be selected. overflow-wrap because a key is one
   unbreakable token and must never widen the page. */
.key-callout__value {
    margin: 0;
}

.key-callout__value code {
    display: block;
    padding: var(--space-3);
    font-size: var(--text-xs);
    overflow-wrap: anywhere;
}

/* ================================================================= SNIPPET */
.snippet-block {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.snippet-block pre {
    margin: 0;
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
    /* Full strength: this is the embed snippet the owner must copy verbatim into
       their storefront. It is the payload of the page, not commentary on it. */
    color: var(--text);
    font-size: var(--text-xs);
    line-height: var(--leading-relaxed);
    overflow-x: auto;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.snippet-block__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
}

/* ======================================================== INSTALL GUIDANCE */
.install-guidance {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.install-guidance__picker {
    max-width: 22rem;
}

.install-guidance ol {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    max-width: var(--measure);
    line-height: var(--leading-relaxed);
}

/* ============================================================== GUIDED TOUR
   driver.js ships light-only defaults, so the popover chrome is re-skinned on
   the portal tokens and covers both themes in one pass. The popover class is
   set by portal-tour.js ("portal-tour-popover") and .portal-tour-resume is
   created by it at runtime — both are JS contracts, not free names.
   The vendored driver.css in wwwroot/vendor/ is untouched. */
.driver-popover.portal-tour-popover {
    max-width: 320px;
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background-color: var(--surface);
    box-shadow: var(--shadow-overlay);
    color: var(--text);
    font-family: inherit;
}

.driver-popover.portal-tour-popover .driver-popover-title {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    font-weight: var(--weight-bold);
    color: var(--text);
}

.driver-popover.portal-tour-popover .driver-popover-description {
    color: var(--text);
}

.portal-tour-popover .portal-tour-popover__progress {
    display: block;
    margin-bottom: var(--space-1);
    font-size: var(--text-micro);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
}

.portal-tour-popover .portal-tour-popover__body {
    display: block;
    font-size: var(--text-sm);
    line-height: var(--leading-relaxed);
    color: var(--text-muted);
}

.driver-popover.portal-tour-popover .driver-popover-close-btn {
    color: var(--text-muted);
}

.driver-popover.portal-tour-popover .driver-popover-close-btn:hover {
    color: var(--text);
}

.driver-popover.portal-tour-popover .driver-popover-footer-btn {
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background-color: var(--surface);
    color: var(--text);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    transition: background-color var(--motion-fast) var(--ease-out),
                border-color var(--motion-fast) var(--ease-out);
}

.driver-popover.portal-tour-popover .driver-popover-footer-btn:hover {
    background-color: var(--surface-2);
}

.driver-popover.portal-tour-popover .driver-popover-next-btn {
    background-color: var(--accent);
    border-color: var(--accent);
    color: #ffffff;
    text-shadow: none;
}

.driver-popover.portal-tour-popover .driver-popover-next-btn:hover {
    background-color: var(--accent-fill-hover);
    border-color: var(--accent-fill-hover);
}

/* driver colours only the pointing side and leaves the other three
   transparent, so each side is overridden individually. */
.portal-tour-popover .driver-popover-arrow-side-bottom { border-bottom-color: var(--surface); }
.portal-tour-popover .driver-popover-arrow-side-top { border-top-color: var(--surface); }
.portal-tour-popover .driver-popover-arrow-side-left { border-left-color: var(--surface); }
.portal-tour-popover .driver-popover-arrow-side-right { border-right-color: var(--surface); }

/* "Resume tour" pill — shown when the owner wanders off the tour's current
   page. Fixed and corner-anchored, above page content but below the widget. */
.portal-tour-resume {
    position: fixed;
    bottom: var(--space-5);
    left: var(--space-5);
    z-index: 999990;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border: none;
    border-radius: var(--radius-pill);
    background-color: var(--accent);
    color: #ffffff;
    box-shadow: var(--shadow-overlay);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    cursor: pointer;
    transition: background-color var(--motion-fast) var(--ease-out);
}

.portal-tour-resume:hover {
    background-color: var(--accent-fill-hover);
}

/* =========================================================== ERROR SURFACE
   The 404/500 page (#530) — the one component that has to render when the rest
   of the unit cannot.

   It lives in the COMPONENTS layer for a mechanical reason, not a stylistic
   one: _AuthLayout links tokens -> base -> layout -> components and NOT
   pages.css, and this surface is carried by BOTH shells (the app shell for a
   signed-in 4xx, the auth card for everything else). A rule in the page layer
   would silently not load on half of its uses.

   Everything here is composed from pieces that already exist — the eyebrow
   treatment, the copyable inset, .help, .page-links — so it introduces no new
   token and needs no dark-theme block of its own. */
.error-page {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

/* "Error 404" NAMES the failure; the h1 titles it in words. Eyebrow, so it
   ranks below the heading it sits above. */
.error-page__label {
    font-family: var(--font-display);
    font-size: var(--text-xs);
    font-weight: var(--weight-bold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
}

.error-page__lead {
    color: var(--text-muted);
    line-height: var(--leading-relaxed);
    max-width: var(--measure);
}

/* The reference id: a read-only value the reader is meant to copy or read out
   over the phone, so it is an inset rather than a disabled field. */
.error-page__ref {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
}

.error-page__ref-label {
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    color: var(--text-muted);
}

.error-page__ref-value {
    /* The block above IS the inset; base.css already gives every <code> its own
       background and padding, which would otherwise nest one inset in another. */
    padding: 0;
    background: none;
    color: var(--text);
    font-size: var(--text-sm);
    overflow-wrap: anywhere;
    /* One click takes the whole id — it is 32 characters of hex that nobody
       should have to drag-select accurately. */
    user-select: all;
}

/* Development only: the block that stands in for the developer exception page
   this unit deliberately does not wire. It never renders on a deployed host. */
.error-page__detail {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.error-page__trace {
    margin: 0;
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
    color: var(--text);
    font-size: var(--text-xs);
    line-height: var(--leading-relaxed);
    /* A scroller is a tab stop and a named region in this system (the pre
       carries tabindex + role in the markup), so the overflow is safe to keep. */
    max-height: 24rem;
    overflow: auto;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* ================================================================ SETTINGS
   The portal's settings page TEMPLATE (#630), matched to the approved prototype
   (docs/design/630-config-redesign-prototype.html): a sub-nav column that
   switches panes (configuration.js), and rows pairing a bold label with a
   control and its help. Rows stack FLUSH into one card per group — no dividers,
   the card border is the only edge. First used by Configuration and meant for
   the next settings surface, so it is a component. Every colour is an already
   theme-aware token; no per-theme block to keep in step. */

/* Settings pages (Assistant, Account) keep the LEFT rail, not the shell's top strip: their
   content is control-sparse, so the rail is a counterweight that balances a comfortable form
   column instead of stranding it in a full-width void (#613 follow-up). The shared pill styling
   ties it to the content sections' top strip; the orientation differs because the density does. */
.settings {
    display: grid;
    grid-template-columns: 13rem minmax(0, 1fr);
    gap: var(--space-8);
    align-items: start;
}

/* The body column beside a routed left rail (a manage section like Members). Unlike
   .settings-panes it is NOT width-capped — its tables want the room the 1fr grid gives. */
.subnav-content {
    min-width: 0;
}

/* The rail sticks under the shell topbar; --z-sticky is BELOW --z-topbar (the
   DESIGN sticky-layer rule). */
.settings-nav {
    position: sticky;
    /* Pin the rail with zero travel: its resting top is the topbar plus .portal-main's own
       top padding, so the sticky offset has to match, or the rail slides up that padding's
       worth on the first scroll before it locks. Keep this in step with .portal-main's
       padding. */
    top: calc(var(--topbar-height) + var(--space-4));
    z-index: var(--z-sticky);
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding-top: var(--space-2);
    /* Cap the rail to the space below its pin and scroll its own overflow, so a rail with
       more items than fit the viewport reaches all of them without dragging the panes along.
       .content-wrapper stays the page scroller. */
    max-height: calc(100vh - var(--topbar-height) - var(--space-4));
    overflow-y: auto;
}

.settings-nav__link {
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-md);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    color: var(--text-muted);
    text-decoration: none;
    transition: background-color var(--motion-fast) var(--ease-out),
                color var(--motion-fast) var(--ease-out);
}

.settings-nav__link:hover {
    background: var(--surface-2);
    color: var(--text);
    text-decoration: none;
}

.settings-nav__link.active {
    background: var(--accent-tint);
    color: var(--accent-on-tint);
}

/* The pane column is width-capped for readability, like the prototype's content. */
.settings-panes {
    max-width: 48rem;
}

.settings-panes > .settings-pane {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
    scroll-margin-top: calc(var(--topbar-height) + var(--space-4));
}

.settings-pane[hidden] {
    display: none;
}

.settings-pane:focus {
    outline: none;
}

@media (max-width: 760px) {
    .settings {
        grid-template-columns: 1fr;
        gap: var(--space-4);
    }

    .settings-nav {
        position: static;
        flex-direction: row;
        flex-wrap: wrap;
        padding-top: 0;
        /* Not a pinned rail here — a wrapping row above the panes, so the viewport cap
           and its scrollbar would only clip the wrapped links. */
        max-height: none;
        overflow-y: visible;
    }

    .settings-panes {
        max-width: none;
    }
}

/* One card per group: rows stack flush inside it, separated by their own padding
   rather than dividers (#630). */
.settings-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
}

/* A row: bold label left, control right, help demoted under the control. */
.setting-row {
    display: grid;
    grid-template-columns: minmax(0, 30%) minmax(0, 1fr);
    gap: var(--space-6);
    align-items: start;
    padding: var(--space-4) var(--space-6);
}

.setting-row__label {
    padding-top: 9px;   /* onto the control's baseline */
}

.setting-row__label label,
.setting-row__label .field-label {
    display: block;
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    color: var(--text);
}

.setting-row__control {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-width: 0;
}

/* Inputs capped so a lone field is not a full-width bar; textareas keep the room. */
.setting-row__control input[type="text"],
.setting-row__control input[type="url"] {
    max-width: 26rem;
}

.setting-row__control select {
    max-width: 18rem;
}

/* A full-width row for card content that is not a label/control pair — an intro, a
   notice, a validation summary, a comparison table, a form's actions. Same padding as a
   row so it lines up, but one column (no label gutter). First needed by the Account page
   (#634), whose panes mix plain forms with these full-width blocks. */
.setting-row--full {
    grid-template-columns: 1fr;
}

@media (max-width: 640px) {
    .setting-row {
        grid-template-columns: 1fr;
        gap: var(--space-3);
    }

    .setting-row__label {
        padding-top: 0;
    }
}

/* An inline action cluster inside a control (the prompt row's Regenerate + status). */
.setting-row__inline {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
}

/* A control and its save affordance. For a text field the Save/Cancel shrink to a check/× icon
   pair that appears only while the field is dirty (the class set by portal-settings.js). The pair
   sits ABOVE the control, flush with its trailing edge and outside its box, so the control never
   reserves room for it: a long value is never clipped behind a gutter and never shifts when the
   icons come. Every shape places it the same way; a select has no icons (it saves on change) and
   only hosts the flash. The "Saved" flash lands in that same spot; an error renders in normal flow
   below the control, where a longer message has room to wrap. The spot overlaps the gap between
   rows, so a long hint on the row above may sit under it while it shows. */
.field-commit {
    position: relative;
}

/* The wrapper takes the control's width, so the slot meets the control's right edge rather than
   the column's. Inputs are capped so a lone field is not a full-width bar; a select is narrower
   still; a textarea keeps the column. */
.field-commit--single {
    width: 26rem;
    max-width: 100%;
}

.field-commit--select {
    width: 18rem;
    max-width: 100%;
}

.field-commit > input,
.field-commit > select,
.field-commit > textarea {
    width: 100%;
}

.field-commit__actions {
    display: none;
    position: absolute;
    right: 0;
    bottom: 100%;
    margin-bottom: 5px;   /* clear of the control's focus ring (2px, offset 2px) */
    align-items: center;
    gap: 4px;
}

.setting-row__control.is-dirty .field-commit__actions {
    display: flex;
}

/* A field locked while the server replaces its value: no Save or Cancel, even when dirty. */
.setting-row__control.is-busy .field-commit__actions {
    display: none;
}

.field-commit .setting-row__status[data-state="saved"] {
    position: absolute;
    right: 0;
    bottom: 100%;
    margin-bottom: 7px;
    pointer-events: none;
}

/* The browser-loaded preview of a shop-hosted image, under its URL field. A thumbnail, so a wide
   welcome image does not take over the row; shrinks to the image's own width and stays hidden
   until the image has loaded (the author display below would otherwise beat the UA's [hidden]). */
.image-preview {
    display: block;
    align-self: flex-start;
    max-width: 26rem;
    max-height: 5rem;
    padding: var(--space-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    object-fit: contain;
}

.image-preview[hidden] {
    display: none;
}

/* A list field: single-line rows editing one hidden text control (the starter questions). Rows
   stack like the textarea they replace and cap at the single-line width; the footer holds the add
   action beside the commit icons, which sit in flow here rather than overlaying a field edge, since
   there is no single field to overlay. The "Saved" flash lands in flow beside them for the same
   reason. */
.list-field {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.list-field__rows {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.list-field__row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    max-width: 26rem;
}

.list-field__row input[type="text"] {
    flex: 1 1 auto;
    min-width: 0;
}

.list-field__footer {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
}

.list-field__footer .field-commit__actions,
.list-field__footer .setting-row__status[data-state="saved"] {
    position: static;
    transform: none;
}

/* The hidden joined control. Paired explicitly: the shared textarea rule above carries author
   declarations, and an author `display` added there would otherwise beat the UA's [hidden]. */
.list-field > textarea[hidden] {
    display: none;
}

/* Compact ghost icon buttons: a check (save) and an × (cancel). Icon-only, so each carries an
   aria-label. */
.field-commit__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    box-shadow: var(--shadow-raised);
    color: var(--text-muted);
    cursor: pointer;
    transition: color var(--motion-base) var(--ease-out),
                background var(--motion-base) var(--ease-out),
                border-color var(--motion-base) var(--ease-out);
}

.field-commit__btn svg {
    width: 16px;
    height: 16px;
}

.field-commit__btn:hover {
    background: var(--surface);
}

/* The check reads save (a green tint at rest); the × reads discard on hover (red). */
.field-commit__btn--save {
    color: var(--success-text);
}

.field-commit__btn--save:hover {
    border-color: var(--success-text);
}

.field-commit__btn--cancel:hover,
.field-commit__btn--remove:hover {
    color: var(--danger-text);
    border-color: var(--danger-text);
}

.field-commit__btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

/* Inline save feedback: a checkmark + "Saved" that flashes in and fades (success),
   or a persistent message (error). */
.setting-row__status {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-relaxed);
}

.setting-row__status:empty {
    display: none;
}

.setting-row__status svg {
    width: 13px;
    height: 13px;
}

.setting-row__status[data-state="saved"] {
    color: var(--success-text);
    opacity: 0;
    transform: translateY(2px);
    transition: opacity var(--motion-base) var(--ease-out),
                transform var(--motion-base) var(--ease-out);
}

.setting-row__status[data-state="saved"].is-shown {
    opacity: 1;
    transform: none;
}

.setting-row__status[data-state="error"] {
    color: var(--danger-text);
    font-weight: var(--weight-medium);
}

/* In-progress state for a discrete action (e.g. deriving the theme from the storefront):
   a persistent muted line shown while the request is in flight. */
.setting-row__status[data-state="working"] {
    color: var(--text-muted);
    font-weight: var(--weight-medium);
}

/* Accent input beside its preset swatches. */
.accent-control {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
}

/* A read-only value the owner reads or copies — an inset, not a disabled input. */
.readonly-inset {
    margin: 0;
    padding: 9px 11px;
    max-width: 26rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    color: var(--text);
    font-size: var(--text-sm);
    word-break: break-all;
}

/* A compact "coming soon" tag inline in a hint. */
.tag-soon {
    display: inline-block;
    margin-right: var(--space-2);
    padding: 0 var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
}

/* The Regenerate confirm: replaces the button in the same inline cluster until the owner picks.
   Both carry an author display, so [hidden] needs its own pairing. */
.regen-confirm {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.regen-confirm[hidden],
.setting-row__inline > .btn[hidden] {
    display: none;
}

.regen-confirm__lead {
    font-size: var(--text-sm);
    color: var(--text);
}

/* The advisor-prompt live-status line (#398): in flow, never a chip. Empty at
   rest; accent while a regeneration is in flight. */
.advisor-status {
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.advisor-status.is-working {
    color: var(--accent-text);
}

/* ============================================================ LAYOUT PREVIEW
   A decorative mockup (#609) under the product-layout select: it shows how the
   two layouts arrange products, with no catalog data. A static picture, so it
   carries no motion. The panes are aria-hidden in the markup; only the one
   matching the wrapper's data-layout-preview value is shown, and configuration.js
   swaps that attribute on change. */
.layout-preview {
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
}

.layout-preview__label {
    display: block;
    margin-bottom: var(--space-3);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    color: var(--text-muted);
}

.layout-preview__pane {
    display: none;
}

.layout-preview[data-layout-preview="carousel"] .layout-preview__pane--carousel,
.layout-preview[data-layout-preview="list"] .layout-preview__pane--list {
    display: flex;
}

/* The shared image + skeleton placeholders read as "content goes here". */
.layout-preview__img {
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text-muted);
    opacity: 0.7;
}

.layout-preview__price {
    font-size: var(--text-sm);
    font-weight: var(--weight-bold);
    color: var(--text);
}

/* ---- Carousel: cards in a row, nav arrows, dots ---- */
.layout-preview__pane--carousel {
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.layout-preview__nav {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border-radius: var(--radius-pill);
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text-muted);
    font-size: var(--text-lg);
    line-height: 1;
}

.layout-preview__track {
    flex: 1;
    display: flex;
    gap: var(--space-2);
    min-width: 0;
    overflow: hidden;
}

.layout-preview__card {
    flex: 0 0 auto;
    width: 92px;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
}

.layout-preview__card .layout-preview__img {
    height: 56px;
}

.layout-preview__stars {
    font-size: var(--text-micro);
    letter-spacing: 0.1em;
    color: var(--accent);
}

.layout-preview__dots {
    flex-basis: 100%;
    display: flex;
    justify-content: center;
    gap: var(--space-1);
    margin-top: var(--space-2);
}

.layout-preview__dots span {
    width: 6px;
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--border);
}

.layout-preview__dots span.is-active {
    background: var(--accent);
}

/* ---- List: stacked rows, icon + two text lines + price ---- */
.layout-preview__pane--list {
    flex-direction: column;
    gap: var(--space-2);
}

.layout-preview__row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
}

.layout-preview__img--sm {
    flex-shrink: 0;
    width: 38px;
    height: 38px;
}

.layout-preview__lines {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
}

.layout-preview__lines span {
    height: 7px;
    border-radius: var(--radius-sm);
    background: var(--border);
}

.layout-preview__lines span:first-child {
    width: 70%;
}

.layout-preview__lines span:last-child {
    width: 45%;
}

/* =================================================================== TOGGLE
   A boolean rendered as a switch with an On/Off label. The checkbox stays the
   control (keyboard + accessible name from its aria-label); it is visually hidden
   and the track is the paint. */
.togglerow {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

.togglerow .toggle__state {
    min-width: 3ch;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.toggle {
    position: relative;
    display: inline-flex;
    flex: none;
    width: 44px;
    height: 26px;
    cursor: pointer;
}

.toggle input {
    position: absolute;
    inset: 0;
    z-index: 1;   /* the transparent input sits ABOVE its painted track so a click/check hits it */
    width: 100%;
    height: 100%;
    margin: 0;
    opacity: 0;
    cursor: pointer;
}

.toggle__track {
    position: relative;
    flex: 1;
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    border: 1px solid var(--border);
    transition: background-color var(--motion-base) var(--ease-out),
                border-color var(--motion-base) var(--ease-out);
}

.toggle__track::after {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 20px;
    height: 20px;
    border-radius: var(--radius-pill);
    background: var(--surface);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
    transition: transform var(--motion-base) var(--ease-out);
}

.toggle input:checked + .toggle__track {
    background: var(--accent);
    border-color: var(--accent);
}

.toggle input:checked + .toggle__track::after {
    transform: translateX(18px);
}

.toggle input:focus-visible + .toggle__track {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
}

/* A locked toggle keeps the state it shows and takes no clicks. */
.toggle:has(input:disabled),
.toggle input:disabled {
    cursor: not-allowed;
}

.toggle input:disabled + .toggle__track {
    opacity: 0.55;
}

/* ==================================================================== BRAND
   "Sailesman" in copy — the product name set apart by weight + full ink (not the
   reserved .says lime), so it stands out even inside muted help text. */
.brand {
    font-weight: var(--weight-semibold);
    color: var(--text);
}

/* Compact + ghost button variants for inline row actions. */
.btn.btn-sm {
    padding: var(--space-1) var(--space-3);
    font-size: var(--text-xs);
}

.btn.btn-ghost {
    background: transparent;
    color: var(--text-muted);
    border-color: transparent;
}

.btn.btn-ghost:hover {
    background: var(--surface-2);
    color: var(--text);
}

/* ======================================================== ATTRIBUTE ICONS
   An attribute icon is drawn in the current text colour and never shrinks in a flex row. A
   mark is an icon and its word; an icon that stands without a word is named by hidden text
   instead (DESIGN.md, "Attribute icons"). */
.attr-icon {
    flex-shrink: 0;
    width: 16px;
    height: 16px;
}

.attr-mark {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    font-weight: var(--weight-medium);
}
