/* ===========================================================================
   Client portal.

   Deliberately its own small stylesheet rather than reusing the client-site
   one: that file is a marketing template whose colors are overridden per
   tenant, and the portal is a tool. Sharing it would mean a client's brand
   palette recoloring the admin UI.

   Organized in layers so specificity stays predictable as this grows:
   tokens -> base -> components -> utilities -> responsive. Everything visual
   is a token; if you are about to type a hex value below the tokens block,
   add a token instead.
   =========================================================================== */

@layer tokens, base, components, utilities, responsive;

/* ===========================================================================
   WEBFONT

   Inter, vendored rather than linked. The portal's CSP is `default-src 'self'`
   with no font-src override, so a font CDN is blocked outright and a committed
   file is the only way a webfont gets in at all.

   One variable file covering 100-900 — the latin subset (48 KB) rather than
   the full 352 KB release, because nothing in the portal renders Cyrillic,
   Greek or Vietnamese. `swap` so a slow font never blanks the page: the stack
   below falls back to system-ui and reflows when Inter lands.
   =========================================================================== */
@font-face {
    font-family: "Inter";
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url("fonts/inter-var-latin.woff2") format("woff2");
}

/* ===========================================================================
   TOKENS
   =========================================================================== */
@layer tokens {
    :root {
        /* --- neutral ramp --------------------------------------------------
           Cool gray. Ten steps rather than the five ad-hoc hexes this file
           used to carry, so "one notch darker" is a decision the ramp already
           made rather than a hex value invented at the call site. */
        --n-0:   #ffffff;
        --n-25:  #fcfcfd;
        --n-50:  #f7f8fa;
        --n-100: #f1f3f7;
        --n-200: #e6e9ef;
        --n-300: #d3d8e0;
        --n-400: #a6aebc;
        --n-500: #7b8595;
        --n-600: #5b6675;
        --n-700: #414b59;
        --n-800: #2a323d;
        --n-900: #171d25;

        /* --- accent ---------------------------------------------------------
           A deep azure-indigo, not the borrowed GitHub blue this file started
           with. Used for links, active state, focus and data — NOT for the
           primary button, which is graphite (see --btn-primary-bg). A dark
           primary action with a colored accent around it is the single
           strongest signal that this is not a 2010 admin panel. */
        --a-50:  #eef2ff;
        --a-100: #dfe6ff;
        --a-200: #c4d1ff;
        --a-300: #9db2ff;
        --a-400: #7089fb;
        --a-500: #4d63f0;
        --a-600: #3547dd;
        --a-700: #2b37b8;
        --a-800: #283296;
        --a-900: #263177;

        /* --- status --------------------------------------------------------
           Three roles, each with a surface, a border and a text tone, so an
           alert and a badge of the same meaning cannot drift apart. */
        --ok-bg:      #e8f7ee;
        --ok-border:  #b6e4c8;
        --ok-text:    #12603a;
        --warn-bg:    #fdf5e4;
        --warn-border:#f0dcae;
        --warn-text:  #7a5310;
        --err-bg:     #fdecec;
        --err-border: #f6c9c9;
        --err-text:   #9b1c1c;

        /* --- semantic roles ------------------------------------------------
           What the rest of the file actually references. Swapping a ramp step
           here restyles the portal; a later dark mode is this block again
           under a media query, not a rewrite. */
        --bg: var(--n-50);
        --surface: var(--n-0);
        --surface-sunken: var(--n-100);
        --text: var(--n-900);
        --text-soft: var(--n-700);
        --muted: var(--n-600);
        --faint: var(--n-500);
        --border: var(--n-200);
        --border-strong: var(--n-300);
        --accent: var(--a-600);
        --accent-hover: var(--a-700);
        --accent-soft: var(--a-50);
        --accent-contrast: #ffffff;

        --btn-primary-bg: var(--n-900);
        --btn-primary-bg-hover: var(--n-800);
        --btn-primary-text: #ffffff;

        /* --- elevation ------------------------------------------------------
           Depth comes from these, not from a 1px gray line around everything.
           Flat-with-borders was the loudest date stamp on the old portal. */
        --shadow-xs: 0 1px 2px rgba(16, 24, 40, 0.05);
        --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.06);
        --shadow-md: 0 2px 4px -1px rgba(16, 24, 40, 0.05), 0 8px 16px -6px rgba(16, 24, 40, 0.10);
        --shadow-lg: 0 4px 8px -2px rgba(16, 24, 40, 0.06), 0 16px 32px -8px rgba(16, 24, 40, 0.12);

        /* --- radius ---------------------------------------------------------
           Two values, not the five the old file drifted into (10/6/5/4/999).
           Surfaces are round; controls inside them are less round. */
        --r-sm: 6px;
        --r-md: 8px;
        --r-lg: 12px;
        --r-xl: 16px;
        --r-full: 999px;

        /* --- spacing --------------------------------------------------------
           A 4px rhythm. Everything snaps to it. */
        --s-1: 0.25rem;
        --s-2: 0.5rem;
        --s-3: 0.75rem;
        --s-4: 1rem;
        --s-5: 1.25rem;
        --s-6: 1.5rem;
        --s-8: 2rem;
        --s-10: 2.5rem;
        --s-12: 3rem;

        /* --- type -----------------------------------------------------------
           Inter, vendored above. The rest of the stack is the fallback while
           it loads and the answer if it ever fails to; the hierarchy does the
           work a typeface alone would not anyway — a real scale, tight
           tracking on large text, and tabular figures wherever a number is
           meant to be read as data. */
        --font: Inter, system-ui, -apple-system, "Segoe UI", Roboto,
                "Helvetica Neue", Arial, sans-serif;
        --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

        --t-xs:   0.75rem;
        --t-sm:   0.8125rem;
        --t-base: 0.9375rem;
        --t-md:   1rem;
        --t-lg:   1.125rem;
        --t-xl:   1.375rem;
        --t-2xl:  1.75rem;
        --t-3xl:  2.25rem;

        /* Large text needs negative tracking to stop looking like a word
           processor; small caps-y labels need positive. */
        --track-tight: -0.02em;
        --track-snug: -0.011em;
        --track-wide: 0.04em;

        /* --- motion ---------------------------------------------------------
           The old file contained not one transition in 557 lines. Every hover
           snapped. */
        --ease: cubic-bezier(0.4, 0, 0.2, 1);
        --dur: 150ms;
        --transition: color var(--dur) var(--ease),
                      background-color var(--dur) var(--ease),
                      border-color var(--dur) var(--ease),
                      box-shadow var(--dur) var(--ease),
                      transform var(--dur) var(--ease),
                      opacity var(--dur) var(--ease);

        /* --- focus ----------------------------------------------------------
           One ring, everywhere. Two shadows so the ring reads on both light
           surfaces and colored ones: an inner halo the color of the surface,
           then the accent outside it. */
        --focus-ring: 0 0 0 2px var(--surface), 0 0 0 4px var(--a-400);

        --shell-width: 1080px;
    }

    @media (prefers-reduced-motion: reduce) {
        :root { --dur: 0ms; }
    }
}

/* ===========================================================================
   BASE
   =========================================================================== */
@layer base {
    * { box-sizing: border-box; }

    html { -webkit-text-size-adjust: 100%; }

    body {
        margin: 0;
        font-family: var(--font);
        font-size: var(--t-base);
        background: var(--bg);
        color: var(--text);
        line-height: 1.55;
        letter-spacing: var(--track-snug);
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
    }

    h1, h2, h3, h4 {
        letter-spacing: var(--track-tight);
        line-height: 1.25;
        color: var(--text);
    }

    a {
        color: var(--accent);
        text-decoration-color: color-mix(in srgb, var(--accent) 35%, transparent);
        text-underline-offset: 2px;
        transition: var(--transition);
    }
    a:hover { color: var(--accent-hover); text-decoration-color: currentColor; }

    /* One focus treatment for the whole portal. :focus-visible so a mouse
       click on a card or a button does not light up a ring nobody asked for,
       while every keyboard path still gets one. */
    :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
        outline: none;
        box-shadow: var(--focus-ring);
        border-radius: var(--r-sm);
    }

    code {
        font-family: var(--font-mono);
        background: var(--surface-sunken);
        border: 1px solid var(--border);
        padding: 0.08rem 0.35rem;
        border-radius: var(--r-sm);
        font-size: 0.875em;
        color: var(--text-soft);
    }

    /* Numbers are data here, so they line up in columns. */
    table, .tile-n, .num, time { font-variant-numeric: tabular-nums; }
}

/* ===========================================================================
   COMPONENTS
   =========================================================================== */
