/* =============================================================================
   WP Provisioning System — admin stylesheet
   =============================================================================

   Derived from the EnterraOutreach design system (same tokens, typography,
   buttons, inputs, cards, badges and table styling) so the builder looks and
   behaves like the rest of the suite.

   Two deliberate differences:
     1. Font URLs are relative to this file, not absolute /wp-content paths,
        because this app is standalone and does not live inside WordPress.
     2. Everything is namespaced under .wpps-* for the app shell, so no class
        used by a generated site can collide with the builder UI.

   TABLE OF CONTENTS
     1. Fonts
     2. Design tokens
     3. Base / reset
     4. Typography
     5. Layout utilities
     6. Components (buttons, inputs, cards, badges, tables, alerts)
     7. App shell (sidebar, topbar, content)
     8. Screen-specific (auth, install, settings, wizard)
     9. Responsive
   ============================================================================= */

/* =============================================================================
   1. FONTS
   ============================================================================= */

/* Material Symbols, subsetted to only the icons this app uses: 32 glyphs at
   ~23 KB rather than the full 3.8 MB set. The subset has no ligature tables,
   so icons are emitted by codepoint through WPPS\Support\Icon rather than by
   writing their name as text. To add an icon, see tools/build-icons.md. */
@font-face {
    font-family: 'Material Symbols Outlined';
    font-style: normal;
    font-weight: 400;
    /* The ?v= value is the file's modification time. Asset URLs are served with
       a one-week max-age, so without it a browser that had already loaded the
       page would keep using the previous font for a week. Bump this whenever
       the font is regenerated (tools/build-icons.md). */
    /* No query string on the font. The stylesheet is a static file and cannot
       hash its own contents, so a version here is a number somebody has to
       remember to change — and this one was not changed when the font was
       rebuilt with two more icons, so browsers kept the old subset and rendered
       those icons as empty boxes.
       The server sends an ETag and a Last-Modified for the font, so without a
       query string the browser revalidates and gets a 304 unless the file has
       actually changed. app.css and app.js carry content hashes because they are
       referenced from PHP, which can compute one; a font referenced from CSS
       cannot. */
    src: url('./fonts/material-symbols.woff2') format('woff2');
    font-display: block;
}

@font-face {
    font-family: 'Plus Jakarta Sans';
    font-style: normal;
    font-weight: 400;
    src: url('./fonts/plus-jakarta-sans/PlusJakartaText-Regular.woff2') format('woff2');
    font-display: swap;
}

@font-face {
    font-family: 'Plus Jakarta Sans';
    font-style: normal;
    font-weight: 300;
    src: url('./fonts/plus-jakarta-sans/PlusJakartaDisplay-Light.woff2') format('woff2');
    font-display: swap;
}

@font-face {
    font-family: 'Plus Jakarta Sans';
    font-style: normal;
    font-weight: 500;
    src: url('./fonts/plus-jakarta-sans/PlusJakartaDisplay-Medium.woff2') format('woff2');
    font-display: swap;
}

@font-face {
    font-family: 'Plus Jakarta Sans';
    font-style: normal;
    font-weight: 700;
    src: url('./fonts/plus-jakarta-sans/PlusJakartaDisplay-Bold.woff2') format('woff2');
    font-display: swap;
}

@font-face {
    font-family: 'Plus Jakarta Sans';
    font-style: normal;
    font-weight: 800;
    src: url('./fonts/plus-jakarta-sans/PlusJakartaDisplay-Bold.woff2') format('woff2');
    font-display: swap;
}

@font-face {
    font-family: 'Plus Jakarta Sans';
    font-style: italic;
    font-weight: 400;
    src: url('./fonts/plus-jakarta-sans/PlusJakartaText-Italic.woff2') format('woff2');
    font-display: swap;
}

/* Base icon styling, matching the conventions Google documents for Material
   Symbols. The subset font is a single weight, so no variation settings are
   applied; the codepoint renders as an outline glyph. */
.material-symbols-outlined {
    font-family: 'Material Symbols Outlined';
    font-style: normal;
    font-weight: 400;
    display: inline-block;
    vertical-align: middle;
    font-size: 1.25rem;
    line-height: 1;
    letter-spacing: normal;
    text-transform: none;
    white-space: nowrap;
    word-wrap: normal;
    direction: ltr;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* Icons are decorative: every one of them sits beside a text label, so
       they are hidden from assistive technology at the markup level. */
    flex-shrink: 0;
}

/* =============================================================================
   2. DESIGN TOKENS
   ============================================================================= */

:root {
    /* Primary blue/ocean scale */
    --color-primary: #005f73;
    --color-primary-dark: #0a3946;
    /* Ink for text on a tinted panel — code, pills, the secondary button's hover
       state. It is the dark teal in light mode, where the tint under it is pale.
       In dark mode the tint is a dark wash of the same hue and dark ink on it is
       invisible, so this token exists rather than each rule reaching for
       --color-primary-dark directly. That is exactly what went wrong: three rules
       named the dark teal and none of them changed when the surfaces did, so code
       inside a paragraph rendered dark-on-dark. */
    --color-on-primary-dim: #0a3946;
    --color-primary-darker: #05262e;
    --color-primary-light: #94d2bd;
    --color-primary-dim: rgba(0, 95, 115, 0.1);
    --color-primary-dim-20: rgba(0, 95, 115, 0.2);

    /* Neutral surfaces */
    --color-white: #ffffff;
    --color-surface: #f4f7f8;
    --color-surface-elevated: #ffffff;
    --color-surface-variant: #cbd5e0;
    /* Surfaces that were fixed gradients.
       Each of these was a white-to-something gradient written for a light page,
       so in dark mode they stayed white while the text on them turned near-white:
       a secondary button with an invisible label, a danger button, and the
       attention card. They are tokens now, so both halves follow the mode. */
    --surface-raised-top: #ffffff;
    --surface-raised-bottom: #f4f1ef;
    --surface-danger-top: #ffffff;
    --surface-danger-bottom: #fff5f5;
    --surface-attention-top: #ffffff;
    --surface-attention-bottom: #fffdf5;
    --surface-bare-bottom: #eaf2f4;
    --color-on-surface: #1a202c;
    --color-on-surface-variant: #4a5568;
    --color-on-surface-inverse: #a0aec0;

    /* Supporting colours */
    --color-secondary: #e9ecef;
    --color-secondary-container: #a3c3d3;
    --color-on-secondary: #2d3748;

    --color-error: #c53030;
    --color-error-container: #fecaca;
    --color-warning: #b7791f;
    --color-warning-container: #fef3c7;
    --color-success: #276749;
    --color-success-container: #c6f6d5;

    /* Effects */
    --color-border-glow: rgba(0, 95, 115, 0.15);
    /* The translucent surface behind the sticky top bar and the save bar, where
       a blur sits underneath. Translucent rather than opaque because the blur is
       the point: content scrolls behind and stays faintly visible. Defined as a
       token because a hardcoded white here is invisible to dark mode, which is
       exactly how the top bar stayed light grey on a dark page. */
    --color-surface-glass: rgba(255, 255, 255, 0.8);
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
    --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
    --shadow-glow: 0 10px 30px -10px rgba(0, 95, 115, 0.2);

    /* Typography */
    --font-headline: 'Plus Jakarta Sans', system-ui, sans-serif;
    --font-body: system-ui, -apple-system, 'Segoe UI', sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

    /* Spacing — a strict 4px scale.
       Every step is a multiple of 4px so the vertical rhythm is predictable and
       no two steps sit awkwardly close together. The upper steps were a full
       8px scale (24/32/40/48) and the app read as too airy as a result: card
       padding, stack gaps and empty states all stacked up. They were tightened
       to this 4px rhythm, which is what actually makes the layout denser.
       A fuller step is still available when something genuinely needs air. */
    --space-1: 0.25rem;   /*  4px */
    --space-2: 0.5rem;    /*  8px */
    --space-3: 0.75rem;   /* 12px */
    --space-4: 1rem;      /* 16px */
    --space-5: 1.25rem;   /* 20px */
    --space-6: 1.5rem;    /* 24px */
    --space-8: 1.75rem;   /* 28px — used for padding rather than gaps */
    --space-10: 2rem;     /* 32px */
    --space-12: 2rem;     /* 32px */
    --space-16: 2.5rem;   /* 40px */
    --space-20: 3rem;     /* 48px */

    /* Radii */
    --radius-sm: 0.5rem;
    --radius-md: 1rem;
    --radius-lg: 1.5rem;
    --radius-l: 2.5rem;
    --radius-xl: 3rem;
    --radius-full: 9999px;

    /* Shell metrics */
    --sidebar-width: 16.5rem;
    --topbar-height: 4.5rem;
}

/* -----------------------------------------------------------------------------
   Dark mode.
   
   The whole interface is built from the custom properties above, so dark mode is
   a second set of the same names rather than a parallel stylesheet. Two rules
   make it work:
   
     - Only the surface and ink tokens change. Brand colours, spacing, radii and
       typography are the same in both modes, so a component written against the
       tokens does not need to know which mode it is in.
   
     - --color-white is NOT inverted. It reads like a surface but it is also the
       text colour on the sidebar and the knob on a switch, both of which are
       white in either mode. Inverting it would make the navigation invisible.
       Surfaces that should follow the mode use --color-surface-elevated.
   
   Applied by a data attribute on <html>, set before paint by a small inline
   script in the layout, so the page never flashes light before turning dark.
   -------------------------------------------------------------------------- */
