/* ============================================================================
   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);
    padding: var(--space-2) var(--space-4);
    min-height: 34px;
    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: 1;
    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.
   Each action is its own small POST form; they stack tight and left-aligned. */
.knowledge-actions {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    align-items: flex-start;
}

.knowledge-actions .knowledge-action,
.knowledge-run-state,
.knowledge-remove-blocked {
    margin: 0;
}

/* JS-free delete confirm: the <summary> is the trigger; opening the disclosure
   reveals the irreversible-delete copy and the confirm button that POSTs. Copy
   in a --danger-bg card so the destructive step reads as one. */
.knowledge-remove__trigger {
    cursor: pointer;
    color: var(--danger-text);
    font: inherit;
}

.knowledge-remove__confirm {
    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);
}

.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. */
.knowledge-type-picker {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: var(--space-3);
    margin-bottom: var(--space-6);
}

.knowledge-type-card {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-4);
    padding-right: var(--space-8);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    cursor: pointer;
    transition: border-color var(--motion-fast) var(--ease-out),
                background-color var(--motion-fast) var(--ease-out),
                box-shadow var(--motion-fast) var(--ease-out);
}

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

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

/* A real, VISIBLE radio in the card corner: the checked dot is the non-colour selection
   cue (WCAG 1.4.1), so selection never rests on the accent tint/border alone. */
.knowledge-type-radio {
    position: absolute;
    top: var(--space-4);
    right: var(--space-4);
    margin: 0;
    accent-color: var(--accent);
}

.knowledge-type-card__icon {
    flex: none;
    margin-top: 0.1em;
    color: var(--accent-text);
}

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

.knowledge-type-card__title {
    font-weight: var(--weight-medium);
    color: var(--text);
}

.knowledge-type-card__body {
    font-size: var(--text-sm);
    color: var(--text-muted);
}

/* The selected card: a light accent tint (8% — essentially white, so muted ink stays
   legal on it) plus an accent border/ring and the checked corner radio. The border + inset
   ring are FOREGROUND lines on a page surface, so they take --accent-text, not --accent
   (which is reserved for filled surfaces and is borderline against the 3:1 non-text floor in
   dark). Keyed on the card's own checked radio, so it needs no id coupling. */
.knowledge-type-card:has(.knowledge-type-radio:checked) {
    background: var(--accent-tint);
    border-color: var(--accent-text);
    box-shadow: inset 0 0 0 1px var(--accent-text);
}

/* The CSS-only reveal (#580). Default: BOTH panes render, so with no :has support the
   add area degrades to two stacked forms — functional, never a dead control. With :has,
   only the picked pane shows; the selected card (above) is the pane's heading, which is
   why the panes carry no repeated title of their own. A hairline keeps picker and form
   distinct. */
.knowledge-add-pane {
    padding-top: var(--space-5);
    border-top: 1px solid var(--border);
}

.knowledge-add:has(#knowledge-type-website:checked) .knowledge-add-pane[data-knowledge-pane="upload"],
.knowledge-add:has(#knowledge-type-upload:checked) .knowledge-add-pane[data-knowledge-pane="website"] {
    display: none;
}

/* 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-6) 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 {
    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-md);
    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 drill-down (#580): the back link + the read-pages list. Each page is a link
   (websites) or plain text (upload refs) over its muted last-read time. */
.knowledge-back {
    margin: 0;
    font-size: var(--text-sm);
}

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

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

.knowledge-read-page {
    display: flex;
    flex-direction: column;
    gap: var(--space-05);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
}

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

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

/* 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: 30px;
    height: 30px;
    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. Same arrangement as .shop-preview__announce in base.css. */
.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/clear padding and join radius, in
   pages.css); only the control look is shared. */
.catalog-search__input,
.chat-filter-bar input[type="date"] {
    width: 100%;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
    font-size: var(--text-sm);
    transition: border-color var(--motion-fast) var(--ease-out);
}

.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,
.catalog-search__input:hover,
.chat-filter-bar input[type="date"]:hover {
    border-color: var(--text-muted);
}

.field textarea,
.form-grid textarea,
.setting-row__control textarea {
    resize: vertical;
    min-height: 5rem;
    line-height: var(--leading-normal);
}

.field input[type="color"],
.setting-row__control input[type="color"] {
    width: 64px;
    height: 34px;
    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: 28px;
    height: 28px;
    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;
}

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

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

.pager__btn,
.pager__page {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 32px;
    height: 32px;
    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 {
    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. */
.pager__btn.is-disabled {
    color: var(--text-muted);
    opacity: 0.5;
}

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

/* ================================================================== 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 {
    display: grid;
    grid-template-columns: 13rem minmax(0, 1fr);
    gap: var(--space-8);
    align-items: start;
}

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

/* The explicit Save/Cancel a text row reveals while edited, with an "Edited" note.
   Shown via the dirty class (configuration.js), not a bare [hidden]. */
.setting-row__commit {
    display: none;
    align-items: center;
    gap: var(--space-2);
}

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

.setting-row__commit .note {
    margin-right: auto;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

/* 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 read-only generated prompt: a scrollable inset, never an editable textarea
   (DESIGN "text you select, not a dead field"). Shares the design-system control
   metrics — same font, radius, border and padding as the editable textareas — so it
   reads as a read-only sibling, not a different box; the surface-2 tint (like the
   read-only shop-address inset) is the only read-only signal, and the ink stays full
   strength because the prompt IS the content to read. */
.promptbox {
    margin: 0;
    padding: var(--space-2) var(--space-3);
    max-height: 9rem;
    overflow: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    color: var(--text);
    font-size: var(--text-sm);
    line-height: var(--leading-normal);
    white-space: pre-wrap;
}

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

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