@layer components {

    /* --- chrome --------------------------------------------------------- */
    .topbar {
        display: flex;
        align-items: center;
        gap: var(--s-6);
        padding: 0 var(--s-6);
        height: 56px;
        background: color-mix(in srgb, var(--surface) 85%, transparent);
        backdrop-filter: saturate(180%) blur(12px);
        border-bottom: 1px solid var(--border);
        position: sticky;
        top: 0;
        z-index: 20;
    }
    .topbar .brand {
        display: inline-flex;
        align-items: center;
        gap: var(--s-2);
        font-weight: 700;
        font-size: var(--t-md);
        letter-spacing: var(--track-tight);
        text-decoration: none;
        color: var(--text);
    }
    /* The mark: a rounded square holding a single B. One letter can be set
       large enough to read as a logo rather than as shrunken text, which two
       never could at 26px. */
    .brand-mark {
        width: 26px;
        height: 26px;
        flex-shrink: 0;
        border-radius: var(--r-md);
        background: linear-gradient(135deg, var(--a-500), var(--a-700));
        color: #fff;
        font-size: 0.875rem;
        font-weight: 700;
        line-height: 1;
        letter-spacing: 0;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        box-shadow: var(--shadow-xs);
    }
    .brand-name { white-space: nowrap; }
    .topbar nav { display: flex; gap: var(--s-1); min-width: 0; overflow-x: auto; }
    .topbar nav a {
        flex-shrink: 0;
        display: inline-flex;
        align-items: center;
        gap: var(--s-2);
        padding: 0.4rem var(--s-3);
        border-radius: var(--r-md);
        color: var(--muted);
        text-decoration: none;
        font-weight: 550;
        font-size: var(--t-base);
        transition: var(--transition);
    }
    .topbar nav a:hover { color: var(--text); background: var(--surface-sunken); }
    .topbar nav a.active { color: var(--text); background: var(--surface-sunken); }

    .topbar-user {
        margin-left: auto;
        display: flex;
        align-items: center;
        gap: var(--s-3);
        font-size: var(--t-base);
        color: var(--muted);
    }
    .topbar-user form { margin: 0; }

    /* The signed-in user as a chip: initials avatar plus name. Replaces a bare
       run of gray text with something that reads as an account — and it is the
       button that opens the account menu, so it has to look pressable. */
    .user-chip {
        display: inline-flex;
        align-items: center;
        gap: var(--s-2);
        padding: 0.25rem 0.5rem 0.25rem 0.25rem;
        border-radius: var(--r-full);
        background: var(--surface-sunken);
        color: var(--text-soft);
        font-weight: 550;
        max-width: 15rem;
        cursor: pointer;
        transition: var(--transition);
        /* A <summary> is a list item in every browser but Edge legacy, and the
           marker has to go from both the standard and the WebKit sides. */
        list-style: none;
        -webkit-user-select: none;
        user-select: none;
    }
    .user-chip::-webkit-details-marker { display: none; }
    .user-chip:hover { background: var(--n-200); color: var(--text); }
    .user-chip .avatar {
        width: 24px;
        height: 24px;
        flex-shrink: 0;
        border-radius: var(--r-full);
        background: var(--n-800);
        color: #fff;
        font-size: 0.625rem;
        font-weight: 700;
        letter-spacing: 0;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        text-transform: uppercase;
    }
    .user-chip .user-name {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* --- the account menu --------------------------------------------------
       A <details> dressed as a menu. The panel is absolutely positioned off
       the chip, so opening it moves nothing else in the topbar — a menu that
       reflows the header is the tell that it is a page element pretending. */
    .usermenu { position: relative; }
    .usermenu[open] > .user-chip { background: var(--n-200); color: var(--text); }

    .usermenu-caret { color: var(--faint); transition: var(--transition); }
    .usermenu[open] .usermenu-caret { transform: rotate(180deg); }

    .usermenu-panel {
        position: absolute;
        top: calc(100% + var(--s-2));
        right: 0;
        z-index: 30;
        min-width: 15rem;
        padding: var(--s-2);
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: var(--r-lg);
        box-shadow: var(--shadow-lg);
        animation: menu-in 140ms var(--ease);
    }
    .usermenu-panel form { margin: 0; }

    @keyframes menu-in {
        from { opacity: 0; transform: translateY(-4px); }
        to   { opacity: 1; transform: none; }
    }
    @media (prefers-reduced-motion: reduce) {
        .usermenu-panel { animation: none; }
    }

    /* Who you are signed in as, above the only thing the menu does. */
    .usermenu-who {
        display: flex;
        flex-direction: column;
        gap: 0.05rem;
        padding: var(--s-2) var(--s-3) var(--s-3);
        margin-bottom: var(--s-2);
        border-bottom: 1px solid var(--border);
    }
    .usermenu-name { font-weight: 650; color: var(--text); }
    .usermenu-email {
        font-size: var(--t-sm);
        color: var(--muted);
        overflow-wrap: anywhere;
    }

    .usermenu-item {
        display: flex;
        align-items: center;
        gap: var(--s-3);
        width: 100%;
        padding: 0.5rem var(--s-3);
        border: none;
        border-radius: var(--r-md);
        background: none;
        font: inherit;
        font-weight: 550;
        color: var(--text-soft);
        text-align: left;
        cursor: pointer;
        transition: var(--transition);
    }
    /* The menu holds a link (Account) and a button (Sign out), so the rule
       above resets both and this drops the anchor's underline. */
    a.usermenu-item { text-decoration: none; box-sizing: border-box; }
    .usermenu-item:hover { background: var(--surface-sunken); color: var(--text); }
    .usermenu-item .icon { color: var(--faint); }
    .usermenu-item:hover .icon { color: var(--text-soft); }

    /* --- layout --------------------------------------------------------- */
    .shell { max-width: var(--shell-width); margin: 0 auto; padding: var(--s-8) var(--s-6) var(--s-12); }
    .centered {
        min-height: 100vh;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: var(--s-6);
        /* A sign-in page on flat gray is the plainest screen a client can be
           shown first. Two very soft accent washes give it some depth without
           becoming decoration. */
        background:
            radial-gradient(60rem 30rem at 15% -10%, var(--a-50), transparent 60%),
            radial-gradient(50rem 28rem at 90% 110%, var(--a-50), transparent 55%),
            var(--bg);
    }

    .page-head { margin-bottom: var(--s-6); }
    .page-head h1 {
        margin: 0 0 var(--s-1);
        font-size: var(--t-2xl);
        font-weight: 700;
    }
    .page-head > p:last-child { margin: 0; }

    /* A heading that carries one action — "New site" on the site list. The
       button sits on the baseline of the title on a wide screen and wraps under
       it on a phone, which is why this is a wrap rather than a fixed column:
       the title is the long part and it must not be squeezed. */
    .page-head-row {
        display: flex;
        flex-wrap: wrap;
        align-items: flex-end;
        justify-content: space-between;
        gap: var(--s-3) var(--s-4);
    }
    .page-head-row > div { margin-right: auto; }
    .page-head-row > div > p:last-child { margin: 0; }

    .card {
        background: var(--surface);
        border: 1px solid transparent;
        border-radius: var(--r-lg);
        padding: var(--s-6);
        box-shadow: var(--shadow-sm);
    }
    .card-narrow { width: 100%; max-width: 400px; box-shadow: var(--shadow-lg); padding: var(--s-8); }

    /* --- sign in / forgot / reset ------------------------------------------
       These three are the first thing a client ever sees, and the only pages
       where nobody is signed in — so the topbar is absent and the brand has to
       appear here or nowhere. */
    .auth { width: 100%; max-width: 400px; }
    .auth-brand {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: var(--s-2);
        margin-bottom: var(--s-5);
        font-weight: 700;
        font-size: var(--t-md);
        letter-spacing: var(--track-tight);
        color: var(--text);
    }
    .auth .card-narrow { max-width: none; }
    .auth-sub { margin: 0 0 var(--s-5); }
    /* Reset and the "link sent" state have no sub-heading, so the thing after
       the title has to open its own gap rather than inherit one. */
    .auth h1 + .alert, .auth h1 + form { margin-top: var(--s-5); }
    .auth-foot { margin: var(--s-5) 0 0; font-size: var(--t-sm); }
    /* The button is here because asking for a sign-in code sends an email, so
       it has to be a POST — but it reads as one more quiet option beside
       "Forgotten your password?", not as a second primary action. Same look,
       different verb. */
    .auth-foot a, .auth-foot button {
        display: inline-flex;
        align-items: center;
        gap: 0.3rem;
        color: var(--muted);
        text-decoration: none;
        font-weight: 550;
    }
    .auth-foot button {
        appearance: none;
        background: none;
        border: 0;
        padding: 0;
        font: inherit;
        font-weight: 550;
        cursor: pointer;
    }
    .auth-foot a:hover, .auth-foot button:hover { color: var(--accent); }

    /* Full-width submit: on these forms there is exactly one thing to do. */
    .btn-block { width: 100%; padding-block: 0.65rem; }
    .card h1 { margin: 0 0 var(--s-2); font-size: var(--t-xl); font-weight: 700; }

    /* Every card heading, one size. These were the browser's default h2 —
       1.5em of whatever the card sat in — which came out bigger than the page
       title on a couple of pages. Flex so a leading icon centers on the text
       instead of sitting on the baseline. The more specific rules further down
       (.card-form, .status-card, .grid-2) still win where a card wants a
       different weight of heading. */
    .card h2 {
        display: flex;
        align-items: center;
        gap: var(--s-2);
        margin: 0 0 var(--s-3);
        font-size: var(--t-lg);
        font-weight: 650;
    }
    .card h2 .icon { color: var(--faint); }
    /* The heading owns the gap to whatever follows it: a <p> brings its own
       1em top margin, which collapses to a bigger space on the cards that have
       an explanation under the heading than on the ones that don't. */
    .card h2 + p { margin-top: 0; }

    /* --- forms ---------------------------------------------------------- */
    .stack { display: flex; flex-direction: column; gap: var(--s-4); }
    .stack label,
    fieldset label {
        display: flex;
        flex-direction: column;
        gap: var(--s-2);
        font-weight: 600;
        font-size: var(--t-sm);
        color: var(--text-soft);
    }

    .stack input,
    .stack select,
    .stack textarea,
    fieldset input[type="text"],
    fieldset input[type="email"],
    fieldset input[type="time"],
    fieldset select,
    fieldset textarea,
    .inline select {
        font: inherit;
        font-size: var(--t-base);
        padding: 0.55rem 0.7rem;
        border: 1px solid var(--border-strong);
        border-radius: var(--r-md);
        background: var(--surface);
        color: var(--text);
        box-shadow: var(--shadow-xs);
        transition: var(--transition);
    }
    .stack textarea, fieldset textarea { resize: vertical; }

    /* A <select> left native was the one control on these pages that did not
       look like the others — a different height, a different border, a
       different corner on every OS. Everything above gives it the input's
       shape; this gives it the input's chevron.

       The arrow is an inline SVG in a data: URI, which the portal's
       `img-src 'self' data:` allows for a background as well as an <img>.
       style-src has nothing to say about it: this is a rule inside the
       nonced stylesheet, not an inline style attribute. The color is the
       literal --n-400, since a data: URI cannot read a custom property.

       min-height matches the 40px tap target the phone rules set for every
       other control. */
    .stack select,
    fieldset select,
    .inline select {
        appearance: none;
        -webkit-appearance: none;
        min-height: 40px;
        padding-right: 2rem;
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23a6aebc' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
        background-repeat: no-repeat;
        background-position: right 0.65rem center;
        background-size: 16px 16px;
    }
    /* Firefox draws a dotted focus ring inside the box on top of ours. */
    .stack select:-moz-focusring,
    fieldset select:-moz-focusring { color: transparent; text-shadow: 0 0 0 var(--text); }

    .stack input:hover,
    .stack select:hover,
    .stack textarea:hover,
    fieldset input:hover,
    fieldset select:hover,
    fieldset textarea:hover { border-color: var(--n-400); }

    .stack input:focus,
    .stack select:focus,
    .stack textarea:focus,
    fieldset input:focus,
    fieldset select:focus,
    fieldset textarea:focus {
        outline: none;
        border-color: var(--a-400);
        box-shadow: var(--focus-ring);
    }

    .stack input::placeholder, .stack textarea::placeholder { color: var(--n-400); }

    .hint { font-weight: 400; font-size: var(--t-sm); color: var(--muted); letter-spacing: 0; }

    /* A heading over a run of fields that belong together — Words,
       Background, Lead form. Quiet: it is a signpost inside a form, not a
       second heading competing with the card's own. The first one takes no
       top margin, so a form that opens with a group does not start with a
       gap. */
    .field-group {
        margin: var(--s-4) 0 calc(var(--s-2) * -1);
        padding-top: var(--s-4);
        border-top: 1px solid var(--border);
        font-size: var(--t-xs);
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: var(--track-wide);
        color: var(--faint);
    }
    /* The first group opens the form; a rule and a gap above it would be a
       line under the card's own heading. */
    .field-group:first-of-type { margin-top: 0; padding-top: 0; border-top: none; }

    /* A yes/no field. .hours-closed already lays a checkbox out beside its
       words; this only takes back the `margin-left: auto` that pushes it to
       the end of an hours row, and gives the hint under it the indent that
       ties the two together. */
    .toggle-field { margin-left: 0 !important; }
    .toggle-hint { margin-top: calc(var(--s-2) * -1); padding-left: 1.6rem; }

    /* --- the page-text field ------------------------------------------------
       A `rich` field (_rich_editor.twig): a label, a toolbar, the textarea,
       and a preview of what the page will do with it. The toolbar and the
       preview are hidden in the markup and unhidden by rich.js, so with the
       script gone this is the plain labelled textarea it used to be — which
       is why the parts below are laid out as one stack rather than positioned
       against each other.
       -------------------------------------------------------------------- */
    .rich-field { display: flex; flex-direction: column; gap: var(--s-2); }
    .rich-label { font-size: var(--t-sm); font-weight: 600; letter-spacing: var(--track-snug); }
    .rich-toolbar { display: flex; flex-wrap: wrap; gap: var(--s-2); }
    /* 40px, the tap target the phone rules give every other control — this
       one is used with a thumb more than the rest of the form is. */
    .rich-toolbar .btn-small { min-height: 40px; }
    .rich-field textarea {
        /* rich.js grows it to fit; this is the floor, and the ceiling stops a
           2,000-word page from burying the Save button. */
        min-height: 12rem;
        max-height: 70vh;
        resize: vertical;
        line-height: 1.6;
    }
    .rich-preview-label {
        margin: var(--s-2) 0 0;
        font-size: var(--t-xs);
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: var(--track-wide);
        color: var(--faint);
    }
    .rich-preview {
        padding: var(--s-4);
        border: 1px solid var(--border);
        border-radius: var(--r-md);
        background: var(--surface-sunken);
        min-height: 3rem;
    }
    /* The same type rules public/assets/site.css gives `.rich`, in this
       stylesheet's tokens: the preview is only worth having if what it shows
       is what the page will do. Change one, change the other. */
    .rich h2 { font-size: 1.15rem; font-weight: 700; margin: 1.5rem 0 0.6rem; }
    .rich h3 { font-size: 1rem; font-weight: 700; margin: 1.1rem 0 0.4rem; }
    .rich p { line-height: 1.6; margin: 0 0 0.9rem; }
    .rich ul, .rich ol { margin: 0 0 0.9rem; padding-left: 1.25rem; line-height: 1.6; }
    .rich li { margin-bottom: 0.35rem; }
    .rich a { color: var(--accent); text-decoration: underline; }
    .rich > :first-child { margin-top: 0; }
    .rich > :last-child { margin-bottom: 0; }

    .btn {
        font: inherit;
        font-size: var(--t-base);
        font-weight: 600;
        letter-spacing: var(--track-snug);
        padding: 0.55rem 1rem;
        border-radius: var(--r-md);
        border: 1px solid transparent;
        cursor: pointer;
        text-decoration: none;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: var(--s-2);
        transition: var(--transition);
        white-space: nowrap;
    }
    .btn:active { transform: translateY(0.5px); }

    /* Graphite, not the accent. See the token block. */
    .btn-primary {
        background: var(--btn-primary-bg);
        color: var(--btn-primary-text);
        box-shadow: var(--shadow-sm);
    }
    .btn-primary:hover { background: var(--btn-primary-bg-hover); box-shadow: var(--shadow-md); color: #fff; }

    .btn-small {
        padding: 0.32rem 0.7rem;
        font-size: var(--t-sm);
        font-weight: 600;
        border: 1px solid var(--border-strong);
        background: var(--surface);
        color: var(--text-soft);
        box-shadow: var(--shadow-xs);
    }
    .btn-small:hover { border-color: var(--n-400); color: var(--text); background: var(--n-25); }
    .btn-on { background: var(--ok-bg); color: var(--ok-text); border-color: var(--ok-border); }
    .btn-on:hover { background: var(--ok-bg); color: var(--ok-text); border-color: var(--ok-text); }
    .btn-quiet { background: transparent; color: var(--muted); border-color: var(--border-strong); }

    .alert {
        display: flex;
        align-items: flex-start;
        gap: var(--s-3);
        padding: 0.7rem 0.9rem;
        border-radius: var(--r-md);
        border: 1px solid transparent;
        margin: 0 0 var(--s-4);
        font-size: var(--t-base);
    }
    .alert .icon { flex-shrink: 0; margin-top: 0.15rem; }
    .alert-error { background: var(--err-bg); border-color: var(--err-border); color: var(--err-text); }
    .alert-ok    { background: var(--ok-bg);  border-color: var(--ok-border);  color: var(--ok-text); }
    .alert-warn  { background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-text); }
    /* For a settled outcome that is neither good news nor bad — a lead you
       marked as spam weeks ago. Red would re-litigate a decision already
       made. */
    .alert-quiet { background: var(--surface-sunken); border-color: var(--border); color: var(--text-soft); }
    .alert code { background: rgba(255, 255, 255, 0.55); border-color: currentColor; color: inherit; }

    /* --- icons ------------------------------------------------------------
       One inline <symbol> sprite in the layout, referenced with <use>. No
       requests, no build step, and CSP-clean — an icon font or a sprite URL
       would each need something the portal's CSP does not grant. Stroke
       inherits currentColor so an icon is always the color of its text. */
    /* The sprite itself: present in the DOM, occupying nothing. Not
       `display: none` — Safari has historically dropped <use> references into
       a fully hidden sprite. */
    .sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

    .icon {
        width: 1em;
        height: 1em;
        flex-shrink: 0;
        fill: none;
        stroke: currentColor;
        stroke-width: 1.75;
        stroke-linecap: round;
        stroke-linejoin: round;
        vertical-align: -0.135em;
    }
    .icon-lg { width: 1.25em; height: 1.25em; }

    /* --- site list ------------------------------------------------------- */
    .site-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); }
    .site-card {
        display: flex;
        align-items: center;
        gap: var(--s-4);
        padding: var(--s-4) var(--s-5);
        transition: var(--transition);
    }
    .site-card:hover { box-shadow: var(--shadow-md); border-color: var(--border); transform: translateY(-1px); }
    .site-main { min-width: 0; }
    .site-main h2 { margin: 0 0 0.1rem; font-size: var(--t-md); font-weight: 650; }
    .site-main h2 a { text-decoration: none; color: var(--text); }
    .site-main h2 a:hover { color: var(--accent); }
    .site-main p { margin: 0; overflow-wrap: anywhere; }
    .site-url {
        display: inline-flex;
        align-items: center;
        gap: 0.3rem;
        font-size: var(--t-sm);
        color: var(--muted);
        text-decoration: none;
    }
    .site-url:hover { color: var(--accent); }
    .site-meta { margin-left: auto; display: flex; align-items: center; gap: var(--s-2); flex-shrink: 0; }

    /* The figures under each site's URL. One line at desktop widths — that is
       the whole design constraint, since a stat block per card turns a list of
       twenty sites into a page nobody scans. It wraps rather than overflows on
       a phone, where a second line is the lesser evil. */
    .site-stats {
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
        column-gap: var(--s-4);
        row-gap: 0.15rem;
        margin: 0.3rem 0 0;
        font-size: var(--t-sm);
        color: var(--muted);
    }
    .site-stats a { color: var(--muted); text-decoration: none; }
    .site-stats a:hover { color: var(--accent); }
    .site-stats strong { color: var(--text); font-weight: 650; font-variant-numeric: tabular-nums; }
    /* Amber, matching .badge-spam: leads waiting on a decision are a nudge,
       not a failure. */
    .site-stats a.site-stats-waiting { color: var(--warn-text); font-weight: 600; }
    .site-stats a.site-stats-waiting:hover { color: var(--warn-text); text-decoration: underline; }

    .badge {
        display: inline-flex;
        align-items: center;
        gap: 0.3rem;
        padding: 0.16rem 0.55rem;
        border-radius: var(--r-full);
        border: 1px solid var(--border);
        background: var(--surface-sunken);
        color: var(--muted);
        font-size: var(--t-xs);
        font-weight: 650;
        letter-spacing: var(--track-wide);
        text-transform: uppercase;
        white-space: nowrap;
    }
    /* A status dot inside the badge — reads faster than the word alone at a
       glance down a list of sites. */
    .badge::before {
        content: "";
        width: 5px;
        height: 5px;
        border-radius: var(--r-full);
        background: currentColor;
        opacity: 0.7;
    }
    .badge-plain::before { display: none; }
    .badge-live    { background: var(--ok-bg);   border-color: var(--ok-border);   color: var(--ok-text); }
    .badge-expired { background: var(--err-bg);  border-color: var(--err-border);  color: var(--err-text); }
    /* Same red as expired: both mean "not serving". The word is what tells
       them apart, and they are never both on one row. */
    .badge-suspended { background: var(--err-bg); border-color: var(--err-border); color: var(--err-text); }
    .badge-preview { background: var(--a-50);    border-color: var(--a-200);       color: var(--a-800); }
    .badge-draft   { background: var(--surface-sunken); border-color: var(--border); color: var(--muted); }
    /* Amber, not red: a held lead is a suspicion to check, not a failure. It
       shares the palette of .alert-warn for that reason. */
    .badge-spam { background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-text); }
    /* A desktop push waiting on the owner (site list). Same amber, same reason. */
    .badge-warn { background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-text); }
    a.badge-warn { text-decoration: none; }
    a.badge-warn:hover { filter: brightness(0.95); }

    /* Client intake. Deliberately reuses the site palette rather than adding a
       second vocabulary of colours: a reader who has learned the site list can
       read this board without learning anything new. Draft is the same muted
       chip in both. */
    .badge-ready    { background: var(--a-50);   border-color: var(--a-200);   color: var(--a-800); }
    /* Amber like .badge-spam: in progress, nothing wrong, but not finished. */
    .badge-building { background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-text); }
    .badge-built    { background: var(--ok-bg);  border-color: var(--ok-border);  color: var(--ok-text); }
    .badge-failed   { background: var(--err-bg); border-color: var(--err-border); color: var(--err-text); }

    /* A single action sitting between the page heading and the list below it. */
    .page-actions { margin: 0 0 var(--s-5); }

    /* A heading with a badge on the same line.
       Same fix, and the same reason, as .card h2 above: a .badge is inline-flex
       whose first flex item is an empty 5px dot, so the box takes its baseline
       from its own bottom edge and sits visibly low next to 1.75rem type.
       Making the heading a flex row centers it instead of dropping it on a
       baseline it does not really have. */
    .heading-badged {
        display: flex;
        align-items: center;
        gap: var(--s-3);
        flex-wrap: wrap;
    }

    /* --- trade combobox ---------------------------------------------------
       Progressive enhancement over ONE plain text input. portal.js wraps that
       input in .combo and hangs .combo-list beneath it; with JS off none of
       this is ever rendered and the input works on its own with its datalist.
       Nothing is hidden, so nothing can fail to hide. */
    .combo { position: relative; }
    .combo-input { width: 100%; }
    .combo-list {
        position: absolute;
        z-index: 20;
        top: calc(100% + 4px);
        left: 0;
        right: 0;
        max-height: 16rem;
        overflow-y: auto;
        margin: 0;
        padding: var(--s-1) 0;
        list-style: none;
        background: var(--surface);
        border: 1px solid var(--border-strong);
        border-radius: var(--r-md);
        box-shadow: var(--shadow-lg);
    }
    .combo-list[hidden] { display: none; }
    .combo-option {
        display: flex;
        align-items: baseline;
        gap: var(--s-2);
        padding: 0.4rem 0.7rem;
        font-size: var(--t-base);
        color: var(--text);
        cursor: pointer;
    }
    /* One highlight class driven by the keyboard, rather than :hover alone, so
       arrow keys and the mouse cannot disagree about what Enter will pick. */
    .combo-option.is-active,
    .combo-option:hover { background: var(--surface-sunken); }
    .combo-option .combo-slug { color: var(--muted); font-size: var(--t-sm); }
    .combo-new { color: var(--accent); font-weight: 600; }
    .combo-empty { padding: 0.4rem 0.7rem; color: var(--muted); font-size: var(--t-sm); }

    /* Uploaded intake photos. A plain wrapping grid: these are working
       reference shots, not a gallery, so they want to be scannable at a glance
       rather than composed. */
    .intake-photos {
        list-style: none;
        margin: 0 0 var(--s-5);
        padding: 0;
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
        gap: var(--s-3);
    }
    .intake-photos li { position: relative; }
    .intake-photos img {
        display: block;
        width: 100%;
        height: 120px;
        object-fit: cover;
        border-radius: var(--r-md);
        border: 1px solid var(--border);
        background: var(--surface-sunken);
    }
    /* Sits on the image rather than below it, so a row of photos stays a row of
       photos. Always visible, not hover-only: hover does not exist on the phone
       somebody will inevitably do this from. */
    .intake-photos .btn-remove {
        position: absolute;
        top: 4px;
        right: 4px;
        width: 22px;
        height: 22px;
        line-height: 1;
        padding: 0;
        border: 1px solid var(--border);
        border-radius: var(--r-full);
        background: var(--surface);
        color: var(--muted);
        cursor: pointer;
        font-size: var(--t-base);
    }
    .intake-photos .btn-remove:hover { color: var(--err-text); border-color: var(--err-border); }

    /* The photo plan sits at the foot of the photos panel, under a rule: it is
       a decision about the photos above it rather than another way to add them,
       and without the separation it reads as part of the upload form. */
    .photo-plan {
        margin-top: var(--s-6);
        padding-top: var(--s-5);
        border-top: 1px solid var(--border);
    }
    .photo-plan h3 {
        display: flex;
        align-items: center;
        gap: var(--s-2);
        margin: 0 0 var(--s-2);
        font-size: var(--t-base);
        font-weight: 650;
    }
    .photo-plan h3 .icon { color: var(--faint); }
    .photo-plan .lede { margin-top: 0; }
    /* A disabled radio still has to read as the chosen one on a locked intake:
       the plan is what the build is acting on right now, and greying it into
       illegibility hides the single fact somebody opened the panel to check. */
    .photo-plan .choice-option input:disabled { opacity: 0.6; }

    /* The build's own account of what it did. Monospace and preserved line
       breaks because it is written as a file — INVENTED and CLAIMS blocks,
       lists, sometimes a verification transcript — and reflowing it as prose
       destroys the structure that makes it skimmable. */
    .build-notes pre {
        margin: 0;
        padding: var(--s-4);
        background: var(--surface-sunken);
        border: 1px solid var(--border);
        border-radius: var(--r-md);
        font-size: var(--t-sm);
        line-height: 1.55;
        white-space: pre-wrap;
        /* Long words wrap rather than pushing the card sideways: a note can
           carry a URL or a 16-hex hash, and one of those must not be able to
           give the whole page a horizontal scrollbar. */
        overflow-wrap: anywhere;
    }

    /* --- one site --------------------------------------------------------- */
    .crumb { margin: 0 0 var(--s-2); font-size: var(--t-sm); }
    .crumb a {
        display: inline-flex;
        align-items: center;
        gap: 0.3rem;
        text-decoration: none;
        color: var(--muted);
        font-weight: 550;
    }
    .crumb a:hover { color: var(--text); }

    .site-head h1 { margin: 0 0 var(--s-2); font-size: var(--t-2xl); font-weight: 700; }
    .site-head .muted { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }

    .subnav {
        display: flex;
        gap: var(--s-1);
        padding: var(--s-1);
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: var(--r-lg);
        box-shadow: var(--shadow-xs);
        margin-bottom: var(--s-6);
        overflow-x: auto;
        scrollbar-width: thin;
    }
    .subnav a {
        display: inline-flex;
        align-items: center;
        gap: var(--s-2);
        padding: 0.45rem 0.75rem;
        border-radius: var(--r-md);
        text-decoration: none;
        color: var(--muted);
        font-weight: 550;
        font-size: var(--t-base);
        white-space: nowrap;
        transition: var(--transition);
    }
    .subnav a:hover { color: var(--text); background: var(--surface-sunken); }
    /* Active is a filled pill, not a 2px underline hanging off a hairline
       border. Same information, and it survives the horizontal scroll on a
       phone where a bottom border does not. */
    .subnav a.active {
        color: var(--text);
        background: var(--surface-sunken);
        box-shadow: inset 0 0 0 1px var(--border);
    }
    .subnav a.active .icon { color: var(--accent); }

    .lede { margin: 0 0 var(--s-5); max-width: 62ch; color: var(--text-soft); }
    .strong { font-weight: 650; margin: 0 0 var(--s-1); }

    .grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--s-4); }
    .grid-2 .card { display: flex; flex-direction: column; transition: var(--transition); }
    .grid-2 .card:hover { box-shadow: var(--shadow-md); }
    .grid-2 .card h2 {
        display: flex;
        align-items: center;
        gap: var(--s-2);
        margin: 0 0 var(--s-3);
        font-size: var(--t-md);
        font-weight: 650;
    }
    .grid-2 .card h2 .icon { color: var(--faint); }
    .grid-2 .card p { margin: 0 0 var(--s-2); }
    .grid-2 .card p:last-child { margin-bottom: 0; }
    /* The card's link sits on the floor of the card so a row of cards of
       different heights still has its actions on one line. */
    .card-action { margin-top: auto; padding-top: var(--s-3); }
    .card-action a {
        display: inline-flex;
        align-items: center;
        gap: 0.3rem;
        font-weight: 600;
        font-size: var(--t-sm);
        text-decoration: none;
    }
    .card-action a:hover { gap: 0.5rem; }

    /* --- empty states ------------------------------------------------------
       A gray sentence in a box is what the portal used to say when there was
       nothing to show. An icon, a line in the text color and a quiet
       explanation underneath reads as a state rather than a failure. */
    .empty {
        display: flex;
        flex-direction: column;
        align-items: center;
        text-align: center;
        gap: var(--s-2);
        padding: var(--s-10) var(--s-6);
        color: var(--muted);
    }
    .empty .icon {
        width: 2rem;
        height: 2rem;
        color: var(--n-400);
        stroke-width: 1.5;
        margin-bottom: var(--s-1);
    }
    .empty-title { margin: 0; font-size: var(--t-md); font-weight: 650; color: var(--text); }
    .empty p { margin: 0; max-width: 44ch; font-size: var(--t-base); }
    /* Inside a card that already has a heading and an explanation above it,
       the full-height version leaves the card mostly air. */
    .empty-tight { padding: var(--s-6) var(--s-4); }

    /* --- homepage blocks --------------------------------------------------- */
    .section-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-2); }
    .section-row {
        display: flex;
        align-items: center;
        gap: var(--s-4);
        padding: var(--s-3) var(--s-4);
        transition: var(--transition);
    }
    .section-row:hover { box-shadow: var(--shadow-md); }
    /* Hidden blocks stay legible rather than being grayed into unreadability —
       you have to be able to find one to switch it back on. */
    .section-row.is-off { background: var(--n-25); box-shadow: var(--shadow-xs); }
    .section-row.is-off .section-main h2 { color: var(--muted); }

    .section-order { display: flex; flex-direction: column; gap: 2px; align-self: center; }
    .section-order form { margin: 0; line-height: 0; }

    /* The Header and Footer rows: pinned above and below the sortable list, in
       lists of their own so the sortable one holds only what may be dragged.
       A list's `gap` does not reach across two lists, so the seam between them
       is set here instead — without this the pinned rows sit tighter against
       their neighbors than every other pair on the page, which is exactly how
       the old fixed Lead form row looked wrong.

       Both margins on both lists rather than a top one and a bottom one: it
       is the same gap either way, and it does not depend on which list is
       first, which is one fewer thing to get wrong the next time a row is
       added to this page. */
    .section-list-pinned { margin: var(--s-2) 0; }
    /* Stands in for the grip so the titles line up down the whole page. The
       row does not drag, and drawing a handle that does nothing would be a
       worse answer than an empty column. */
    .sort-spacer { display: block; width: 36px; height: 36px; }

    /* "Add a section" is the last row of the list rather than a control above
       it, because that is where the section it adds lands. Dashed and
       shadowless: the one row that is not yet a section on the page.

       Its own `display` has to beat .section-row's flex — a <details> that is
       a flex container lays its summary and its contents out side by side. */
    .add-row {
        display: block;
        margin-top: var(--s-2);
        border: 2px dashed var(--n-300);
        box-shadow: none;
    }
    .add-row:hover { box-shadow: none; }
    .add-row > summary {
        display: flex;
        align-items: center;
        gap: var(--s-4);
        cursor: pointer;
        list-style: none;
    }
    .add-row > summary::-webkit-details-marker { display: none; }
    /* The grip's column, so the title lines up with the titles above it. */
    .add-row-plus {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        flex: 0 0 36px;
        color: var(--muted);
    }
    .add-row[open] .add-row-plus { color: var(--accent); }
    .add-row-main { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
    .add-row-title { font-size: var(--t-base); font-weight: 650; }
    /* One chip per type, all of them readable at once instead of one at a time
       behind a menu. They wrap onto as many lines as they need on a phone,
       where 40px is what makes each one a thumb target. */
    .add-row-options {
        display: flex;
        flex-wrap: wrap;
        gap: var(--s-2);
        margin-top: var(--s-3);
        padding-left: calc(36px + var(--s-4));
    }
    .add-row-options form { margin: 0; }
    .add-row-options .btn { min-height: 40px; }
    .add-row-done { padding-left: calc(36px + var(--s-4) + var(--s-4)); }
    .block-list-head { margin-top: var(--s-6); }
    .website-nav { display: flex; flex-wrap: wrap; gap: var(--s-2); margin: calc(-1 * var(--s-2)) 0 var(--s-6); border-bottom: 1px solid var(--border); padding-bottom: var(--s-3); }
    .website-nav a { padding: var(--s-2) var(--s-3); color: var(--muted); text-decoration: none; border-radius: var(--r-sm); }
    .website-nav a.active { color: var(--accent); background: var(--surface); font-weight: 650; }
    .website-group-heading { margin-top: var(--s-6); }
    .website-card { display: block; color: inherit; text-decoration: none; }
    .website-card h3 { margin-top: 0; }
    .website-card:hover { border-color: var(--accent); }
    .dashboard-welcome { margin-bottom: var(--s-5); }
    .dashboard-welcome h2 { margin-top: 0; }
    .menu-link-row { padding: var(--s-3) 0; border-bottom: 1px solid var(--border); display: grid; grid-template-columns: 36px minmax(0, 1fr) minmax(0, 1fr) 40px; gap: var(--s-3); align-items: center; }
    .menu-link-row > label { min-width: 0; margin: 0; }
    .menu-link-row > .row-menu { justify-self: end; }
    @media (max-width: 640px) {
        .menu-link-row { grid-template-columns: 36px minmax(0, 1fr) 40px; gap: var(--s-2); }
        .menu-link-row > .sort-handle { grid-column: 1; grid-row: 1 / 3; }
        .menu-link-row > label { grid-column: 2; }
        .menu-link-row > .row-menu { grid-column: 3; grid-row: 1 / 3; }
    }
    .pairs { border: 0; padding: 0; margin: 0; display: grid; gap: var(--s-2); }
    .pairs legend { padding: 0; margin-bottom: var(--s-1); }
    /* A fieldset standing in for one field — the pairs rows, a picture picker
       — is labelled by its legend, so that legend is a field label and has to
       look like the <label>s around it. The base rule below is micro-caps,
       which is right for a card's section caption and wrong for this: it made
       "Background image" shout beside a plain "Headline", and dressed the
       pairs legend exactly like the .field-group heading above it. */
    .pairs legend,
    .pick-grid-wrap > legend {
        font-weight: 600;
        font-size: var(--t-sm);
        text-transform: none;
        letter-spacing: 0;
        color: var(--text-soft);
    }
    .pairs-row input { min-width: 0; }

    /* --- drag to reorder (sortable.js) ------------------------------------
       The grip starts a drag at once; so does a mouse pressed anywhere else
       on the item once it has moved a few pixels. On a phone the whole item
       does, with a press and hold. The ghost is a clone of the item kept
       inside the list (so it keeps the list's styling) and fixed to the
       viewport; the selector is two classes deep so it wins over
       .photo-tile's own position. */
    .sort-handle {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 36px;
        height: 36px;
        padding: 0;
        border: 1px solid var(--border-strong);
        border-radius: var(--r-md);
        background: var(--surface);
        color: var(--muted);
        cursor: grab;
        touch-action: none;
        user-select: none;
        -webkit-user-select: none;
        transition: var(--transition);
    }
    .sort-handle:hover { color: var(--text); border-color: var(--n-400); }
    .sort-handle .icon { width: 18px; height: 18px; }
    .is-sorting, .is-sorting * { cursor: grabbing !important; }
    body.is-dragging { user-select: none; -webkit-user-select: none; }
    .is-sortable > .sort-ghost {
        position: fixed;
        top: 0;
        left: 0;
        z-index: 50;
        margin: 0;
        box-sizing: border-box;
        list-style: none;
        pointer-events: none;
        opacity: 0.96;
        box-shadow: 0 12px 32px rgba(15, 23, 42, 0.22);
        transition: none;
        will-change: transform;
        transform: translate(-9999px, -9999px);
    }
    .is-placeholder { opacity: 0.3; }
    .is-placeholder.card, .is-placeholder .photo-tile-link { box-shadow: none; outline: 2px dashed var(--n-300); outline-offset: -2px; }
    .is-saving { opacity: 0.7; }
    .sr-only {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
        border: 0;
    }

    /* --- the Photos page --------------------------------------------------- */
    .photo-grid {
        list-style: none;
        margin: 0 0 var(--s-6);
        padding: 0;
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
        gap: var(--s-3);
    }
    .photo-tile { position: relative; margin: 0; }
    .photo-tile-link {
        display: block;
        border-radius: var(--r-md);
        overflow: hidden;
        background: var(--surface-sunken);
        box-shadow: var(--shadow-xs);
    }
    .photo-thumb {
        display: block;
        width: 100%;
        aspect-ratio: 4 / 3;
        object-fit: cover;
    }
    .photo-thumb-missing { display: flex; align-items: center; justify-content: center; }
    .photo-tile.is-off .photo-thumb { opacity: 0.45; filter: grayscale(0.6); }
    .photo-tile .sort-handle {
        position: absolute;
        top: 6px;
        left: 6px;
        width: 32px;
        height: 32px;
        background: rgba(255, 255, 255, 0.92);
    }
    .photos .upload-card h2 { margin-top: 0; }

    .details-card > summary {
        display: flex;
        align-items: center;
        gap: var(--s-2);
        cursor: pointer;
        font-weight: 650;
        list-style: none;
    }
    .details-card > summary::-webkit-details-marker { display: none; }
    .details-card > summary::before { content: "\25B8"; color: var(--muted); }
    .details-card[open] > summary::before { content: "\25BE"; }
    .details-card > summary .badge { margin-left: auto; }
    .details-card[open] > summary { margin-bottom: var(--s-4); }

    /* Overview: a tile that is a link, and the compact recent-changes list. */
    .tile-link { display: flex; flex-direction: column; gap: 0.1rem; color: inherit; text-decoration: none; }
    .tile-link:hover .tile-label { color: var(--accent); }
    .history-compact .history-row { padding: var(--s-2) 0; box-shadow: none; border: 0; border-bottom: 1px solid var(--border); border-radius: 0; }
    .history-compact .history-row:last-child { border-bottom: 0; }

    .photo-page { display: grid; gap: var(--s-4); max-width: 720px; }
    .photo-preview img { display: block; width: 100%; height: auto; border-radius: var(--r-md); }
    .photo-preview.is-off img { opacity: 0.5; filter: grayscale(0.6); }
    .photo-actions { display: grid; gap: var(--s-2); }
    .photo-actions form { margin: 0; }
    .photo-file { margin: var(--s-3) 0 0; }
    .iconbtn {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 26px;
        height: 20px;
        border: 1px solid var(--border-strong);
        background: var(--surface);
        border-radius: var(--r-sm);
        cursor: pointer;
        color: var(--muted);
        line-height: 1;
        padding: 0;
        transition: var(--transition);
    }
    /* A fixed size rather than the usual 1em: this button has no text for an
       icon to be the size of. */
    .iconbtn .icon { width: 14px; height: 14px; vertical-align: 0; }
    .iconbtn:hover:not(:disabled) { color: var(--text); border-color: var(--n-400); background: var(--n-25); }
    .iconbtn:disabled { opacity: 0.3; cursor: default; }

    .section-main { flex: 1; min-width: 0; }
    /* .heading-badged, so a count pill sits beside the name rather than on a
       baseline it does not have; its gap is tuned for a page heading and is
       too wide next to type this size. */
    .section-main h2 { margin: 0; font-size: var(--t-base); font-weight: 650; gap: var(--s-2); }
    .section-main h2 a { text-decoration: none; color: var(--text); }
    .section-main h2 a:hover { color: var(--accent); }
    .section-main p { margin: 0.15rem 0 0; }

    .section-controls { display: flex; align-items: center; gap: var(--s-2); flex-shrink: 0; }
    .row-menu { position: relative; }
    .row-menu > summary { list-style: none; min-width: 40px; min-height: 40px; justify-content: center; }
    .row-menu > summary::-webkit-details-marker { display: none; }
    .row-menu-panel {
        display: grid;
        gap: 4px;
        min-width: 205px;
        position: absolute;
        right: 0;
        top: calc(100% + 4px);
        z-index: 10;
        padding: var(--s-2);
        background: var(--surface);
        border: 1px solid var(--border-strong);
        border-radius: var(--r-sm);
        box-shadow: 0 4px 12px rgb(0 0 0 / 12%);
    }
    .row-menu-item {
        display: block;
        width: 100%;
        padding: 8px 12px;
        border: 0;
        border-radius: 4px;
        background: transparent;
        color: var(--text);
        font: inherit;
        font-size: var(--t-sm);
        font-weight: 400;
        text-align: left;
        text-decoration: none;
        cursor: pointer;
        box-shadow: none;
        white-space: nowrap;
    }
    .row-menu-item:hover, .row-menu-item:focus-visible {
        background: var(--bg);
        color: var(--text);
        text-decoration: none;
    }
    .inline { margin: 0; display: inline-flex; gap: var(--s-1); }
    .inline select { font-size: var(--t-sm); padding: 0.3rem 0.45rem; }

    /* --- wider forms (theme, business) ------------------------------------- */
    .card-form { max-width: 660px; }
    .card-form h2 { margin: 0 0 var(--s-5); font-size: var(--t-lg); font-weight: 700; }
    .card-wide { max-width: 780px; }

    /* Wraps: the lead detail page puts "Send to <address>" and "Mark as spam"
       in here, and the two squash together on a narrow phone otherwise. */
    .form-actions { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
    .form-actions[hidden] { display: none; }

    fieldset {
        border: none;
        border-top: 1px solid var(--border);
        padding: var(--s-5) 0 0;
        margin: var(--s-2) 0 0;
        display: flex;
        flex-direction: column;
        gap: var(--s-4);
    }
    fieldset:first-of-type { border-top: none; padding-top: 0; }
    legend {
        display: flex;
        align-items: center;
        gap: var(--s-2);
        font-weight: 700;
        font-size: var(--t-xs);
        text-transform: uppercase;
        letter-spacing: var(--track-wide);
        color: var(--faint);
        padding: 0;
    }
    /* Bigger than the 1em the label sets, because the label is 12px and an
       icon that small is a smudge. */
    legend .icon { width: 1.25rem; height: 1.25rem; stroke-width: 1.6; }

    .row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-4); }
    .row-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s-4); }

    /* --- hours ------------------------------------------------------------- */
    .hours { display: flex; flex-direction: column; gap: var(--s-2); }
    .hours-row { display: flex; align-items: center; gap: var(--s-3); }
    .hours-day { width: 5.5rem; font-weight: 600; font-size: var(--t-sm); flex-shrink: 0; }
    .hours-sep { color: var(--faint); font-size: var(--t-sm); }
    .hours-closed {
        display: flex !important;
        flex-direction: row !important;
        align-items: center;
        gap: var(--s-1);
        margin-left: auto;
        font-weight: 400 !important;
        color: var(--muted);
    }
    .hours-closed input { width: auto; }

    /* --- theme ------------------------------------------------------------- */
    .theme-row {
        display: grid;
        grid-template-columns: 1fr 1fr;
        align-items: center;
        gap: var(--s-2) var(--s-4);
        padding: var(--s-3) 0;
        border-bottom: 1px solid var(--border);
    }
    .theme-row:last-child { border-bottom: none; }
    .theme-row > label { font-weight: 600; font-size: var(--t-sm); display: flex; align-items: center; gap: var(--s-2); }
    .theme-input { display: flex; align-items: center; gap: var(--s-2); }
    .theme-input input[type="text"] {
        flex: 1;
        font-family: var(--font-mono);
        font-size: var(--t-sm);
        padding: 0.45rem 0.55rem;
        border: 1px solid var(--border-strong);
        border-radius: var(--r-md);
        background: var(--surface);
        color: var(--text);
        transition: var(--transition);
    }
    .swatch-picker,
    .swatch {
        width: 34px;
        height: 32px;
        border: 1px solid var(--border-strong);
        border-radius: var(--r-md);
        padding: 0;
        flex-shrink: 0;
        cursor: pointer;
        box-shadow: var(--shadow-xs);
        transition: var(--transition);
    }
    .swatch-picker:hover { border-color: var(--n-400); box-shadow: var(--shadow-sm); }
    .swatch { cursor: default; display: inline-block; }
    .theme-default {
        grid-column: 2;
        margin: 0;
        font-size: var(--t-xs);
        color: var(--faint);
        font-family: var(--font-mono);
    }
    .pill {
        display: inline-flex;
        align-items: center;
        font-size: var(--t-xs);
        font-weight: 650;
        text-transform: uppercase;
        letter-spacing: var(--track-wide);
        background: var(--ok-bg);
        border: 1px solid var(--ok-border);
        color: var(--ok-text);
        padding: 0.05rem 0.4rem;
        border-radius: var(--r-full);
    }

    .advanced { margin: var(--s-4) 0; }
    .advanced summary {
        cursor: pointer;
        font-weight: 600;
        font-size: var(--t-base);
        padding: var(--s-2) 0;
        color: var(--text-soft);
        transition: var(--transition);
    }
    .advanced summary:hover { color: var(--accent); }

    /* --- site status ------------------------------------------------------- */
    .status-card { margin-bottom: var(--s-4); }

    /* The push review page (site/push_review.twig) and the overview's
       "a push is waiting" card. */
    .push-card { border-color: var(--warn-border); }
    .push-notes {
        white-space: pre-wrap;
        font-size: var(--t-sm);
        background: var(--surface-sunken);
        border: 1px solid var(--border);
        border-radius: var(--r-1);
        padding: var(--s-3);
        max-height: 24rem;
        overflow: auto;
    }
    .push-list { list-style: none; padding: 0; margin: 0 0 var(--s-3); }
    .push-list li { padding: 0.25rem 0; border-bottom: 1px solid var(--border); }
    .push-list li:last-child { border-bottom: 0; }
    .push-table-wrap { overflow-x: auto; margin-bottom: var(--s-3); }
    .push-table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); }
    .push-table th, .push-table td { text-align: left; padding: 0.4rem 0.6rem; border-bottom: 1px solid var(--border); vertical-align: top; }
    .push-actions { display: flex; flex-wrap: wrap; gap: var(--s-3); align-items: center; }
    .status-card h2 {
        display: flex;
        align-items: center;
        gap: var(--s-2);
        margin: 0 0 var(--s-4);
        font-size: var(--t-md);
        font-weight: 650;
    }
    /* A labelled radio stack: one choice per row, the current one outlined.
       .choice-* is the general name and .status-* is kept beside it because the
       site status panel was here first and its markup says so; both are the same
       control, so a second page reaching for it (the intake photo plan) gets a
       class that describes the shape rather than borrowing one that describes
       somebody else's subject. */
    .status-form,
    .choice-form { display: flex; flex-direction: column; gap: var(--s-2); align-items: flex-start; }
    .status-option,
    .choice-option {
        display: flex;
        align-items: flex-start;
        gap: var(--s-3);
        padding: 0.65rem 0.8rem;
        border: 1px solid var(--border);
        border-radius: var(--r-md);
        cursor: pointer;
        width: 100%;
        font-size: var(--t-base);
        background: var(--surface);
        transition: var(--transition);
    }
    .status-option:hover,
    .choice-option:hover { border-color: var(--n-400); background: var(--n-25); }
    .status-option.is-current,
    .choice-option.is-current {
        border-color: var(--a-400);
        background: var(--a-50);
        box-shadow: 0 0 0 1px var(--a-400);
    }
    .status-option span,
    .choice-option span { display: flex; flex-direction: column; gap: 0.05rem; }
    .status-option strong,
    .choice-option strong { font-weight: 650; }
    .status-option input,
    .choice-option input { margin-top: 0.2rem; accent-color: var(--accent); }
    .status-form .btn,
    .choice-form .btn { margin-top: var(--s-2); }

    /* --- history ----------------------------------------------------------- */
    .history { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-2); }
    .history-row { display: flex; align-items: center; gap: var(--s-4); padding: var(--s-3) var(--s-4); }
    .history-main { flex: 1; min-width: 0; }
    .history-what { margin: 0 0 0.1rem; font-size: var(--t-base); font-weight: 550; }
    .history-main p:last-child { margin: 0; }
    /* Two buttons on the right of a .history-row — the People tab's Resend and
       Remove. History's rows carry one button and need no wrapper; these do,
       or the second form wraps under the first at every width. */
    .row-actions { display: flex; align-items: center; gap: var(--s-2); flex-shrink: 0; }

    /* --- stats -------------------------------------------------------------- */
    .headline { margin-bottom: var(--s-4); }
    .headline-main {
        margin: 0 0 var(--s-1);
        font-size: var(--t-xl);
        font-weight: 700;
        letter-spacing: var(--track-tight);
    }
    .headline p:last-child { margin: 0; }

    .window-switch { display: flex; align-items: center; gap: var(--s-2); margin: 0 0 var(--s-4); flex-wrap: wrap; }
    .chip {
        font-size: var(--t-sm);
        font-weight: 600;
        text-decoration: none;
        color: var(--muted);
        border: 1px solid var(--border);
        background: var(--surface);
        border-radius: var(--r-full);
        padding: 0.3rem 0.75rem;
        box-shadow: var(--shadow-xs);
        transition: var(--transition);
    }
    .chip:hover { border-color: var(--n-400); color: var(--text); box-shadow: var(--shadow-sm); }
    .chip.is-current {
        background: var(--n-900);
        border-color: var(--n-900);
        color: #fff;
        box-shadow: var(--shadow-sm);
    }
    .chip.is-current:hover { color: #fff; }

    .tiles {
        list-style: none;
        margin: 0 0 var(--s-3);
        padding: 0;
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
        gap: var(--s-3);
    }
    .tile { display: flex; flex-direction: column; gap: 0.1rem; padding: var(--s-4) var(--s-5); }
    .tile-n {
        font-size: var(--t-3xl);
        font-weight: 700;
        line-height: 1.05;
        letter-spacing: var(--track-tight);
    }
    .tile-label {
        display: flex;
        align-items: center;
        gap: var(--s-2);
        font-weight: 600;
        font-size: var(--t-base);
        color: var(--text-soft);
    }
    .tile-label .icon { color: var(--n-400); }
    /* The one number on the scorecard that decides whether reviewing every lead
       can stop: spam the filter missed. Red only when it is not zero — a
       permanent alarm is one nobody reads. */
    .tile-alarm { box-shadow: var(--shadow-sm), inset 0 0 0 1px var(--err-border); background: var(--err-bg); }
    .tile-alarm .tile-n { color: var(--err-text); }
    .tile-alarm .tile-label { color: var(--err-text); }

    .footnote { margin: 0 0 var(--s-5); max-width: 72ch; }

    /* Bar chart. Heights come from a nonced <style> the template emits, because
       inline style="" attributes are not covered by the CSP's style-src nonce. */
    .spark {
        list-style: none;
        margin: 0;
        padding: 0;
        display: flex;
        align-items: flex-end;
        gap: 3px;
        height: 96px;
    }
    .spark li { flex: 1; height: 100%; display: flex; align-items: flex-end; min-width: 2px; }
    .sp {
        display: block;
        width: 100%;
        background: linear-gradient(180deg, var(--a-400), var(--a-600));
        border-radius: 3px 3px 0 0;
        min-height: 3px;
        transition: var(--transition);
    }
    .spark li:hover .sp { background: var(--n-900); }
    /* A day with nothing on it draws as a flat tick rather than disappearing —
       the gaps are part of the shape. */
    .sp.is-zero { background: var(--border-strong); height: 3px; }
    .spark-axis { display: flex; justify-content: space-between; margin: var(--s-2) 0 0; }

    table.data { width: 100%; border-collapse: collapse; margin-top: var(--s-3); font-size: var(--t-base); }
    table.data th, table.data td {
        text-align: left;
        padding: 0.6rem 0.65rem;
        border-bottom: 1px solid var(--border);
        vertical-align: top;
    }
    table.data th {
        font-size: var(--t-xs);
        text-transform: uppercase;
        letter-spacing: var(--track-wide);
        color: var(--faint);
        font-weight: 650;
        border-bottom-color: var(--border-strong);
    }
    table.data tbody tr { transition: var(--transition); }
    table.data tbody tr:hover { background: var(--n-25); }
    table.data tr:last-child td { border-bottom: 0; }
    table.data .num { text-align: right; }
    table.data code { overflow-wrap: anywhere; }

    .visitor-link { text-decoration: none; }
    .visitor-link:hover code { background: var(--n-900); border-color: var(--n-900); color: #fff; }

    /* --- one visitor -------------------------------------------------------- */
    .tile-strong { font-weight: 650; font-size: var(--t-md); line-height: 1.35; }
    .raw-ua { overflow-wrap: anywhere; margin: var(--s-1) 0 0; }

    .timeline { list-style: none; margin: var(--s-3) 0 0; padding: 0; }
    .timeline-row {
        display: flex;
        gap: var(--s-4);
        padding: 0.55rem 0 0.55rem var(--s-3);
        border-left: 2px solid var(--border);
        font-size: var(--t-base);
    }
    /* A call or text tap is the outcome the whole site exists for, so it is the
       one thing on this page allowed to shout. */
    .timeline-row.is-action {
        border-left-color: var(--accent);
        background: var(--a-50);
        border-radius: 0 var(--r-md) var(--r-md) 0;
    }
    .timeline-when { flex: 0 0 11rem; }
    .timeline-what { min-width: 0; overflow-wrap: anywhere; }

    /* --- lead review queue -------------------------------------------------- */
    .lead-open { color: inherit; text-decoration: none; overflow-wrap: anywhere; }
    .lead-open:hover { color: var(--accent); }
    .lead-signals { margin-top: var(--s-1); display: flex; flex-wrap: wrap; gap: var(--s-1); }
    /* .pill is green by default, which reads as "good" — wrong for evidence that
       a lead was flagged. Same amber as .badge-spam and .alert-warn. */
    .pill-spam { background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-text); }
    /* The one place a customer's own words are shown in full: preserve the line
       breaks they typed rather than running their paragraphs together. */
    .lead-message { white-space: pre-wrap; overflow-wrap: anywhere; margin: var(--s-2) 0 0; }
    /* The queue's only control, and it is a link: it opens the lead. The
       decisions live on the detail page because both are permanent, so the row
       offers one target that costs nothing to hit by mistake. */
    .lead-review { display: inline-flex; }

    /* Deployed release, at the foot of the site list. Quiet on purpose: it has
       to be findable when you go looking and invisible the rest of the time. */
    .release-line {
        margin: var(--s-10) 0 0;
        color: var(--faint);
        font-size: var(--t-sm);
    }

    /* --- pictures (site/media.twig) ---------------------------------------- */
    /* A row is thumbnail + name + caption form; the thumbnail is boxed to a
       fixed size so a portrait photo and a wide logo take the same room and
       the list scans as a list. */
    .media-row { display: flex; align-items: center; gap: var(--s-4); }
    .media-thumb {
        display: block;
        width: 96px;
        height: 72px;
        object-fit: cover;
        border-radius: var(--r-sm);
        background: var(--n-100);
        flex-shrink: 0;
    }
    .media-thumb-missing {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        color: var(--muted);
    }
    .media-row .inline input[type="text"] { min-width: 16rem; font-size: var(--t-sm); padding: 0.3rem 0.45rem; }

    /* --- picking a picture (site/_media_pick.twig) ------------------------- */
    /* Tiles instead of a list of filenames. The radio itself is taken out of
       the layout rather than merely hidden: it stays focusable and it stops
       .stack input painting an input's border and padding onto a radio
       button. The tile's own outline is the checked state, and :focus-within
       gives a keyboard the same ring every other control has. */
    .pick-grid-wrap { border: 0; padding: 0; margin: 0; min-width: 0; }
    .pick-grid-wrap > legend { padding: 0; }
    .pick-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
        gap: var(--s-3);
        margin-top: var(--s-2);
    }
    .pick-tile {
        display: flex !important;
        flex-direction: column;
        gap: var(--s-1);
        position: relative;
        cursor: pointer;
        font-weight: 400 !important;
        font-size: var(--t-sm);
        min-height: 40px;
    }
    .pick-tile input[type="radio"] {
        position: absolute;
        width: 1px;
        height: 1px;
        opacity: 0;
        margin: 0;
        padding: 0;
    }
    .pick-face {
        display: flex;
        align-items: center;
        justify-content: center;
        aspect-ratio: 4 / 3;
        overflow: hidden;
        border: 1px solid var(--border-strong);
        border-radius: var(--r-md);
        background: var(--n-100);
        transition: var(--transition);
    }
    .pick-face img, .pick-face video { width: 100%; height: 100%; object-fit: cover; display: block; }
    .pick-face-blank { color: var(--faint); }
    .pick-name {
        color: var(--muted);
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .pick-tile:hover .pick-face { border-color: var(--n-400); }
    .pick-tile input:checked ~ .pick-face {
        border-color: var(--a-400);
        box-shadow: 0 0 0 2px var(--a-400);
    }
    .pick-tile input:checked ~ .pick-name { color: var(--text); font-weight: 600; }
    .pick-tile input:focus-visible ~ .pick-face { box-shadow: var(--focus-ring); border-color: var(--a-400); }
    .pick-tile.is-missing .pick-face { border-style: dashed; border-color: var(--warn-border); color: var(--warn-text); }

    /* The file input is hidden and its <label> is the button, so "Add a
       picture" opens the picker with one tap and there is no second control
       to explain. */
    .pick-add { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; margin-top: var(--s-3); }
    .pick-add .btn { cursor: pointer; }
    .pick-add .hint:empty { display: none; }
    .pick-add .hint.is-bad { color: var(--err-text); }

    /* The Own domain card (Settings) and the Jobs page. What a job or the
       root script printed is shown as it was printed — a certbot failure is
       read as lines, not reflowed — inside a closed <details>, so a card
       with a long transcript behind it is still one screen tall. */
    .run-output { margin: var(--s-3) 0; }
    .run-output pre {
        margin: var(--s-2) 0 0;
        padding: var(--s-3) var(--s-4);
        background: var(--surface-sunken);
        border: 1px solid var(--border);
        border-radius: var(--r-md);
        font-size: var(--t-sm);
        line-height: 1.5;
        white-space: pre-wrap;
        overflow-wrap: anywhere;
        max-height: 24rem;
        overflow: auto;
    }
    .button-row { display: flex; gap: var(--s-3); flex-wrap: wrap; align-items: center; }
    .button-row form { margin: 0; }
    /* A job that has not checked in: the row itself says so, not just its
       badge, because it sorts to the top and should read as one thing. */
    .history-row.is-stale { box-shadow: var(--shadow-sm), inset 0 0 0 1px var(--err-border); background: var(--err-bg); }
    /* A list of alert lines with no bullets — the dashboard's "box" card. */
    ul.plain { list-style: none; margin: 0; padding: 0; }
    ul.plain > li.alert { margin-bottom: var(--s-2); }
    ul.plain.small { margin: 0 0 var(--s-4); display: grid; gap: 0.2rem; }
    /* A finding or a certificate line that is a problem, inside ordinary
       text rather than an alert box. */
    .alert-error-text { color: var(--err-text); }
}