[data-mode='dark'] {
    /* Surfaces, from the page upwards. Not pure black: a page that is #000 makes
       borders and shadows invisible, and every real dark interface sits slightly
       above black so there is somewhere for depth to go. */
    --color-surface: #14181d;
    --color-surface-elevated: #1c2229;
    --color-surface-variant: #2a323b;

    /* Ink, from body text to the faintest label, all checked against the surfaces
       above for at least 4.5:1 — the same bar the generated sites are held to. */
    --color-on-surface: #f1f4f7;
    --color-on-surface-variant: #b6c0cb;
    --color-on-surface-inverse: #8794a3;

    /* The neutral container used for chips, badges and hover states. It is a
       light grey in light mode and has to become a dark one. */
    --color-secondary: #262d35;
    --color-on-secondary: #e7ecf1;

    /* Borders and glows are built from the primary colour at low opacity, which
       is nearly invisible on a dark surface, so they get more of it. */
    --color-border-glow: rgba(148, 210, 189, 0.22);
    --color-primary-dim: rgba(148, 210, 189, 0.12);
    --color-primary-dim-20: rgba(148, 210, 189, 0.22);

    /* The primary is dark by design — it is a deep teal that reads as a link on
       white and disappears on a dark surface. The lighter end of the scale takes
       over as the accent in dark mode; the dark end stays for the sidebar. */
    --color-primary: #6cc4b0;

    /* Status colours, brightened for the same reason: the light-mode values are
       chosen to be readable on white, which makes them too dark here. */
    --color-error: #fc8181;
    --color-error-container: #4a2020;
    --color-warning: #f6c177;
    --color-warning-container: #443416;
    --color-success: #7fd1a8;
    --color-success-container: #1e3a2c;

    /* Shadows do less work on a dark surface, where depth comes from the surface
       steps above, so they are deepened rather than removed. */
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
    --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.4);
    --shadow-glow: 0 10px 30px -10px rgba(0, 0, 0, 0.6);

    /* A pale tint of the accent, because the surface under it is dark. */
    --color-on-primary-dim: #a9e0d2;

    /* The same glass, on a dark page. Slightly more opaque than the light one,
       because a translucent dark panel over dark content reads as a smudge
       rather than as a surface. */
    --color-surface-glass: rgba(20, 24, 29, 0.85);

    /* A step above --color-surface-elevated, so a raised button reads as raised
       on a dark page the same way a white one does on a light page. */
    --surface-raised-top: #262d35;
    --surface-raised-bottom: #1f262d;

    /* The tinted surfaces keep a trace of their meaning — red for danger, amber
       for attention — instead of becoming the same grey. */
    --surface-danger-top: #34232a;
    --surface-danger-bottom: #2a1d22;
    --surface-attention-top: #332c1c;
    --surface-attention-bottom: #2a2418;

    --surface-bare-bottom: #101418;
}

/* The browser scrollbar and form controls follow the mode too, so a dark page
   does not have a white scrollbar down its edge. */
[data-mode='dark'] {
    color-scheme: dark;
}

/* =============================================================================
   3. BASE / RESET
   ============================================================================= */

::selection {
    background-color: color-mix(in srgb, var(--color-primary) 40%, transparent);
    color: var(--color-on-surface);
}

*,
*::before,
*::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

html {
    scroll-behavior: smooth;
}

body {
    font-family: var(--font-body);
    font-size: 1rem;
    line-height: 1.5;
    color: var(--color-on-surface);
    background-color: var(--color-surface);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

img,
picture,
video,
canvas,
svg {
    display: block;
    max-width: 100%;
    height: auto;
}

a {
    color: inherit;
    text-decoration: none;
    transition: color 150ms ease;
}

button {
    font: inherit;
    background: none;
    border: none;
    cursor: pointer;
}

input,
select,
textarea {
    font: inherit;
    color: inherit;
}

ul,
ol {
    margin: 0;
    list-style: none;
}

:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}

input:focus-visible,
button:focus-visible,
select:focus-visible {
    outline: none;
}

/* =============================================================================
   4. TYPOGRAPHY
   ============================================================================= */

.font-headline { font-family: var(--font-headline); }
.font-bold { font-weight: 700; }
.font-extrabold { font-weight: 800; }

.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.text-base { font-size: 1rem; line-height: 1.5rem; }
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
.text-8xl { font-size: 6rem; line-height: 1; }

.tracking-tighter { letter-spacing: -0.05em; }
.uppercase { text-transform: uppercase; }
.text-a-center { text-align: center; }
.text-primary { color: var(--color-primary); }
.text-on-surface { color: var(--color-on-surface); }
.text-white { color: var(--color-white); }

h1, h2, h3, h4 {
    color: var(--color-on-surface);
    font-weight: 800;
}

p, li {
    color: var(--color-on-surface-variant);
}

code {
    font-family: var(--font-mono);
    font-size: 0.85em;
    background: var(--color-primary-dim);
    color: var(--color-on-primary-dim);
    padding: 0.1em 0.4em;
    border-radius: var(--radius-sm);
}

.wpps-muted {
    color: var(--color-on-surface-variant);
    font-size: 0.875rem;
}

/* =============================================================================
   5. LAYOUT UTILITIES
   ============================================================================= */

.block { display: block; }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.flex-center { display: flex; justify-content: center; }
.flex-right { display: flex; justify-content: flex-end; }
.flex-sb { display: flex; justify-content: space-between; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.grid { display: grid; }
/* minmax(0, 1fr), not 1fr: a grid track defaults to min-width:auto, so a long
   unbreakable word in one cell widens that column and can overflow the card.
   .wpps-grid-2 already did this; these generic helpers did not. */
.gc-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.gc-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

.gap-2 { gap: var(--space-2); }
.gap-4 { gap: var(--space-4); }
.gap-6 { gap: var(--space-6); }
.gap-8 { gap: var(--space-8); }

.w-full { width: 100%; }
.w-fit { width: fit-content; }
.m-auto { margin: auto; }
.m-0 { margin: 0; }
.mt-2 { margin-top: var(--space-2); }
.mt-4 { margin-top: var(--space-4); }
.mt-6 { margin-top: var(--space-6); }
.mt-8 { margin-top: var(--space-8); }
.mb-4 { margin-bottom: var(--space-4); }
.my-8 { margin-top: var(--space-8); margin-bottom: var(--space-8); }
.relative { position: relative; }
.hidden { display: none; }
.clickable { cursor: pointer; }

.blur-decal {
    position: absolute;
    border-radius: 50%;
    background: var(--color-primary-dim-20);
    filter: blur(100px);
    pointer-events: none;
    z-index: 0;
}

.blur-decal-md { width: 20rem; height: 20rem; }
.blur-decal-lg { width: 24rem; height: 24rem; opacity: 80%; }

/* =============================================================================
   6. COMPONENTS
   ============================================================================= */

/* --- 6.1 Buttons --- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-6);
    font-weight: 600;
    font-size: 0.875rem;
    border-radius: var(--radius-full);
    transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 150ms ease, background-color 150ms ease;
    cursor: pointer;
    user-select: none;
    border: 1px solid transparent;
    white-space: nowrap;
}

.btn-primary {
    background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
    color: #ffffff;
    box-shadow: var(--shadow-glow);
    border-color: var(--color-border-glow);
}

.btn-primary:hover {
    transform: scale(1.03);
    color: #ffffff;
}

.btn-primary:active {
    transform: scale(0.98);
    opacity: 0.9;
}

.btn-secondary {
    background: linear-gradient(135deg, var(--surface-raised-top) 0%, var(--surface-raised-bottom) 100%);
    color: var(--color-on-surface);
    border-color: var(--color-border-glow);
    box-shadow: var(--shadow-md);
}

.btn-secondary:hover {
    color: var(--color-on-primary-dim);
    transform: scale(1.03);
}

.btn-text {
    color: var(--color-on-surface-variant);
    padding: var(--space-1) var(--space-2);
    font-weight: 500;
}

.btn-text:hover {
    color: var(--color-primary);
    background-color: transparent;
}

.btn-sm {
    padding: var(--space-1) var(--space-4);
    font-size: 0.75rem;
}

.btn-lg {
    padding: var(--space-4) var(--space-8);
    font-size: 1rem;
    /* Matches the input height so a large button sitting under a field lines up
       with the controls above it. */
    min-height: 3.25rem;
}

.btn:disabled,
.btn[disabled] {
    opacity: 0.45;
    cursor: not-allowed;
    transform: none !important;
    box-shadow: none;
}

.btn-danger,
.wpps-btn-danger {
    color: var(--color-error);
    border-color: color-mix(in srgb, var(--color-error) 35%, transparent);
    background: linear-gradient(135deg, var(--surface-danger-top) 0%, var(--surface-danger-bottom) 100%);
}

.btn-danger:hover,
.wpps-btn-danger:hover {
    background: var(--color-error);
    color: #ffffff;
    transform: scale(1.03);
}

.icon-btn {
    padding: var(--space-1);
    background: transparent;
    border: none;
    border-radius: var(--radius-sm);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--color-on-surface-variant);
    transition: all 150ms ease;
}

.icon-btn:hover {
    background: var(--color-primary-dim);
    color: var(--color-primary);
    transform: scale(1.1);
}

.icon-sm { font-size: 1.25rem; }
.icon-lg { font-size: 2rem; }

/* --- 6.2 Inputs --- */

.input {
    width: 100%;
    padding: var(--space-3) var(--space-4);
    border: 1px solid color-mix(in srgb, var(--color-primary) 20%, transparent);
    border-radius: var(--radius-full);
    background: var(--color-surface-elevated);
    color: var(--color-on-surface);
    transition: border-color 150ms ease, box-shadow 150ms ease;
    /* A consistent control height across inputs, selects and buttons keeps
       rows aligned when a label sits beside a field. */
    min-height: 3rem;
}

.input::placeholder {
    color: var(--color-on-surface-inverse);
}

.input:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px var(--color-primary-dim);
}

.input.is-invalid {
    border-color: var(--color-error);
}

.input[readonly] {
    background: var(--color-secondary);
    color: var(--color-on-surface-variant);
}

.input-group {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: 0 var(--space-3) 0 var(--space-4);
    border: 1px solid color-mix(in srgb, var(--color-primary) 20%, transparent);
    border-radius: var(--radius-full);
    background: var(--color-surface-elevated);
    transition: border-color 150ms ease, box-shadow 150ms ease;
}

.input-group:focus-within {
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px var(--color-primary-dim);
}

/* The leading icon sits in a fixed-width cell so the text starts at the same
   x position whether the icon is present or not. */
.input-group > .material-symbols-outlined:first-child {
    width: 1.25rem;
    flex: 0 0 1.25rem;
    justify-content: center;
}

.input-group .input {
    border: none;
    background: transparent;
    padding: var(--space-3) 0;
    box-shadow: none;
    min-height: 2.75rem;
}

.input-group .input:focus {
    box-shadow: none;
}

.input-group .icon-btn {
    flex: 0 0 auto;
}

.input-group .material-symbols-outlined {
    color: var(--color-on-surface-inverse);
}

.select,
.wpps-select {
    width: 100%;
    padding: var(--space-3) var(--space-4);
    border: 1px solid color-mix(in srgb, var(--color-primary) 20%, transparent);
    border-radius: var(--radius-md);
    background: var(--color-surface-elevated);
    color: var(--color-on-surface);
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%234a5568' d='M6 8 0 0h12z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right var(--space-4) center;
    padding-right: var(--space-10);
}

.select:focus,
.wpps-select:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px var(--color-primary-dim);
}

.wpps-form {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
}

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

.wpps-label {
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--color-on-surface);
    letter-spacing: 0.01em;
}

.wpps-field__error {
    font-size: 0.8125rem;
    color: var(--color-error);
    font-weight: 500;
}

.wpps-hint {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    font-size: 0.8125rem;
    color: var(--color-on-surface-variant);
    line-height: 1.55;
}

.wpps-hint .material-symbols-outlined {
    color: var(--color-primary);
    flex-shrink: 0;
    margin-top: 0.1rem;
}

/* Guidance that follows a whole form rather than one field, e.g. the note
   about two-step sign-in. Separated by a rule so it reads as an aside. */
.wpps-hint--block {
    margin-top: var(--space-6);
    padding-top: var(--space-5);
    border-top: 1px solid color-mix(in srgb, var(--color-primary) 12%, transparent);
}

.wpps-grid-2 {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-5);
    /* Fields in one row line up. Without this a hint that wraps to two lines in
       the left column pushes its input down while the right column's input
       stays put, so "API base URL" sat visibly higher than "Request timeout"
       and the row looked broken rather than two columns of one form. Stretching
       makes both cells the height of the taller one; because .wpps-field is a
       column, the label and control still sit at the top and only the cell
       grows. */
    align-items: stretch;
}

/* The control sits directly under its label even when the cell is taller than
   its contents, so a short field does not drift to the middle of a row. */
.wpps-grid-2 > .wpps-field {
    align-self: start;
}

.wpps-code-input {
    font-family: var(--font-mono);
    font-size: 1.25rem !important;
    letter-spacing: 0.4em;
    text-align: center;
}

/* Switch */
.wpps-switch {
    display: flex;
    align-items: flex-start;
    gap: var(--space-4);
    cursor: pointer;
    padding: var(--space-4) 0;
}

.wpps-switch input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.wpps-switch__track {
    flex-shrink: 0;
    width: 2.75rem;
    height: 1.5rem;
    border-radius: var(--radius-full);
    background: var(--color-surface-variant);
    position: relative;
    transition: background 200ms ease;
    margin-top: 0.15rem;
}

.wpps-switch__track::after {
    content: '';
    position: absolute;
    top: 0.1875rem;
    left: 0.1875rem;
    width: 1.125rem;
    height: 1.125rem;
    border-radius: 50%;
    background: var(--color-white);
    box-shadow: var(--shadow-sm);
    transition: transform 200ms ease;
}

.wpps-switch input:checked + .wpps-switch__track {
    background: var(--color-primary);
}

.wpps-switch input:checked + .wpps-switch__track::after {
    transform: translateX(1.25rem);
}

.wpps-switch input:focus-visible + .wpps-switch__track {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}

.wpps-switch__text {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.wpps-switch__text strong {
    font-size: 0.9375rem;
    font-weight: 600;
    color: var(--color-on-surface);
}

/* --- 6.3 Cards --- */

.card {
    background: var(--color-surface-elevated);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    padding: var(--space-6);
}

.card-glass {
    background: var(--color-surface-glass);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border: 1px solid var(--color-border-glow);
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-lg);
}

.wpps-card {
    border: 1px solid color-mix(in srgb, var(--color-primary) 8%, transparent);
}

/* Page content is full width everywhere. These two classes used to cap a page
   at 46rem or 64rem, which left a large empty area to the right on any wide
   window — the card stopped partway across while the rest of the viewport sat
   blank. Both names are kept so the templates that ask for them keep working;
   they simply no longer narrow anything.

   The remaining caps in this file are deliberate and are NOT page widths:
   .wpps-card--auth keeps a sign-in box a readable size, .wpps-error-card does
   the same for an error, and .wpps-actions__input bounds a single inline field. */
.wpps-card--wide {
    width: 100%;
}

.wpps-card--auth {
    max-width: 27rem;
    width: 100%;
    /* The sign-in card is the first thing anyone sees, and the base .card
       padding of 1.5rem reads as cramped at this width. Generous padding is
       what makes the form breathe. */
    padding: var(--space-8);
}

.wpps-card--attention {
    border-color: color-mix(in srgb, var(--color-warning) 30%, transparent);
    background: linear-gradient(135deg, var(--surface-attention-top) 0%, var(--surface-attention-bottom) 100%);
}

.wpps-card--danger {
    border-color: color-mix(in srgb, var(--color-error) 25%, transparent);
}

.wpps-card__head {
    margin-bottom: var(--space-5);
}

.wpps-card__head--row {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
}

/* The text block yields space before the badge does, and stops growing once its
   content fits. Without min-width:0 it could not shrink, so a long subtitle
   pushed the badge onto the wrapped line and the two collided. Without
   flex-grow:0 it expanded to the full row width, which pinned the badge to the
   far right edge and left the header looking like one long line of text running
   into the pill. */
.wpps-card__head--row > :first-child {
    flex: 0 1 auto;
    min-width: 0;
}

.wpps-card__head--row > :first-child > .wpps-card__title,
.wpps-card__head--row > :first-child > .wpps-muted {
    overflow-wrap: anywhere;
}

.wpps-card__head--row > .badge,
.wpps-card__head--row > a,
.wpps-card__head--row > .btn,
.wpps-card__head--row > span:not(:first-child) {
    flex: 0 0 auto;
    align-self: flex-start;
}

.wpps-card__title {
    font-family: var(--font-headline);
    font-size: 1.25rem;
    font-weight: 800;
    color: var(--color-on-surface);
    letter-spacing: -0.02em;
}

/* Subtitle immediately under a card title. The slightly larger top margin is
   deliberate: a title and its subtitle are one unit, and the gap after them
   should be visibly larger than the gap between them. */
.wpps-card__head .wpps-muted {
    margin-top: var(--space-2);
    line-height: 1.55;
}

.wpps-card__head--row .wpps-muted {
    margin-top: var(--space-1);
}

/* --- 6.4 Badges --- */

.badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-3);
    background: var(--color-primary-dim);
    color: var(--color-primary);
    border-radius: var(--radius-full);
    font-size: 0.65rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    white-space: nowrap;
}

.badge--muted {
    background: var(--color-secondary);
    color: var(--color-on-surface-variant);
}

.badge--warn {
    background: var(--color-warning-container);
    color: var(--color-warning);
}

.badge--ok {
    background: var(--color-success-container);
    color: var(--color-success);
}

/* --- Contrast audit ---
   A small table of every pairing the built stylesheet relies on, with the ratio
   it actually achieves. The swatches are the two colours in the pairing, so a
   failure can be seen rather than only read. */
.wpps-contrast {
    /* The table sits inside a summary cell, which is one grid column wide. It is
       allowed to be narrower than its natural width, and the swatch and ratio
       columns keep their size while the pairing name takes what is left. */
    table-layout: fixed;
    width: 100%;
    margin-top: var(--space-2);
    border-collapse: collapse;
    font-size: 0.875rem;
}

.wpps-contrast th {
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--color-border-glow);
    color: var(--color-on-surface-inverse);
    font-size: 0.7rem;
    font-weight: 600;
    text-align: left;
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

.wpps-contrast td {
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid color-mix(in srgb, var(--color-border-glow) 50%, transparent);
    vertical-align: middle;
}

.wpps-contrast tr:last-child td {
    border-bottom: none;
}

.wpps-contrast__swatch {
    display: inline-flex;
    margin-right: var(--space-2);
    border: 1px solid var(--color-border-glow);
    border-radius: var(--radius-sm);
    overflow: hidden;
    vertical-align: middle;
}

.wpps-contrast__swatch i {
    display: block;
    width: 1rem;
    height: 1rem;
}