/* ===========================================================================
   UTILITIES
   =========================================================================== */
@layer utilities {
    .muted { color: var(--muted); font-size: var(--t-base); }
    .small { font-size: var(--t-sm); }
    .block { display: block; }
    .mono { font-family: var(--font-mono); }
    .nowrap { white-space: nowrap; }
}

/* ===========================================================================
   RESPONSIVE
   =========================================================================== */
@layer responsive {
    /* One breakpoint for the whole portal. There used to be three (720, 620
       and 600px), chosen page by page, which meant a phone got some of the
       phone layout and not the rest. Most clients open this on a phone, so
       everything below is the phone layout, and it switches on at once. */
    @media (max-width: 640px) {
        .shell { padding: var(--s-5) var(--s-4) var(--s-10); }
        .page-head h1, .site-head h1 { font-size: var(--t-xl); }
        .tile-n { font-size: var(--t-2xl); }
        .card { padding: var(--s-5); }

        /* The top bar has to fit 375px with the owner's three links on it.
           Before this it measured 401px and every page scrolled sideways. The
           brand name goes (the mark stays), the links tighten, and the account
           chip is the avatar alone — the full name and address are in the
           panel it opens. */
        .topbar { padding: 0 var(--s-3); gap: var(--s-2); }
        .topbar nav { gap: 0; flex: 1; min-width: 0; overflow-x: auto; }
        .topbar nav a { flex-shrink: 0; }
        .topbar nav a { padding: 0.5rem 0.55rem; gap: 0.3rem; font-size: var(--t-sm); min-height: 40px; }
        .user-chip { padding: 0.25rem; }
        .user-chip .user-name, .user-chip .usermenu-caret { display: none; }
        .brand-name { display: none; }

        /* The site tabs wrap into rows instead of scrolling off the edge: a
           strip that shows three of ten tabs, with nothing to say there are
           more, hid exactly the ones a client wants (Photos, Business). */
        .site-head h1 { margin-bottom: var(--s-1); }
        .subnav { flex-wrap: wrap; overflow: visible; gap: 2px; }
        .subnav a { padding: 0.45rem 0.6rem; gap: 0.3rem; font-size: var(--t-sm); min-height: 40px; }

        /* Tap targets: nothing a thumb has to hit is under 40px tall. */
        .btn, .btn-small { min-height: 40px; }
        .chip { display: inline-flex; align-items: center; min-height: 40px; }
        .iconbtn { width: 36px; height: 36px; }
        .iconbtn .icon { width: 16px; height: 16px; }

        .timeline-row { flex-direction: column; gap: 0.15rem; }
        .timeline-when { flex: none; }

        .row-2, .row-3 { grid-template-columns: 1fr; }
        .theme-row { grid-template-columns: 1fr; }
        .theme-default { grid-column: 1; }
        .hours-row { flex-wrap: wrap; }
        .history-row { flex-wrap: wrap; }

        /* On a phone a wide table becomes one card per row — five columns at
           360px are unreadable whatever is in them. Opt-in by class: the
           leads tables and the Visits table on the stats page. A cell that
           is just a number carries its heading in data-label, since the
           header row is gone. */
        table.lead-table thead, table.stack-table thead { display: none; }
        table.lead-table, table.stack-table,
        table.lead-table tbody, table.stack-table tbody,
        table.lead-table tr, table.stack-table tr,
        table.lead-table td, table.stack-table td { display: block; }
        table.lead-table tr, table.stack-table tr { padding: var(--s-3) 0; border-bottom: 1px solid var(--border); }
        table.lead-table tbody tr:hover, table.stack-table tbody tr:hover { background: transparent; }
        table.lead-table tr:last-child, table.stack-table tr:last-child { border-bottom: 0; padding-bottom: 0; }
        table.lead-table td, table.stack-table td { border-bottom: 0; padding: 0.1rem 0; }
        table.stack-table td.num { text-align: left; }
        table.stack-table td[data-label]::before {
            content: attr(data-label) ": ";
            color: var(--muted);
            font-size: var(--t-sm);
        }

        /* Review gets its own line and a 44px tap target — it is the way into
           the lead on a phone, where the excerpt is too small a thing to aim
           at. */
        .lead-review {
            margin-top: var(--s-2);
            min-height: 44px;
            padding-inline: var(--s-4);
            align-items: center;
            justify-content: center;
        }

        .site-card { flex-direction: column; align-items: flex-start; gap: var(--s-3); }
        .site-meta { margin-left: 0; }
        /* A list row on a phone: grip and title on the first line with the
           title taking all the width, the buttons on a line of their own
           under it. Side by side, the title was squeezed into a three-word
           column beside two buttons. */
        .section-row { flex-wrap: wrap; }
        .section-row .section-main { flex: 1 1 60%; }
        .section-controls { flex: 0 0 100%; margin-left: 0; padding-left: calc(36px + var(--s-4)); justify-content: flex-start; }

        /* The gallery rows on the Pictures page, until phase 2 replaces them
           with a grid: the caption box's 16rem minimum was the last thing in
           the portal wider than a phone. */
        .media-row { flex-wrap: wrap; }
        .media-row .inline { width: 100%; }
        .media-row .inline input[type="text"] { min-width: 0; flex: 1; }
    }

    /* -----------------------------------------------------------------------
       The prospect upload page (/upload/<token>) and the intake page's link
       card. Public page first: it is opened on a phone, so everything is one
       column, every target is a thumb wide, and the grid is the same one the
       intake page uses so a file looks the same on both sides.
       -------------------------------------------------------------------- */
    .upload-shell {
        max-width: 640px;
        margin: 0 auto;
        padding: var(--s-6) var(--s-4) var(--s-12);
    }
    .upload-brand { margin-bottom: var(--s-5); }
    .upload .page-head h1 { font-size: 1.4rem; line-height: 1.25; }
    .upload-card { margin-bottom: var(--s-4); }

    /* A label styled as the button, with the real file input inside it and
       invisible. Tapping the label opens the picker with no script at all;
       the input itself stays in the tab order and keyboard-reachable. */
    .upload-pick {
        position: relative;
        overflow: hidden;
        min-height: 52px;
        font-size: 1.05rem;
        cursor: pointer;
    }
    .upload-pick input[type="file"] {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        opacity: 0;
        cursor: pointer;
    }
    .upload-pick:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
    /* The label sits inside a .stack form, whose label rule would otherwise
       out-rank .btn-primary and dim the button's text. */
    .upload-pick.btn-primary { color: var(--btn-primary-text); }
    .upload-pick.btn-quiet { color: var(--muted); }
    .upload-hint { text-align: center; }

    .upload-queue {
        list-style: none;
        margin: var(--s-4) 0 0;
        padding: 0;
        display: grid;
        gap: var(--s-2);
    }
    .upload-row {
        display: grid;
        grid-template-columns: 1fr auto;
        grid-template-areas: "name state" "bar bar" "actions actions";
        gap: var(--s-1) var(--s-3);
        padding: var(--s-3);
        border: 1px solid var(--border);
        border-radius: var(--r-md);
        background: var(--surface-sunken);
        font-size: 0.9rem;
    }
    .upload-name {
        grid-area: name;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        color: var(--text-soft);
    }
    .upload-state { grid-area: state; color: var(--muted); white-space: nowrap; }
    .upload-bar {
        grid-area: bar;
        display: block;
        height: 6px;
        border-radius: var(--r-full);
        background: var(--n-200);
        overflow: hidden;
    }
    .upload-fill {
        display: block;
        height: 100%;
        width: 0;
        background: var(--accent);
        transition: width 0.2s ease-out;
    }
    .upload-actions { grid-area: actions; }
    .upload-actions:empty { display: none; }
    .upload-row.is-failed { border-color: var(--err-border); background: var(--err-bg); }
    .upload-row.is-failed .upload-state { color: var(--err-text); white-space: normal; }
    .upload-row.is-failed .upload-fill { background: var(--err-text); }

    .upload-grid {
        list-style: none;
        margin: var(--s-3) 0 0;
        padding: 0;
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
        gap: var(--s-2);
    }
    .upload-grid li { position: relative; }
    .upload-grid img,
    .upload-grid video,
    .intake-photos video {
        display: block;
        width: 100%;
        height: 120px;
        object-fit: cover;
        border-radius: var(--r-md);
        border: 1px solid var(--border);
        background: var(--n-900);
    }
    .upload-grid img { background: var(--surface-sunken); }
    .upload-grid li.is-removing { opacity: 0.4; }
    .upload-grid .btn-remove {
        position: absolute;
        top: 4px;
        right: 4px;
        width: 28px;
        height: 28px;
        line-height: 1;
        padding: 0;
        border: 1px solid var(--border);
        border-radius: var(--r-full);
        background: var(--surface);
        color: var(--muted);
        cursor: pointer;
        font-size: 1.1rem;
    }
    .upload-kind,
    .file-source {
        position: absolute;
        left: 4px;
        bottom: 4px;
        padding: 0.1rem 0.4rem;
        border-radius: var(--r-sm);
        background: rgba(16, 24, 40, 0.72);
        color: #fff;
        font-size: 0.7rem;
        letter-spacing: 0.02em;
        text-transform: uppercase;
        pointer-events: none;
    }
    .upload-foot { text-align: center; margin-top: var(--s-4); }

    /* The intake page's side: the link, and marks on the files it brought. */
    .upload-link-row {
        display: flex;
        gap: var(--s-2);
        align-items: stretch;
        margin-bottom: var(--s-4);
    }
    .upload-link-row input[type="text"] {
        flex: 1;
        min-width: 0;
        font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
        font-size: 0.9rem;
        padding: 0.5rem 0.65rem;
        border: 1px solid var(--border-strong);
        border-radius: var(--r-md);
        background: var(--surface-sunken);
        color: var(--text);
    }
    .intake-photos li.from-prospect img,
    .intake-photos li.from-prospect video { border-color: var(--a-300); }
    .removed-files .intake-photos { margin-top: var(--s-3); }
    .intake-photos li.is-removed img,
    .intake-photos li.is-removed video { opacity: 0.55; filter: grayscale(0.6); }
}