.wpps-tag {
    display: inline-block;
    margin-left: var(--space-2);
    padding: 0 var(--space-2);
    background: var(--color-secondary);
    color: var(--color-on-surface-variant);
    border-radius: var(--radius-full);
    font-size: 0.65rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

/* --- 6.5 Alerts --- */

.wpps-alert {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-5);
    border-radius: var(--radius-md);
    font-size: 0.875rem;
    margin-bottom: var(--space-6);
    line-height: 1.5;
}

.wpps-alert .material-symbols-outlined {
    flex-shrink: 0;
    margin-top: 0.1rem;
}

.wpps-alert--success {
    background: var(--color-success-container);
    color: var(--color-success);
    border: 1px solid color-mix(in srgb, var(--color-success) 25%, transparent);
}

.wpps-alert--error {
    background: var(--color-error-container);
    color: var(--color-error);
    border: 1px solid color-mix(in srgb, var(--color-error) 25%, transparent);
}

.wpps-alert__list {
    margin-top: var(--space-2);
    list-style: disc;
    padding-left: 1.1rem;
}

.wpps-alert__list li {
    color: inherit;
    font-size: 0.875rem;
}

.wpps-alert a {
    color: inherit;
    text-decoration: underline;
    font-weight: 600;
}

/* --- 6.6 Tables --- */

.enterra-table-container {
    position: relative;
    overflow-x: auto;
}

.enterra-table {
    border-collapse: collapse;
    width: 100%;
    border: none;
}

.enterra-table th,
.enterra-table td {
    white-space: nowrap;
    padding: 0.5rem 0.75rem;
}

.enterra-table thead th {
    font-weight: 600;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-on-surface-variant);
    border-bottom: 2px solid #979797;
    text-align: left;
}

.enterra-table tbody td {
    border-bottom: 1px solid #f1f5f9;
    font-size: 0.875rem;
    color: var(--color-on-surface-variant);
}

.enterra-table tbody tr:last-child td {
    border-bottom: none;
}

.enterra-table tbody tr:hover td {
    background: color-mix(in srgb, var(--color-primary) 3%, transparent);
}

.wpps-table__strong {
    display: block;
    font-weight: 600;
    color: var(--color-on-surface);
}

.wpps-table__sub {
    display: block;
    font-size: 0.8125rem;
    color: var(--color-on-surface-inverse);
    font-family: var(--font-mono);
}

.wpps-table__actions {
    text-align: right;
}

.wpps-table__actions .btn {
    display: inline-flex;
}

/* Text for screen readers only.
   Used by the icon buttons below: the visible control is an icon, and this is
   where its name lives. Not display:none, because that removes it from the
   accessibility tree as well as the page, which is the opposite of the point.

   position: absolute alone is not enough inside a flex button. The span is
   clipped to a 1px box, but its text is still 246px wide as far as scrollWidth
   is concerned, and an overflowing descendant widens the scrollable area of
   every ancestor — which is how five 36px icons made a 309px cell overflow its
   table by 19px. Taking it out of flow entirely with width/height 0 and no
   min-width stops it contributing to any ancestor's scroll width. */
.wpps-visually-hidden {
    position: absolute;
    width: 0;
    height: 0;
    min-width: 0;
    padding: 0;
    margin: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
    pointer-events: none;
}

/* Row actions shown as icons.
   A table row has four or five actions and every one of them was a labelled
   button, which wrapped onto its own line and turned the column into a list of
   words wide enough to squeeze the table. The label is still there for anyone
   who needs it — it is the accessible name and the tooltip — it is simply not
   drawn.
   Colour and shape follow .btn so these read as the same controls, just smaller. */
.wpps-iconbtn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border: none;
    border-radius: var(--radius-full);
    background: transparent;
    color: var(--color-on-surface-variant);
    cursor: pointer;
    text-decoration: none;
    transition: background 150ms ease, color 150ms ease;
}

.wpps-iconbtn:hover {
    background: var(--color-primary-dim);
    color: var(--color-primary);
}

.wpps-iconbtn:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}

.wpps-iconbtn--danger:hover {
    background: var(--color-error-container);
    color: var(--color-error);
}

/* Disabled, which happens when a site has no preview URL to open yet. */
.wpps-iconbtn[aria-disabled='true'],
.wpps-iconbtn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
    pointer-events: none;
}

/* The tooltip. Drawn with CSS so it needs no JavaScript and cannot get out of
   step with the label: the text comes from the same span the accessible name
   does. Positioned above and centred, with a small arrow. */
.wpps-iconbtn[data-tip] {
    position: relative;
}

.wpps-iconbtn[data-tip]::after {
    content: attr(data-tip);
    position: absolute;
    bottom: calc(100% + 0.5rem);
    /* Right-aligned to the button rather than centred on it.
       Any absolutely-positioned box that extends past its parent adds to that
       parent's scrollWidth, and scrollWidth is what an ancestor table uses to
       decide it is overflowing. Centring a 113px label on a 36px button put
       95px of it outside, which is where the table's 19px overflow came from.
       Right-aligning keeps the label outside the button on the left, where the
       table has room, instead of on the right, where it pushes past the edge.

       The label is right-aligned to match, so a long tooltip grows leftwards
       and never reaches the card's edge. */
    left: auto;
    right: 0;
    width: max-content;
    max-width: 14rem;
    text-align: right;
    padding: 0.25rem 0.5rem;
    border-radius: var(--radius-sm);
    background: var(--color-on-surface);
    color: var(--color-surface);
    font-size: 0.6875rem;
    font-weight: 600;
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity 120ms ease, visibility 120ms ease;
    z-index: 20;
}

.wpps-iconbtn[data-tip]:hover::after,
.wpps-iconbtn[data-tip]:focus-visible::after {
    opacity: 1;
    visibility: visible;
}

/* The last column, so a tooltip on the rightmost button is not clipped by the
   table's own overflow. */
.wpps-table__actions {
    text-align: right;
    white-space: nowrap;
}

.wpps-table__actions form {
    display: inline-flex;
}

/* The buttons are inline elements, so the newlines between them in the markup
   were rendering as word spaces — five 36px buttons in a 326px cell, where they
   need 180px. font-size: 0 removes the whitespace nodes without touching the
   buttons, which set their own size.
   
   The selector has to outrank `.enterra-table tbody td`, which also sets a font
   size and is a class plus a class plus an element. A lone class lost to it, and
   so did `td.` plus the class — specificity beats source order, so both attempts
   were silently ignored. This matches it and comes later. */
.enterra-table tbody td.wpps-table__actions {
    font-size: 0;
}

.wpps-table__actions > * {
    vertical-align: middle;
}

/* The icons keep their own size, since the cell's is zero. */
.wpps-table__actions .wpps-iconbtn {
    font-size: 1.25rem;
}

/* --- Colour mode switch ---
   Sits with the account block at the foot of the sidebar. Two named states
   rather than one toggle: the buttons say what they do, and neither is ambiguous
   about which way it points.
   Colours come from the sidebar's own palette, not the surface tokens, because
   the sidebar is dark in both modes. */
.wpps-mode {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-1);
    margin-bottom: var(--space-4);
    padding: var(--space-1);
    border-radius: var(--radius-full);
    background: rgba(255, 255, 255, 0.08);
}

.wpps-mode__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border: none;
    border-radius: var(--radius-full);
    background: transparent;
    color: rgba(255, 255, 255, 0.65);
    font-size: 0.75rem;
    font-weight: 600;
    cursor: pointer;
    transition: background 150ms ease, color 150ms ease;
}

.wpps-mode__btn:hover {
    color: var(--color-white);
}

.wpps-mode__btn[aria-pressed='true'] {
    background: rgba(255, 255, 255, 0.16);
    color: var(--color-white);
}

.wpps-mode__btn:focus-visible {
    outline: 2px solid var(--color-primary-light);
    outline-offset: 2px;
}

/* A field label with a control on the same line, used where an action belongs
   beside the field it acts on rather than at the bottom of the form. */
.wpps-field__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-bottom: var(--space-2);
}

.wpps-field__head .wpps-label {
    margin-bottom: 0;
}

/* What an expanded brief added, and what it had to assume. */
.wpps-brief-note {
    margin-top: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-border-glow);
    border-left: 3px solid var(--color-primary);
    border-radius: var(--radius-sm);
    background: var(--color-surface-elevated);
    font-size: 0.875rem;
    color: var(--color-on-surface-variant);
}

.wpps-brief-note[hidden] {
    display: none;
}

.wpps-brief-note strong {
    display: block;
    margin-bottom: var(--space-2);
    color: var(--color-on-surface);
}

/* The assumptions. Warn-coloured, because they are the part that needs acting
   on: everything downstream treats the brief as true. */
.wpps-brief-note [data-brief-guesses] {
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--color-border-glow);
    color: var(--color-warning);
}

.wpps-brief-note [data-brief-guesses][hidden] {
    display: none;
}

.wpps-brief-note ul {
    margin: var(--space-1) 0 0;
    padding-left: var(--space-5);
}

.wpps-brief-note li {
    margin-bottom: var(--space-1);
}

/* Autosave indicator. Quiet when it worked, because a form saving itself is not
   news; visible when it did not, because that is when someone needs to know. */
.wpps-autosave {
    min-height: 1.25rem;
    margin-top: var(--space-4);
    font-size: 0.8125rem;
    color: var(--color-on-surface-inverse);
}

.wpps-autosave:empty {
    margin-top: 0;
}

.wpps-autosave--warn {
    color: var(--color-warning);
}

/* --- 6.7 Status pills --- */

.wpps-status {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    font-size: 0.8125rem;
    font-weight: 600;
}

.wpps-status::before {
    content: '';
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    background: currentColor;
}

.wpps-status--ok { color: var(--color-success); }
.wpps-status--off { color: var(--color-on-surface-inverse); }
.wpps-status--bad { color: var(--color-error); }
.wpps-status--busy { color: var(--color-warning); }

/* =============================================================================
   7. APP SHELL
   ============================================================================= */

.wpps-body {
    min-height: 100vh;
}

.wpps-shell {
    display: flex;
    min-height: 100vh;
}

.wpps-sidebar {
    width: var(--sidebar-width);
    flex-shrink: 0;
    background: var(--color-primary-darker);
    color: var(--color-white);
    display: flex;
    flex-direction: column;
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    z-index: 50;
    padding: var(--space-6) var(--space-4);
    overflow-y: auto;
}

.wpps-sidebar__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--space-8);
    padding: 0 var(--space-2);
}

.wpps-sidebar__logo {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

.wpps-sidebar__logo .badge {
    background: var(--color-primary-light);
    color: var(--color-primary-darker);
    font-size: 0.7rem;
    padding: var(--space-1) var(--space-2);
}

.wpps-sidebar__name {
    font-family: var(--font-headline);
    font-weight: 700;
    font-size: 0.9375rem;
    color: var(--color-white);
}

.wpps-sidebar__close {
    display: none;
    color: var(--color-white);
}

.wpps-nav {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    flex: 1;
}

.wpps-nav__link {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-md);
    color: rgba(255, 255, 255, 0.72);
    font-size: 0.9375rem;
    font-weight: 500;
    transition: background 150ms ease, color 150ms ease;
}

.wpps-nav__link .material-symbols-outlined {
    color: inherit;
}

.wpps-nav__link:hover {
    background: rgba(255, 255, 255, 0.08);
    color: var(--color-white);
}

.wpps-nav__link.is-active {
    background: var(--color-primary);
    color: var(--color-white);
    font-weight: 600;
    box-shadow: var(--shadow-glow);
}

.wpps-sidebar__foot {
    border-top: 1px solid rgba(255, 255, 255, 0.12);
    padding-top: var(--space-4);
    margin-top: var(--space-4);
}

.wpps-user {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2);
    border-radius: var(--radius-md);
    transition: background 150ms ease;
}

.wpps-user:hover {
    background: rgba(255, 255, 255, 0.08);
}

.wpps-user__avatar {
    width: 2.25rem;
    height: 2.25rem;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--color-primary-light);
    color: var(--color-primary-darker);
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 0.9375rem;
}

.wpps-user__meta {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.wpps-user__name {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--color-white);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.wpps-user__role {
    font-size: 0.75rem;
    color: rgba(255, 255, 255, 0.55);
}

.wpps-logout__btn {
    color: rgba(255, 255, 255, 0.6);
    width: 100%;
    justify-content: flex-start;
    margin-top: var(--space-1);
}

.wpps-logout__btn:hover {
    color: var(--color-white);
    background: rgba(255, 255, 255, 0.08);
}

.wpps-sidebar-scrim {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(5, 38, 46, 0.5);
    backdrop-filter: blur(4px);
    z-index: 40;
}

.wpps-main {
    flex: 1;
    min-width: 0;
    margin-left: var(--sidebar-width);
    display: flex;
    flex-direction: column;
}

.wpps-topbar {
    height: var(--topbar-height);
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: 0 var(--space-8);
    background: var(--color-surface-glass);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--color-border-glow);
    position: sticky;
    top: 0;
    z-index: 30;
}

.wpps-topbar__menu {
    display: none;
}

.wpps-topbar__title {
    font-family: var(--font-headline);
    font-size: 1.125rem;
    font-weight: 800;
    letter-spacing: -0.02em;
    color: var(--color-on-surface);
}

/* No max-width. The content fills the space beside the sidebar on any screen.
   It was capped at 78rem, which left a large dead zone on the right of a wide
   window — the cards stopped two thirds of the way across while the rest of the
   viewport sat empty. Every page is full width now, including the form pages
   that used to opt into a narrower column, so there is no page-level cap to
   apply here or anywhere else. */
.wpps-content {
    padding: var(--space-8);
    width: 100%;
}

.wpps-stack {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
}

/* Forms that are a single column of short inputs. 46rem was too tight: the
   wizard's naming step left most of the viewport empty and the URL field was
   the only wide thing on the page. 64rem gives those forms room to breathe
   without stretching a one-field form across a very wide monitor, which is what
   makes full-width unreadable for a short column of inputs. */

/* =============================================================================
   8. SCREEN-SPECIFIC
   ============================================================================= */

/* --- Auth / bare layout --- */

.wpps-body--bare {
    background: linear-gradient(160deg, var(--color-surface) 0%, var(--surface-bare-bottom) 100%);
}

.wpps-blur-wrap {
    position: fixed;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
}

.wpps-blur-wrap .blur-decal-lg { top: -6rem; left: -6rem; }
.wpps-blur-wrap .blur-decal-md { bottom: -8rem; right: -6rem; }

.wpps-auth {
    position: relative;
    z-index: 1;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: var(--space-8) var(--space-4);
    gap: var(--space-6);
}

.wpps-auth__brand .badge {
    background: var(--color-white);
    box-shadow: var(--shadow-sm);
}

.wpps-otp-actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
}

/* --- Dashboard --- */

/* A labelled row with an action on the right: used for per-site toggles such as
   whether a preview needs a password. Same shape as a checklist item so it reads
   as part of the card rather than a form bolted on. */
.wpps-toggle-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
    padding: var(--space-4) 0;
    border-top: 1px solid var(--color-border-glow);
}

.wpps-toggle-row__text {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
    flex: 1 1 18rem;
}

/* --- Section builder ---
   One row per section: a name, move and remove controls, and a note describing
   what the section should say. Rows are reordered with buttons rather than drag
   and drop so the list stays usable by keyboard and on a phone. */
.wpps-sections {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.wpps-section-row {
    border: 1px solid var(--color-border-glow);
    border-radius: var(--radius-md);
    padding: var(--space-4);
    background: var(--color-surface-elevated);
}

.wpps-section-row__head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
}

.wpps-section-row__handle {
    display: inline-flex;
    color: var(--color-on-surface-inverse);
}

.wpps-section-row__name {
    font-weight: 700;
    color: var(--color-on-surface);
    flex: 1 1 auto;
    min-width: 0;
}

.wpps-section-row__move {
    display: inline-flex;
    gap: var(--space-1);
}

.wpps-section-row__move .icon-btn {
    width: 2rem;
    height: 2rem;
    font-size: 0.9rem;
    line-height: 1;
}

.wpps-section-row .wpps-textarea {
    min-height: 4.5rem;
    resize: vertical;
}

/* A row that was just added, or that a name collided with. Adding a section
   whose name already exists used to scroll the page to a row already on screen,
   which is indistinguishable from a button that does nothing. */
.wpps-section-row--flash {
    animation: wpps-row-flash 1.4s ease-out 1;
}

@keyframes wpps-row-flash {
    0%,
    40% { box-shadow: 0 0 0 2px var(--color-primary); }
    100% { box-shadow: 0 0 0 2px transparent; }
}

@media (prefers-reduced-motion: reduce) {
    .wpps-section-row--flash { animation-duration: 0.01s; }
}

/* The line that says why nothing appeared to happen. */
.wpps-section-notice {
    margin-top: var(--space-2);
}

.wpps-textarea {
    border-radius: var(--radius-md);
    padding: var(--space-3) var(--space-4);
    font-family: inherit;
    line-height: 1.5;
    resize: vertical;
}

/* Theme cards that act as a radio choice: the whole card is the control, so the
   input itself is hidden rather than replaced, which keeps it focusable and
   announced correctly. */
.wpps-theme__radio {
    position: absolute;
    opacity: 0;
    pointer-events: none;
}

/* Selection is drawn from the radio itself rather than from a class.
   The server adds .is-selected when it renders the card, but clicking one only
   changes the radio — nothing added the class in the browser, so a card looked
   unselected until the step was saved and reloaded, which is indistinguishable
   from the click not registering.
   :has() reads the state that is already correct in the DOM, so the highlight
   cannot disagree with what the form will submit. The class rule stays for the
   saved card in case a browser does not support :has(). */
.wpps-theme:has(.wpps-theme__radio:checked),
.wpps-theme.is-selected {
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px var(--color-primary-dim);
}

/* A tick on the selected card. Drawn rather than an icon font glyph so it needs
   no font to load, and positioned over the thumbnail so it does not shift the
   layout when it appears. */
.wpps-theme__shot::after {
    content: '';
    position: absolute;
    top: var(--space-3);
    right: var(--space-3);
    width: 1.75rem;
    height: 1.75rem;
    border-radius: var(--radius-full);
    background: var(--color-primary);
    /* The tick is two borders on a rotated box: no image, no font, and it scales
       with the container. */
    background-image: none;
    opacity: 0;
    transform: scale(0.6);
    transition: opacity 150ms ease, transform 150ms ease;
    box-shadow: var(--shadow-md);
}

.wpps-theme:has(.wpps-theme__radio:checked) .wpps-theme__shot::after,
.wpps-theme.is-selected .wpps-theme__shot::after {
    opacity: 1;
    transform: scale(1);
}

/* The tick mark itself, inside the circle above. */
.wpps-theme__shot::before {
    content: '';
    position: absolute;
    top: calc(var(--space-3) + 0.5rem);
    right: calc(var(--space-3) + 0.62rem);
    width: 0.5rem;
    height: 0.28rem;
    border-left: 2px solid var(--color-white);
    border-bottom: 2px solid var(--color-white);
    transform: rotate(-45deg) scale(0.6);
    opacity: 0;
    transition: opacity 150ms ease, transform 150ms ease;
    z-index: 1;
    pointer-events: none;
}