.page-editor-bar { display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; margin-bottom:24px; }
.page-editor-bar label { display:flex; align-items:center; gap:12px; font-weight:600; min-width:0; }
.page-editor-bar select { font:inherit; color:inherit; background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:10px 36px 10px 12px; max-width:100%; min-width:200px; }
@media(max-width:480px) { .page-editor-bar label { flex-direction:column; align-items:flex-start; width:100%; } .page-editor-bar select { width:100%; min-width:0; } }
.page-editor-bar { padding:12px 16px; margin-bottom:20px; }
.page-editor-bar > label { margin-right:auto; }
.page-editor-bar [data-editor-save][hidden] { display:none; }
.editor-workspace.has-section-list { display:grid; grid-template-columns:220px minmax(0,1fr); gap:20px; align-items:start; }
.editor-sections { padding:12px; position:sticky; top:76px; }
.editor-sections h2 { font-size:15px; padding:4px 10px; }
.section-nav-link { display:block; padding:10px; border-radius:6px; color:var(--text); text-decoration:none; }
.section-nav-link:hover, .section-nav-link.active { background:var(--surface-sunken); }
.section-nav-link.active { font-weight:650; }
.section-nav-link small { display:block; font-size:11px; color:var(--muted); font-weight:400; }
.editor-fields { min-width:0; }
.editor-fields .card-form { max-width:none; }
.standalone-page-form { display:grid; grid-template-columns:minmax(0,2fr) minmax(220px,1fr); gap:24px; align-items:start; }
.page-main-fields, .page-side-fields { display:grid; gap:20px; min-width:0; }
.standalone-page-form > .form-actions { grid-column:1 / -1; }
@media(max-width:760px) {
 .editor-workspace.has-section-list, .standalone-page-form { grid-template-columns:1fr; }
 .editor-sections { position:static; }
 .editor-sections { display:flex; overflow-x:auto; gap:4px; align-items:center; }
 .editor-sections h2 { display:none; }
 .section-nav-link { flex:0 0 auto; }
 .section-nav-link small { max-width:150px; }
}