.wpps-theme:has(.wpps-theme__radio:checked) .wpps-theme__shot::before,
.wpps-theme.is-selected .wpps-theme__shot::before {
    opacity: 1;
    transform: rotate(-45deg) scale(1);
}

/* Keyboard focus has to be visible on the card, since the radio itself is
   hidden. Without this the picker cannot be used without a mouse. */
.wpps-theme:has(.wpps-theme__radio:focus-visible) {
    outline: 2px solid var(--color-primary);
    outline-offset: 3px;
}

.wpps-theme-grid--choose {
    /* The space below the gallery is padding rather than a margin, and that is
       the whole point: this sits at the end of a flex column, where a margin
       collapses through the container's edge. The fieldset then measured 20px
       shorter than the tiles inside it, so the last row of theme cards overhung
       its own fieldset and landed on the heading beneath. Padding is part of the
       box, so it cannot collapse. The visual gap is unchanged. */
    padding-bottom: var(--space-5);
}

/* A value that is masked until asked for: the password in a site's client
   access panel. Both states are in the markup, so nothing has to be fetched to
   reveal it. */
.wpps-reveal {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}

.wpps-reveal code {
    font-size: 0.9em;
}

.wpps-reveal [hidden] {
    display: none;
}

/* A choice that spans the full width, for a single option with a longer
   explanation beside it rather than a row of short labels. */
.wpps-choice--wide {
    width: 100%;
}

.wpps-choice--wide > span {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.wpps-choice > span em {
    font-style: normal;
}

/* --- Theme gallery ---
   Cards carry a wide screenshot, so the grid is sized by minmax rather than a
   fixed column count: the same markup then gives 2 columns on a laptop and 4 on
   a wide monitor without a breakpoint for each. */
.wpps-theme-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: var(--space-5);

    /* Belt and braces alongside the fieldset above. A grid whose tracks have a
       15rem floor also reports a large min-content width, so if any flex
       ancestor measures it by its contents the tracks collapse towards the
       floor and every card ends up a sliver in one long row. Width: 100% makes
       the element take the box it is given, whatever the ancestors are doing. */
    width: 100%;
    min-width: 0;
}

.wpps-theme {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--color-border-glow);
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--color-surface-elevated);
    color: inherit;
    text-decoration: none;
    transition: border-color 150ms ease, transform 150ms ease, box-shadow 150ms ease;

    /* A card is a fixed shape: a thumbnail above a caption. Nothing inside it
       should be able to make it taller, whatever the image turns out to be —
       an oversized thumbnail used to stretch the whole gallery and push the
       cards over the controls below them.

       `contain: paint` rather than `contain: layout paint`. The paint half is
       what does the work here: it clips the thumbnail to the card's rounded
       corners. The layout half was also telling the browser it need not
       consider the card's contents when sizing it, and Safari took that
       literally — a card whose only sized child is an aspect-ratio box then
       measured zero tall, so the caption was clipped away and every card
       rendered as a tall empty shell. Firefox and Chrome infer an intrinsic
       height anyway, which is why this only ever showed up in Safari. */
    contain: paint;
}

/* The two halves keep the shape they were given rather than being stretched or
   shrunk by the flex column around them. */
.wpps-theme__shot,
.wpps-theme__body {
    flex: 0 0 auto;
}

.wpps-theme:hover {
    border-color: color-mix(in srgb, var(--color-primary) 35%, transparent);
    transform: translateY(-2px);
    box-shadow: var(--shadow-lg);
}

.wpps-theme:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}

/* A theme that cannot be used is dimmed but still readable, so the reason it is
   unavailable stays visible rather than the card simply vanishing. */
.wpps-theme.is-disabled {
    opacity: 0.72;
}

.wpps-theme__shot {
    position: relative;
    display: block;
    /* The source screenshots are 4:3, so reserving that shape stops the grid
       reflowing as images arrive. */
    aspect-ratio: 4 / 3;
    background: var(--color-surface);
    overflow: hidden;
}

.wpps-theme__shot img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: top center;
    display: block;
}

.wpps-theme__noimg {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    height: 100%;
    color: var(--color-on-surface-inverse);
    font-size: 0.8125rem;
}

.wpps-theme__flag {
    position: absolute;
    top: var(--space-2);
    left: var(--space-2);
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-full);
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    background: var(--color-surface-elevated);
}

.wpps-theme__flag--review {
    background: var(--color-warning);
    color: #3d2c00;
}

.wpps-theme__flag--bad {
    background: var(--color-error);
    color: var(--color-white);
}

.wpps-theme__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-4);
}

.wpps-theme__name {
    font-family: var(--font-headline);
    font-size: 1rem;
    font-weight: 800;
    color: var(--color-on-surface);
}

.wpps-theme__meta {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    font-size: 0.75rem;
}

.wpps-theme__desc {
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--color-on-surface-variant);
}

/* The full screenshot on a theme's own page, bounded so a tall image cannot
   push the rest of the page out of reach. */
.wpps-theme-preview {
    border: 1px solid var(--color-border-glow);
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--color-surface);
}

.wpps-theme-preview img {
    width: 100%;
    height: auto;
    display: block;
}

/* Simple label pills, for listing a theme's section parts. */
.wpps-pills {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    list-style: none;
    margin: 0;
    padding: 0;
}

.wpps-pill {
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-full);
    background: var(--color-primary-dim);
    color: var(--color-on-primary-dim);
    font-size: 0.75rem;
    font-weight: 600;
}

.wpps-stats {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-6);
}

.wpps-stat {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.wpps-stat__label {
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--color-on-surface-variant);
}

.wpps-stat__value {
    font-family: var(--font-headline);
    font-size: 2.5rem;
    font-weight: 800;
    line-height: 1;
    color: var(--color-primary);
}

.wpps-stat__meta {
    font-size: 0.8125rem;
    color: var(--color-on-surface-inverse);
}

.wpps-stat--action {
    justify-content: space-between;
    gap: var(--space-4);
}

.wpps-checklist {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.wpps-checklist__item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-2);
    border-radius: var(--radius-md);
    border-bottom: 1px solid #f1f5f9;
}

.wpps-checklist__item:last-child {
    border-bottom: none;
}

.wpps-checklist__item .material-symbols-outlined {
    color: var(--color-on-surface-inverse);
    flex-shrink: 0;
}

.wpps-checklist__item.is-ok .material-symbols-outlined {
    color: var(--color-success);
}

.wpps-checklist__item.is-fail .material-symbols-outlined {
    color: var(--color-error);
}

.wpps-checklist__body {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-width: 0;
}

.wpps-checklist__label {
    font-size: 0.9375rem;
    font-weight: 600;
    color: var(--color-on-surface);
}

.wpps-checklist__detail {
    font-size: 0.8125rem;
    color: var(--color-on-surface-variant);
    word-break: break-word;
}

.wpps-checklist--compact .wpps-checklist__item {
    padding: var(--space-2) 0;
}

.wpps-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--space-2);
    padding: var(--space-12) var(--space-6);
}

.wpps-empty .material-symbols-outlined {
    color: var(--color-on-surface-inverse);
}

.wpps-empty .btn {
    align-self: center;
}

.wpps-activity {
    display: flex;
    flex-direction: column;
}

.wpps-activity__item {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-3) 0;
    border-bottom: 1px solid #f1f5f9;
}

.wpps-activity__item:last-child {
    border-bottom: none;
}

.wpps-activity__action {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--color-on-surface);
    font-family: var(--font-mono);
}

.wpps-activity__meta {
    font-size: 0.8125rem;
    color: var(--color-on-surface-inverse);
}

/* --- Settings --- */

.wpps-actions {
    display: flex;
    /* Flex-end rather than centre, because a row here often pairs a button with
       a labelled field. The field is taller — it has a label above it — so
       centring them puts the button somewhere in the middle of the label, which
       is what it looked like: a button floating beside a heading rather than
       beside the input. Aligning to the end lines the button up with the input's
       own bottom edge. */
    align-items: flex-end;
    gap: var(--space-4);
    flex-wrap: wrap;
    margin-top: var(--space-6);
    padding-top: var(--space-5);
    border-top: 1px solid var(--color-border-glow);
}

/* A button and a field on one row should have their controls on the same line,
   which means the button needs the height of the field's input rather than of
   the whole field. */
.wpps-actions > .btn {
    min-height: 3rem;
}

.wpps-actions__note {
    font-size: 0.8125rem;
    color: var(--color-on-surface-inverse);
}

.wpps-actions__input {
    max-width: 18rem;
    flex: 1 1 14rem;
}

.wpps-test-result {
    margin-top: var(--space-4);
    padding: var(--space-4);
    border-radius: var(--radius-md);
    font-size: 0.875rem;
    line-height: 1.5;
}

.wpps-test-result.is-ok {
    background: var(--color-success-container);
    color: var(--color-success);
}

.wpps-test-result.is-error {
    background: var(--color-error-container);
    color: var(--color-error);
}

.wpps-test-result.is-busy {
    background: var(--color-secondary);
    color: var(--color-on-surface-variant);
}

.wpps-envnote {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    background: var(--color-surface);
    border-radius: var(--radius-md);
    font-size: 0.8125rem;
    color: var(--color-on-surface-variant);
    margin-top: var(--space-4);
}

.wpps-envnote code {
    font-size: 0.8em;
}

.wpps-savebar {
    position: sticky;
    bottom: var(--space-4);
    display: flex;
    justify-content: flex-end;
    padding: var(--space-4);
    background: var(--color-surface-glass);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid var(--color-border-glow);
    border-radius: var(--radius-full);
    box-shadow: var(--shadow-lg);
    width: fit-content;
    margin-left: auto;
    z-index: 20;
}

/* --- Wizard --- */

.wpps-steps {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    counter-reset: step;
}

.wpps-steps__item {
    display: flex;
    align-items: flex-start;
    gap: var(--space-4);
    padding: var(--space-4) var(--space-2);
    border-bottom: 1px solid #f1f5f9;
}

.wpps-steps__item:last-child {
    border-bottom: none;
}

.wpps-steps__num {
    flex-shrink: 0;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    background: var(--color-primary-dim);
    color: var(--color-primary);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.8125rem;
    font-weight: 700;
}

.wpps-steps__item > div {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.wpps-steps__label {
    font-size: 0.9375rem;
    font-weight: 600;
    color: var(--color-on-surface);
}

.wpps-steps__detail {
    font-size: 0.8125rem;
    color: var(--color-on-surface-variant);
}

/* --- Error pages --- */

.wpps-center {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-8);
}

.wpps-error-card {
    max-width: 30rem;
    padding: var(--space-8);
    text-align: center;
}

/* =============================================================================
   9. RESPONSIVE
   ============================================================================= */

@media (max-width: 1100px) {
    .wpps-stats {
        grid-template-columns: 1fr;
    }
}

@media (max-width: 900px) {
    .wpps-sidebar {
        transform: translateX(-100%);
        transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
        box-shadow: var(--shadow-lg);
    }

    .wpps-sidebar.is-open {
        transform: translateX(0);
    }

    .wpps-sidebar__close {
        display: inline-flex;
    }

    .wpps-sidebar-scrim.is-open {
        display: block;
    }

    .wpps-main {
        margin-left: 0;
    }

    .wpps-topbar {
        padding: 0 var(--space-5);
    }

    .wpps-topbar__menu {
        display: inline-flex;
    }

    .wpps-content {
        padding: var(--space-5);
    }

    .wpps-grid-2 {
        grid-template-columns: 1fr;
    }
}

@media (max-width: 600px) {
    .wpps-card {
        padding: var(--space-5);
    }

    /* The sign-in card keeps a little more room: it holds no sidebar or table,
       so squeezing it only makes the form feel tight. */
    .wpps-card--auth {
        padding: var(--space-6);
    }

    .wpps-auth {
        padding: var(--space-6) var(--space-4);
        gap: var(--space-5);
    }

    .wpps-card__head--row {
        flex-direction: column;
        align-items: stretch;
    }

    .wpps-table__actions {
        text-align: left;
    }

    .wpps-savebar {
        width: 100%;
        border-radius: var(--radius-lg);
        justify-content: stretch;
    }

    .wpps-savebar .btn {
        width: 100%;
    }

    .wpps-stats {
        gap: var(--space-4);
    }
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* =============================================================================
   10. DEPLOYMENT & DIAGNOSTICS
   ============================================================================= */

/* Generated configuration blocks */

.wpps-config {
    border: 1px solid color-mix(in srgb, var(--color-primary) 15%, transparent);
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--color-primary-darker);
}

.wpps-config__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-3) var(--space-4);
    background: rgba(255, 255, 255, 0.06);
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.wpps-config__title {
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--color-primary-light);
}

.wpps-config__head .btn-text {
    color: rgba(255, 255, 255, 0.75);
}

.wpps-config__head .btn-text:hover {
    color: var(--color-white);
    background: rgba(255, 255, 255, 0.1);
}

.wpps-config__body {
    margin: 0;
    padding: var(--space-4);
    max-height: 28rem;
    overflow: auto;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    line-height: 1.65;
    color: #d7e6ea;
    background: transparent;
    white-space: pre;
    tab-size: 4;
}

.wpps-config__body::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}

.wpps-config__body::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.18);
    border-radius: var(--radius-full);
}

/* Diagnostics */

.wpps-diagnostics__group {
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--color-on-surface-inverse);
    margin: var(--space-6) 0 var(--space-2);
}

.wpps-diagnostics__group:first-of-type {
    margin-top: 0;
}

.wpps-checklist__hint {
    font-size: 0.78rem;
    color: var(--color-primary);
    margin-top: 0.15rem;
}

/* Copy-to-clipboard feedback */

.btn.is-copied {
    color: var(--color-success) !important;
}

/* =============================================================================
   11. WIZARD
   ============================================================================= */

/* Step rail */

.wpps-steps--compact {
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.wpps-steps--compact .wpps-steps__item {
    border-bottom: none;
    padding: var(--space-2);
    border-radius: var(--radius-md);
    gap: var(--space-2);
    align-items: center;
}

.wpps-steps--compact .wpps-steps__item a {
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--color-on-surface-variant);
}

.wpps-steps--compact .wpps-steps__item.is-current {
    background: var(--color-primary-dim);
}

.wpps-steps--compact .wpps-steps__item.is-current a {
    color: var(--color-primary);
    font-weight: 700;
}

.wpps-steps--compact .wpps-steps__item.is-done .wpps-steps__num {
    background: transparent;
    color: var(--color-success);
    width: auto;
    height: auto;
}

/* Choice groups */

.wpps-fieldset {
    border: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);

    /* As a flex item this defaults to min-width: auto, which resolves to
       min-content — and its min-content width is set by the widest thing inside
       it, which for the theme gallery is a 15rem grid track. That lets the
       fieldset refuse to shrink below 240px and be measured by its contents
       instead of by the space it has. Forcing the minimum to zero keeps every
       fieldset the width of its container, so what is inside is resolved against
       the real available width. */
    min-width: 0;
}

.wpps-choices {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: var(--space-3);
}

.wpps-choice {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-border-glow);
    border-radius: var(--radius-md);
    background: var(--color-surface-elevated);
    cursor: pointer;
    font-size: 0.875rem;
    color: var(--color-on-surface-variant);
    transition: border-color 150ms ease, background 150ms ease;
}

.wpps-choice:hover {
    border-color: var(--color-primary);
}

.wpps-choice:has(input:checked) {
    border-color: var(--color-primary);
    background: var(--color-primary-dim);
    color: var(--color-on-surface);
    font-weight: 600;
}

.wpps-choice input {
    margin-top: 0.15rem;
    accent-color: var(--color-primary);
}

/* Offered but not yet usable — shown so the option is discoverable, muted so it
   is clear it will not do anything until its prerequisite is met. */
.wpps-choice:has(input:disabled) {
    cursor: not-allowed;
    opacity: 0.6;
}

.wpps-choice:hover:has(input:disabled) {
    border-color: var(--color-border-glow);
}

.wpps-choice input:disabled {
    cursor: not-allowed;
}

/* Palettes */

.wpps-palette {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-border-glow);
    border-radius: var(--radius-md);
    background: var(--color-surface-elevated);
    cursor: pointer;
    font-size: 0.875rem;
    color: var(--color-on-surface);
    text-align: left;
}

.wpps-palette:hover,
.wpps-palette.is-selected {
    border-color: var(--color-primary);
    transform: translateY(-1px);
}

.wpps-palette__swatches {
    display: inline-flex;
    gap: 3px;
    flex-shrink: 0;
}

.wpps-palette__swatches i {
    width: 1.15rem;
    height: 1.15rem;
    border-radius: var(--radius-sm);
    border: 1px solid rgba(0, 0, 0, 0.08);
}

.wpps-colour {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

.wpps-colour input[type="color"] {
    width: 3rem;
    /* Matches the text field beside it, which is a pill: a square swatch next to
       a fully rounded input reads as two different kinds of control. */
    height: 3rem;
    padding: 3px;
    border: 1px solid color-mix(in srgb, var(--color-primary) 20%, transparent);
    border-radius: var(--radius-full);
    background: var(--color-surface-elevated);
    cursor: pointer;
    flex-shrink: 0;
}

/* The native swatch is a rectangle inside the control. Rounding it too is what
   actually makes the colour area itself round, rather than a rectangle sitting
   in a rounded box. */
.wpps-colour input[type="color"]::-webkit-color-swatch-wrapper {
    padding: 0;
}

.wpps-colour input[type="color"]::-webkit-color-swatch {
    border: none;
    border-radius: var(--radius-full);
}

.wpps-colour input[type="color"]::-moz-color-swatch {
    border: none;
    border-radius: var(--radius-full);
}

/* --- Typeface preview ---
   Shows the two chosen faces at the sizes the built site uses, so a choice can be
   judged before it is built rather than after. The stacks come from the same
   catalogue the build reads, so this is the real thing and not an impression. */
.wpps-type-preview {
    display: flex;
    align-items: center;
    gap: var(--space-5);
    margin-top: var(--space-4);
    padding: var(--space-5);
    border: 1px solid var(--color-border-glow);
    border-radius: var(--radius-md);
    background: var(--color-surface);
}

.wpps-type-preview__sample {
    flex: 0 0 auto;
    font-family: var(--preview-h1, var(--preview-heading));
    font-size: 4rem;
    font-weight: 800;
    line-height: 1;
    letter-spacing: -0.03em;
    color: var(--color-primary);
}

.wpps-type-preview__text {
    min-width: 0;
}

.wpps-type-preview__title {
    font-family: var(--preview-heading);
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: -0.02em;
    color: var(--color-on-surface);
}

.wpps-type-preview__body {
    margin-top: var(--space-2);
    font-family: var(--preview-body);
    font-size: 0.9375rem;
    line-height: 1.6;
    color: var(--color-on-surface-variant);
}

@media (max-width: 900px) {
    .wpps-type-preview {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-3);
    }

    .wpps-type-preview__sample {
        font-size: 3rem;
    }
}