/* Home page rows have one actions menu, so keep it beside the text on phones. */
@media (max-width: 600px) {
    .section-row:has(> .section-controls > .row-menu) {
        flex-wrap: nowrap;
        align-items: flex-start;
        gap: 10px;
        padding: 12px;
    }
    .section-row:has(> .section-controls > .row-menu) > .section-main {
        flex: 1 1 0;
    }
    .section-row:has(> .section-controls > .row-menu) > .section-controls {
        flex: 0 0 auto;
        padding-left: 0;
    }
    .section-row:has(> .section-controls > .row-menu) > .section-order {
        align-self: flex-start;
    }
    .section-row:has(> .section-controls > .row-menu) .section-main p {
        font-size: 13px;
        line-height: 1.4;
    }
    .section-list-pinned .section-order { display: none; }
    .section-controls .row-menu > summary { min-width: 44px; min-height: 44px; }
}
@media (max-width: 480px) {
    .page-editor-bar label { flex-direction: row; align-items: center; }
    .page-editor-bar select { flex: 1; width: auto; min-width: 0; }
}

/* Shared photo library and deliberate, reversible media selection. */
.library-heading { display:flex; align-items:center; justify-content:space-between; gap:16px; }
.library-heading h2 { margin:0; }
.library-heading form { margin:0; }
.library-filters { display:flex; gap:8px; flex-wrap:wrap; margin:20px 0; }
.library-filters [aria-pressed="true"] { background:var(--accent-soft, #eef0ff); color:var(--accent, #4444ff); }
.library-grid .photo-tile[hidden] { display:none; }
.library-photo-label { display:block; padding:10px; font-size:12px; overflow-wrap:anywhere; color:var(--muted); }
.photo-preview > img, .photo-preview > video { display:block; max-width:100%; max-height:65vh; margin:auto; object-fit:contain; }
.media-current { display:flex; gap:16px; align-items:center; flex-wrap:wrap; }
.media-current .pick-face { width:140px; height:105px; border-radius:8px; overflow:hidden; }
.media-current-actions { display:flex; gap:8px; flex-wrap:wrap; }
.media-current-actions [hidden] { display:none; }
.media-dialog { width:min(760px, calc(100vw - 32px)); max-height:85dvh; border:1px solid #d6dce5; border-radius:16px; padding:24px; box-shadow:0 16px 60px #0003; }
.media-dialog::backdrop { background:#17202d80; }
.media-dialog h2 { margin-top:0; }
.media-dialog .pick-grid { max-height:50dvh; overflow:auto; padding:4px; }
.media-dialog-actions { display:flex; justify-content:flex-end; gap:12px; margin-top:16px; }
@media (max-width:600px) {
    .media-dialog { width:100%; height:100dvh; max-height:100dvh; max-width:100%; margin:0; border-radius:0; padding:20px; }
    .media-dialog .pick-grid { max-height:calc(100dvh - 240px); }
    .library-heading { gap:8px; }
    .photo-tile .sort-handle { width:40px; height:40px; }
}

.hero-editor > .card { margin:0; }
.hero-editor h3 { margin:0; }
.hero-editor [hidden] { display:none; }
.hero-editor input[type="range"] { width:100%; accent-color:var(--a-500); padding:0; }
.hero-editor output { font-weight:400; }
.hero-background-sample { position:relative; height:170px; overflow:hidden; border-radius:10px; background:#4b5563; }
.hero-background-sample img { width:100%; height:100%; object-fit:cover; }
.hero-background-shade { position:absolute; inset:0; background:#000; }
.hero-background-text { position:absolute; inset:0; display:flex; align-items:center; padding:24px; color:white; font-size:22px; font-weight:700; }
.hero-highlights[open] { display:grid; gap:16px; }
.hero-save-bar { position:sticky; bottom:0; display:flex; align-items:center; justify-content:space-between; gap:12px; padding:12px 16px calc(12px + env(safe-area-inset-bottom)); background:var(--bg,#fff); border:1px solid var(--border,#dce1e8); border-radius:12px; z-index:10; box-shadow:0 -3px 12px #0001; }

.hero-editor [data-auto-grow] { min-height: 160px; overflow-y: hidden; }

.hero-save-actions { display:flex; gap:8px; align-items:center; }
.hero-save-bar [hidden] { display:none; }
.hero-save-bar [data-hero-save]:disabled { background:#e1e4e8; color:#717782; border-color:#e1e4e8; cursor:default; box-shadow:none; }
.hero-save-bar.is-attention { background:#eaf0ff; border-color:#6787ee; animation:hero-save-shake .35s ease-in-out; }
@keyframes hero-save-shake { 25%,75% { transform:translateX(-4px); } 50% { transform:translateX(4px); } }
@media (prefers-reduced-motion:reduce) { .hero-save-bar.is-attention { animation:none; } }
@media (max-width:480px) { .hero-save-bar { flex-wrap:wrap; } .hero-save-actions { margin-left:auto; } }

@media (max-width:600px) {
    .hero-editor .hero-save-bar { position:sticky; top:64px; bottom:auto; padding:12px; z-index:15; box-shadow:0 3px 12px #0001; }

}

.service-list-row { display:flex; align-items:center; gap:12px; }
.service-list-image { width:48px; height:48px; flex:0 0 48px; display:grid; place-items:center; background:var(--surface-sunken); border-radius:8px; overflow:hidden; }
.service-list-image img { width:100%; height:100%; object-fit:cover; }
.service-list-summary { display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.service-list-row .section-main { flex:1; min-width:0; }
.service-list-row .section-controls { flex:0 0 auto; }

.service-editor .service-search { border-top:1px solid var(--border); padding-top:16px; }
.service-search > .stack { margin-top:16px; }
.description-preview > summary, .service-search > summary { cursor:pointer; font-weight:600; padding:8px 0; }
.service-editor .rich-toolbar { gap:4px; }
.service-editor .rich-toolbar .btn { box-shadow:none; }
.service-editor .rich-preview { margin-top:12px; }

.standalone-page-form > .hero-save-bar { grid-column:1 / -1; }

/* Appearance's local style sample never changes the portal theme. */
.appearance-editor .theme-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.appearance-editor .theme-row > .alert { grid-column: 1 / -1; }
.appearance-editor .theme-input { min-width: 0; }
.appearance-editor .theme-input input[type="text"] { min-width: 0; width: 100%; }
.corner-choices { display: flex; flex-wrap: wrap; gap: 24px; }
.corner-choices label { display: flex; align-items: center; gap: 10px; }
.corner-example { display: inline-block; width: 48px; height: 32px; border: 2px solid currentColor; }
.corner-rounded { border-radius: 10px; }
.appearance-sample { border: 1px solid var(--border); overflow: hidden; border-radius: var(--sample-radius, 12px); }
.appearance-sample-header { background: var(--sample-header-bg); color: var(--sample-header-text); padding: 16px; font-weight: 600; }
.appearance-sample-body { background: var(--sample-light-bg); color: var(--sample-light-text); padding: 20px; }
.appearance-sample-card { background: var(--sample-light-panel); border: 1px solid var(--sample-light-border); padding: 20px; border-radius: var(--sample-radius); }
.appearance-sample-button { display: inline-block; padding: 10px 18px; background: var(--sample-accent); color: var(--sample-accent-contrast); border-radius: var(--sample-radius); }
@media (max-width: 640px) { .appearance-editor .theme-row { grid-template-columns: 1fr; } }
.project-photo-row { display: grid; grid-template-columns: 36px minmax(0, 1fr) minmax(0, 2fr); gap: 12px; align-items: center; margin-bottom: 10px; }
.project-photo-row img { display: block; width: 100%; max-width: 160px; height: 100px; object-fit: cover; border-radius: 6px; }
.project-photo-row label { min-width: 0; }
[data-project-available] .sort-handle { visibility: hidden; }
@media(max-width:640px) { .project-photo-row { grid-template-columns: 36px minmax(0,1fr); } .project-photo-row > label:last-child { grid-column: 2; } }
.library-toolbar { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; margin:20px 0; }
.library-toolbar .library-filters { margin:0; }
.library-sort { display:flex; align-items:center; gap:8px; margin:0 0 0 auto; }
.library-sort select { width:auto; max-width:100%; cursor:pointer; }
.library-sort label { white-space:nowrap; font-size:13px; }
@media(max-width:480px) { .library-sort { margin-left:0; } }

/* Library previews use the shared gallery viewer. */
.lightbox {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 3.5rem 1rem;
    background: rgba(11, 16, 24, 0.92);
}
.lightbox[hidden] { display: none; }
/* Stop the page behind the overlay scrolling under the photo. */
body.lightbox-open { overflow: hidden; }
.lightbox-figure {
    margin: 0;
    max-width: 100%;
    max-height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
}
.lightbox-image,
.lightbox-video {
    max-width: 100%;
    /* Leaves room for the caption and counter without the photo ever
       pushing them off screen on a short viewport. */
    max-height: calc(100vh - 9rem);
    object-fit: contain;
    border-radius: var(--radius-sm);
    display: block;
}
.lightbox-image[hidden], .lightbox-video[hidden] { display: none; }
.lightbox-caption {
    margin: 0;
    color: var(--text);
    text-align: center;
    max-width: 60ch;
}
.lightbox-caption[hidden] { display: none; }
.lightbox-counter {
    position: absolute;
    bottom: 1rem;
    left: 50%;
    transform: translateX(-50%);
    margin: 0;
    color: var(--muted);
    font-size: 0.85rem;
}
.lightbox-counter[hidden] { display: none; }
.lightbox button {
    position: absolute;
    background: rgba(11, 16, 24, 0.6);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 50%;
    /* Comfortably past the 44px touch target minimum — these sit over a
       photo, so a near miss means closing the viewer by accident. */
    width: 48px;
    height: 48px;
    font-size: 1.75rem;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
}
@media (hover: hover) and (pointer: fine) {
    .lightbox button:hover { background: var(--accent); color: var(--accent-contrast); border-color: var(--accent); }
}
.lightbox button:active { background: var(--accent); color: var(--accent-contrast); border-color: var(--accent); }
.lightbox button:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.lightbox button[hidden] { display: none; }
.lightbox-close { top: 1rem; right: 1rem; }
.lightbox-prev { left: 1rem; top: 50%; transform: translateY(-50%); }
.lightbox-next { right: 1rem; top: 50%; transform: translateY(-50%); }

/* Playback controls sit below the clip, never over its picture. */
.lightbox-video { max-height: calc(100dvh - 13rem); }
.lightbox-controls {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: min(100%, 40rem);
    color: var(--text);
}
.lightbox-controls[hidden] { display: none; }
.lightbox-controls button {
    position: static;
    flex: 0 0 auto;
    width: auto;
    min-width: 44px;
    height: 44px;
    padding: 0 0.5rem;
    border-radius: var(--radius-sm);
    font-size: 0.8rem;
}
.lightbox-controls input { flex: 1; min-width: 0; width: 5rem; height: 44px; }
.lightbox-time { font-size: 0.75rem; white-space: nowrap; font-variant-numeric: tabular-nums; }

/* Keep the timeline still when the sound action label changes. */
.lightbox-controls .lightbox-mute { width: 5rem; }

.lightbox { --text:#f4f6fa; --muted:#aab5c6; --border:#344050; --accent:#6366f1; --accent-contrast:#fff; --radius-sm:6px; z-index:10000; }
.library-preview { position:relative; display:block; }
.library-preview video { pointer-events:none; }
.library-play { position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); display:grid; place-items:center; width:44px; height:44px; border-radius:50%; background:#10151ecc; color:#fff; font-size:20px; }
.library-duration { position:absolute; right:6px; bottom:6px; border-radius:4px; padding:3px 6px; background:#10151ecc; color:#fff; font-size:12px; }
.library-card-info { display:flex; align-items:baseline; justify-content:space-between; gap:8px; padding:10px; font-size:12px; }
.library-usage { color:var(--text-soft); overflow-wrap:anywhere; }
.library-card-info a { flex-shrink:0; }
.library-specs { margin:0; padding:0 10px 12px; font-size:12px; }
.library-specs[hidden] { display:none; }
.library-specs dt { color:var(--text-soft); margin-top:6px; }
.library-specs dd { margin:0; overflow-wrap:anywhere; }

.library-grid .photo-tile { position:relative; }
.library-select { position:absolute; top:2px; left:2px; z-index:1; padding:4px; display:flex; }
.library-select input { width:16px; height:16px; margin:0; cursor:pointer; }
.library-select input:disabled { opacity:0.65; cursor:default; }
[data-library-delete] { margin-bottom:16px; }
[data-library-delete][hidden] { display:none; }

.media-detail { display:grid; grid-template-columns:minmax(0, 1.35fr) minmax(280px, 1fr); gap:24px; align-items:start; }
.media-detail .photo-preview > img, .media-detail .photo-preview > video { max-height:60vh; }
.media-detail-info .library-specs { padding:0; margin-bottom:24px; font-size:14px; }
.media-detail-info .library-specs dt { margin-top:12px; }
.media-placements { list-style:none; padding:0; margin:0 0 24px; }
.media-placements li { margin:8px 0; }
@media(max-width:760px) { .media-detail { grid-template-columns:minmax(0,1fr); } .media-detail .photo-preview > img, .media-detail .photo-preview > video { max-height:45vh; } }

.website-intro { margin:0 0 24px; }
.website-intro h2 { margin:0; font-size:22px; line-height:1.4; }
.website-intro > p { margin:12px 0 0; max-width:none; }
.website-intro:has(.library-heading) { display:grid; grid-template-columns:minmax(0,1fr) auto; column-gap:24px; }
.website-intro .library-heading { display:contents; }
.website-intro .library-heading h2 { grid-column:1; grid-row:1; }
.website-intro .library-heading form { grid-column:2; grid-row:1 / 3; align-self:start; }
.website-intro:has(.library-heading) > p { grid-column:1; grid-row:2; }
.page-editor-guidance { margin:0 0 24px; }
.page-editor-guidance p { max-width:none; margin:0 0 12px; }
.page-editor-guidance summary { cursor:pointer; color:var(--text-soft); font-size:13px; }
.page-editor-guidance details > p { margin:8px 0 0; }
@media(max-width:640px) { .website-intro:has(.library-heading) { grid-template-columns:minmax(0,1fr); } .website-intro .library-heading form { grid-column:1; grid-row:3; margin-top:16px; } }

.editor-save-heading { position:sticky; top:64px; z-index:20; display:flex; align-items:flex-start; justify-content:space-between; gap:16px; padding:0 0 12px; margin-bottom:12px; background:var(--bg,#f6f7fa); border-bottom:1px solid var(--border,#dce1e8); }
.editor-save-heading > h2 { margin:0; font-size:22px; line-height:1.4; }
.editor-save-heading .hero-save-bar { position:static; margin:0 0 0 auto; padding:0; border:0; border-radius:0; box-shadow:none; background:transparent; }
.editor-save-heading [data-hero-save]:not(:disabled) { background:#16713e; border-color:#16713e; color:#fff; }
.editor-save-heading [data-hero-save]:not(:disabled):hover { background:#10592f; border-color:#10592f; }
.editor-save-heading + .website-intro > p { margin-top:0; }
@media(max-width:640px) { .editor-save-heading { flex-wrap:wrap; gap:8px; } .editor-save-heading .hero-save-bar { width:100%; } .editor-save-heading [data-hero-save-status] { font-size:12px; } }

.library-uses { min-width:0; width:100%; }
.library-uses summary { cursor:pointer; color:var(--accent,#4f46e5); }
.library-uses ul { list-style:none; padding:0; margin:8px 0 0; }
.library-uses li { margin:6px 0; overflow-wrap:anywhere; }

.library-card-info[hidden] { display:none; }