/* Screenshot uploads */

.wpps-dropzone {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-8);
    border: 2px dashed var(--color-border-glow);
    border-radius: var(--radius-lg);
    background: var(--color-surface);
    text-align: center;
}

.wpps-dropzone .material-symbols-outlined {
    color: var(--color-on-surface-inverse);
}

.wpps-dropzone input[type="file"] {
    font-size: 0.875rem;
    color: var(--color-on-surface-variant);
}

.wpps-uploads {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
    gap: var(--space-4);
    margin-top: var(--space-6);
}

.wpps-upload {
    margin: 0;
    border: 1px solid var(--color-border-glow);
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--color-surface-elevated);
}

.wpps-upload img {
    width: 100%;
    height: 8rem;
    object-fit: cover;
    object-position: top center;
    border-bottom: 1px solid var(--color-border-glow);
}

.wpps-upload figcaption {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    font-size: 0.75rem;
}

.wpps-upload figcaption > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--color-on-surface-variant);
}

/* Page list */

.wpps-pages {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.wpps-page-row {
    display: grid;
    grid-template-columns: 1fr 12rem 12rem auto;
    gap: var(--space-3);
    align-items: end;
    padding: var(--space-3);
    border: 1px solid var(--color-border-glow);
    border-radius: var(--radius-md);
    background: var(--color-surface);
}

.wpps-page-row__remove {
    color: var(--color-error);
    align-self: end;
    padding-bottom: var(--space-3);
}

.wpps-page-row__remove:hover {
    background: var(--color-error-container);
}

/* Review summary */

.wpps-summary {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
    gap: var(--space-4) var(--space-6);
    margin: 0 0 var(--space-6);
}

.wpps-summary dt {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--color-on-surface-inverse);
    margin-bottom: 0.2rem;
}

.wpps-summary dd {
    margin: 0;
    font-size: 0.9375rem;
    color: var(--color-on-surface);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    /* A summary value is often one long unbreakable string — a URL, an email
       address, a join of two of them. Without this the cell is pushed wider than
       its grid track and the text runs out past the card's edge, which is what it
       did. min-width: 0 lets the cell shrink to its track; the rules below give
       the text somewhere to break. */
    min-width: 0;
}

/* Break a long value rather than let it overflow. anywhere is used ahead of
   break-word because a URL has no spaces: break-word would only break it once the
   line is already overflowing, whereas anywhere breaks inside the word as soon as
   it needs to. */
.wpps-summary dd,
.wpps-summary dd * {
    overflow-wrap: anywhere;
}

/* The one value that is a link is still a link, so it stays clickable when it
   wraps onto three lines. */
.wpps-summary dd a {
    word-break: break-word;
}

/* Warning variant, used for "works but with a caveat". */
.wpps-alert--warn {
    background: var(--color-warning-container);
    color: var(--color-warning);
    border: 1px solid color-mix(in srgb, var(--color-warning) 25%, transparent);
}

@media (max-width: 900px) {
    .wpps-page-row {
        grid-template-columns: 1fr;
    }

    .wpps-page-row__remove {
        padding-bottom: 0;
    }

    .wpps-steps--compact {
        flex-direction: column;
    }
}

/* -----------------------------------------------------------------------------
   Image library

   A two-column card: the picture on the left at a fixed width, everything that
   describes it on the right. The image is deliberately small and fixed — the
   point of this page is the words, and a grid of large tiles pushes the fields
   that matter off the screen.
   -------------------------------------------------------------------------- */

.wpps-media {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.wpps-media__item {
    display: grid;
    grid-template-columns: 11rem 1fr;
    gap: var(--space-5);
    padding: var(--space-4);
    border: 1px solid var(--color-border-glow);
    border-radius: var(--radius-md);
    background: var(--color-surface-elevated);
}

.wpps-media__figure {
    margin: 0;
    align-self: start;
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--color-surface);
}

.wpps-media__figure img {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 3;
    object-fit: cover;
}

.wpps-media__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    min-width: 0;
}

.wpps-media__name {
    margin: 0;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.wpps-media__meta {
    margin: 0;
    font-size: 0.75rem;
    color: var(--color-on-surface-variant);
}

.wpps-media__saved {
    color: var(--color-success, var(--color-on-surface-variant));
}

.wpps-media__body .wpps-field {
    margin: 0;
}

.wpps-media__actions {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

/* The page is one long column of forms, so the fields sit on a grid rather than
   stacking, which halves the scrolling and keeps every card the same shape. */
.wpps-media__body form {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    gap: var(--space-3);
}

.wpps-media__body form .wpps-field:has(textarea),
.wpps-media__body form .wpps-media__actions {
    grid-column: 1 / -1;
}

@media (max-width: 720px) {
    .wpps-media__item {
        grid-template-columns: 1fr;
    }
}

/* -----------------------------------------------------------------------------
   The agent's review of a build

   A score beside its verdict, the photographs it was judged from, and the findings
   as a list ordered by how much they matter. Severity is carried as a coloured
   edge rather than as a badge colour, so the list reads top to bottom as a
   priority order without turning into a traffic-light.
   -------------------------------------------------------------------------- */

.wpps-review {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-4);
    border: 1px solid var(--color-border-glow);
    border-radius: var(--radius-md);
    background: var(--color-surface-elevated);
}

.wpps-review__score {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-width: 5rem;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
}

.wpps-review__number {
    font-size: 2.25rem;
    font-weight: 700;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}

.wpps-review__outof {
    font-size: 0.6875rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-on-surface-variant);
}

/* A score is context, not an alarm: the number carries the meaning and the
   colour only separates "showable" from "not yet". */
.wpps-review__score[data-score="0"] .wpps-review__number,
.wpps-review__score[data-score="1"] .wpps-review__number,
.wpps-review__score[data-score="2"] .wpps-review__number,
.wpps-review__score[data-score="3"] .wpps-review__number,
.wpps-review__score[data-score="4"] .wpps-review__number {
    color: var(--color-danger, #b91c1c);
}

.wpps-review__score[data-score="8"] .wpps-review__number,
.wpps-review__score[data-score="9"] .wpps-review__number,
.wpps-review__score[data-score="10"] .wpps-review__number {
    color: var(--color-success);
}

.wpps-review__body { min-width: 0; }

.wpps-review__verdict {
    margin: 0;
    font-size: 1rem;
    line-height: 1.5;
}

.wpps-shots {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: var(--space-4);
    margin-top: var(--space-4);
}

.wpps-shot {
    margin: 0;
    border: 1px solid var(--color-border-glow);
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--color-surface);
}

.wpps-shot img {
    display: block;
    width: 100%;
    height: auto;
    border-bottom: 1px solid var(--color-border-glow);
}

.wpps-shot figcaption {
    padding: var(--space-2) var(--space-3);
    font-size: 0.75rem;
    color: var(--color-on-surface-variant);
}

.wpps-findings {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin: var(--space-5) 0 0;
    padding: 0;
    list-style: none;
}

.wpps-finding {
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-border-glow);
    border-left-width: 4px;
    border-radius: var(--radius-sm);
    background: var(--color-surface-elevated);
}

.wpps-finding[data-severity="high"] { border-left-color: var(--color-danger, #b91c1c); }
.wpps-finding[data-severity="medium"] { border-left-color: var(--color-warning, #b45309); }
.wpps-finding[data-severity="low"] { border-left-color: var(--color-border-glow); }

.wpps-finding__head {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    margin: 0 0 var(--space-2);
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-on-surface-variant);
}

.wpps-finding__severity { font-weight: 700; }

.wpps-finding__problem {
    margin: 0;
    line-height: 1.55;
}

.wpps-finding__fix {
    margin: var(--space-2) 0 0;
    font-size: 0.875rem;
    line-height: 1.55;
    color: var(--color-on-surface-variant);
}

.wpps-review__good {
    margin-top: var(--space-4);
    font-size: 0.875rem;
    color: var(--color-on-surface-variant);
}

.wpps-review__good summary { cursor: pointer; }

.wpps-review__good ul {
    margin: var(--space-2) 0 0;
    padding-left: 1.1rem;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    line-height: 1.55;
}

/* The library's bulk describe action and the per-image one. */
.wpps-media__bulk {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    flex-wrap: wrap;
    margin: 0 0 var(--space-5);
}

.wpps-media__bulk .wpps-hint { margin: 0; }

.wpps-media__describe {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin: 0;
}

.wpps-media__describe .wpps-hint { margin: 0; }

/* The review's two actions: look at it again, or fix what it found. */
.wpps-review__actions {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-top: var(--space-4);
}

.wpps-review__actions form { margin: 0; }

/* -----------------------------------------------------------------------------
   A build in progress

   Work the browser is no longer attached to. The bar reports what the server
   says rather than animating on its own, because a bar that moves by itself
   tells the operator nothing about whether anything is happening.
   -------------------------------------------------------------------------- */

.wpps-bar {
    position: relative;
    height: 0.625rem;
    margin: var(--space-4) 0 var(--space-3);
    border-radius: 999px;
    overflow: hidden;
    background: var(--color-surface);
    border: 1px solid var(--color-border-glow);
}

.wpps-bar__fill {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: var(--color-primary);
    /* Slow enough to read as progress rather than as a jump, and only on width:
       the value itself comes from the server. */
    transition: width 600ms ease;
}

.wpps-build__stage {
    margin: 0;
    font-size: 0.875rem;
    color: var(--color-on-surface-variant);
    font-variant-numeric: tabular-nums;
}

.wpps-build__stage--stale {
    color: var(--color-warning);
}
