/*
 * The webapp's stylesheet — the SINGLE source for both shells.
 *
 * The app is served by two different HTML shells: `index.html` (the webpack dev server, from the origin root)
 * and `appui`'s `AppUiPage` (production, a Kotlin-rendered string under the app context root). Both link this
 * file, so a rule added here reaches both. It is authored once here, in the webapp's resources, next to the
 * components that style against it; `appui` embeds it from `:webapp`'s distribution the same way it embeds
 * `webapp.js` and the SVGs.
 *
 * This file exists BECAUSE that split went wrong once: each shell used to carry its own inline copy of the
 * CSS, and every feature added after the app bar (the app bar, the home page, the display engine) only ever
 * styled the dev shell. The production sheet fell to 7 classes against the dev shell's 33 and its `body` rule
 * was still the pre-app-bar centred-card layout, so a built jar rendered the app largely unstyled — while the
 * dev server, which is what everyone looked at, was fine. If you are about to add CSS to a shell's `<style>`
 * block instead of here, that is the bug growing back.
 *
 * The class names are the contract with the React components; grep a name here to find its component.
 */

/*
 * The palette, named by the job each colour does rather than by what it looks like. This file carried 52
 * colour literals for 11 actual colours -- the same slate re-spelled in fifteen places -- and no rule said
 * which of them meant "a border" as opposed to "happens to be #334155". Every colour below is now named once
 * and referenced; a rule that spells a hex is either a new colour or a mistake.
 *
 * The names describe the four text weights and the accent pair the design already used, rather than inventing
 * a scheme: they were read off the rules, not chosen.
 *
 * These are custom properties, not preprocessor variables: they resolve in the browser, so a deployment or an
 * account can later be varied by emitting a different `:root` block, without touching a single rule below.
 * If that day comes, note that antd is themed from its *own* tokens (see App.kt's ConfigProvider) and covers
 * roughly half this app's surface, so the two token sets have to be driven from one source -- otherwise a
 * brand colour would apply to our half and not antd's.
 */
:root {
    color-scheme: light dark;

    /* The page itself, and the inset wells that sit *into* it: code, pre, inputs. */
    --surface-page: #0f172a;
    /* Raised above the page: the app bar, the menu, a card. */
    --surface-raised: #1e293b;

    /* The width of a *content* page -- Home, the documents, the operator pages, the catalog, the forms list
       (issue #554). One token, so the shell widens as one thing: a wide window used to leave 42% of itself
       empty beside a 1040px card. A genuine form (the auth flow, the profile) keeps the narrow `.card`, and a
       table-first page keeps its own `.card.full`. Prose stays readable at this width because Markdown
       paragraphs are capped at 80ch below, on the column rather than the card. */
    --content-width: min(1400px, 95vw);
    /* Every border in the app, the hairline between sections, and a hover wash. */
    --border: #334155;

    /* Body text, and the three steps down from it. */
    --text: #e2e8f0;
    /* Labels and button text: softened, still foreground. */
    --text-dim: #cbd5e1;
    /* Secondary prose: subtitles, menu labels. */
    --text-muted: #94a3b8;
    /* Annotations that must not compete: type hints. */
    --text-faint: #64748b;

    /* The indigo pair: the accent, and the brighter one it moves to on hover (also links). */
    --accent: #6366f1;
    --accent-bright: #818cf8;
    /* One step brighter on the same indigo ramp: the hover shade for links (7.3:1 against the card). antd's
       dark algorithm derives a DARKER hover for links (tuned for its near-black backgrounds), which on this
       slate card drops to 1.5:1 and all but vanishes; hover must move away from the background, not toward it.
       Read directly by `.back-link:hover` and `.ant-btn-link:hover` below. */
    --accent-brighter: #a5b4fc;

    --danger: #f87171;
    --success: #4ade80;

    /* The elevated-privilege pair: the app bar's surface and hairline while an administrator is signed in.
       Amber rather than red -- this is a standing state, not a fault, and red would read as something broken.
       Deep enough that --text keeps its contrast against it, so no text colour changes with the bar. */
    --surface-admin: #422006;
    --border-admin: #d97706;

    /* The sandbox's colors (issue #1049): teal, apart from the indigo accent, the admin bar's amber and the success
       green, so the sandbox never reads as one of those. The band, and the page behind the cards. */
    --sandbox: #14b8a6;
    --sandbox-page: oklch(0.45 0.08 190);
}

/* In a sandbox (issue #1049): the page behind the cards turns teal, and a band runs along the top edge above
   everything, so being in a sandbox shows at a glance -- on a screen share, in a cropped screenshot -- and not only
   in the bar's chip. Outside a sandbox there is neither. The attribute is set by the shell from its config
   (`markPageSandbox`).

   The page's own background only, not --surface-page: that token is also the inset wells inside the cards (inputs,
   code, chips), where text sits, and they stay as they are. No text sits on the page itself -- every page draws its
   content on a card -- so the tint answers to no text contrast and can be a mid-tone, bold enough to read on a call,
   with the slate cards standing out on it as panels. */
:root[data-sandbox] body {
    background: var(--sandbox-page);
}
:root[data-sandbox] body::before {
    content: "";
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 4px;
    background: var(--sandbox);
    z-index: 1100;
    pointer-events: none;
}

/* A public test deployment (issue #1150): a band fixed along the bottom edge of every page says the deployment is for
   testing and development only, for every caller. A deep rose, apart from the indigo accent, the admin bar's amber,
   the sandbox's teal and the success green, so it reads as none of them -- dark enough to be noticed without pulling
   the eye from the page; white text on it measures 7.9:1. The page keeps its
   last content clear of it (`markPageTestDeployment`). */
.test-deployment-band {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1090;
    padding: 4px 16px;
    background: #9d174d;
    color: #fff;
    font-size: 0.8rem;
    text-align: center;
    pointer-events: none;
}
:root[data-test-deployment] body {
    padding-bottom: 32px;
}

/* Becoming someone on a test instance (issue #1183): the Home page's card, and the remembered identities as a wrap of
   buttons. The card sits above the welcome copy, set off by the inset surface like the other wells. */
.test-access-card {
    margin-bottom: 20px;
    padding: 12px 16px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface-page);
}
.test-access-card h2 {
    margin: 0 0 8px;
    font-size: 1rem;
}
.become-recent {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 10px 0;
}

* { box-sizing: border-box; }

body {
    margin: 0;
    min-height: 100vh;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: var(--surface-page);
    color: var(--text);
}

/* The app bar sits at the top; the page content fills the rest and centers its card. */
#root { min-height: 100vh; display: flex; flex-direction: column; }
.app-content { flex: 1; display: flex; justify-content: center; align-items: flex-start; padding: 32px 16px; }
/* "A new version is available" bar (issue #136): sits above the app bar, non-intrusive, offering a reload. */
.update-banner {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    padding: 8px 24px;
    background: var(--accent);
    color: #fff;
    font-size: 0.9rem;
}
.update-banner-reload {
    background: transparent;
    border: 1px solid rgba(255, 255, 255, .7);
    color: #fff;
    font-size: 0.85rem;
    font-weight: 600;
    padding: 2px 12px;
    border-radius: 8px;
    cursor: pointer;
}
.update-banner-reload:hover { background: rgba(255, 255, 255, .15); }

.app-bar {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 24px;
    background: var(--surface-raised);
    border-bottom: 1px solid var(--border);
    box-shadow: 0 2px 12px rgba(0, 0, 0, .35);
}

/* The brand lockup: mark + wordmark, sitting on one baseline. */
.app-bar-brand {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    color: var(--text);
    font-weight: 700;
    font-size: 1.15rem;
    text-decoration: none;
    letter-spacing: 0.03em;
}
/* `object-fit: contain` because a client may upload a mark of its own (issue #1130), which need not be square:
   it is fitted inside the box the deployment's square mark fills, never stretched to it. */
.app-bar-logo { display: block; width: 26px; height: 26px; object-fit: contain; }
/* The wordmark's loading cue (issue #469): a muted, lighter-weight ellipsis beside the mark, distinct from the
   loaded wordmark so it reads as "still arriving" rather than as the brand itself. Only shown after a short
   delay (see useDelayedFlag), so a fast load never flashes it. */
.app-bar-brand-loading { color: var(--text-faint); font-weight: 400; letter-spacing: normal; }

/* The app bar while the signed-in user holds administrative rights: a standing visual cue that actions taken
   here carry more weight than usual. The colour is deliberately not the only signal -- see .admin-badge --
   because a cue that exists solely as a hue is no cue at all to a colourblind user. */
.app-bar.admin {
    background: var(--surface-admin);
    border-bottom-color: var(--border-admin);
}

/* The shared chip look for the app bar's session-status badges (env, admin): one size, radius, spacing and
   uppercase treatment, so two of them side by side read as one vocabulary rather than two stray stickers.
   Colour and border are the per-badge modifier's job, and they are spaced by the container's gap, not a
   margin, now that they cluster on the right beside the identity rather than floating apart. */
.bar-badge {
    padding: 2px 8px;
    border: 1px solid transparent;
    border-radius: 10px;
    font: inherit;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    white-space: nowrap;
}

/* The env-auth badge (issue #360), which is also the toggle. Styled as a control rather than a label because
   it is one -- but quiet, since it reports a standing condition rather than inviting an action. The `.off`
   variant deliberately stays visible and legible: it is the only route back from a suppressed session, so
   fading it out would hide the affordance exactly when it is needed. */
.env-badge {
    border-color: var(--border-admin);
    background: var(--surface-admin);
    color: var(--text);
    cursor: pointer;
}

.env-badge.off {
    border-style: dashed;
    border-color: var(--border);
    background: transparent;
    color: var(--text-dim);
}

/* The persistent `_debug` box (issue #517, slice 3): a small text field beside the env badge, shown only in
   debug. It shares the .bar-badge chip shape for size but is deliberately styled as an editable field -- an
   inset page-surface background and a left-aligned, non-uppercase value -- so it reads as somewhere to type
   rather than a button. Wide enough to show a typical tag (`explainAccess`) without truncating, and it does
   NOT animate on focus: a box that resizes when clicked reads as "something happened" when nothing did. */
.env-debug-input {
    /* Override the global `input` rule (flex: 1; min-width: 180px), or the box grows to fill the bar. */
    flex: 0 0 auto;
    min-width: 0;
    width: 16ch;
    padding: 2px 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface-page);
    color: var(--text);
    text-transform: none;
    letter-spacing: normal;
    font-size: 12px;
    font-weight: 500;
}
.env-debug-input::placeholder {
    color: var(--text-faint);
    text-transform: none;
    letter-spacing: normal;
}
.env-debug-input:focus {
    outline: none;
    border-color: var(--accent-bright);
}
/* Not-applied state: the value fails the variable-name/length rule, so it is neither stored nor sent. Flagged
   rather than silently dropped, matching aria-invalid on the element. */
.env-debug-input.invalid {
    border-color: var(--danger);
}

/* The debug index's tool links (issue #227): plain <a>s otherwise fall to the browser's default blue, which is
   near-unreadable on the dark card. Use the same bright accent the Markdown links use. */
.debug-index a {
    color: var(--accent-bright);
    text-decoration: none;
}
.debug-index a:hover { text-decoration: underline; }

/* The "← Listing" link atop a child page (issue #554; was the debug-only `.debug-back`). */
.back-link {
    display: inline-block;
    margin-bottom: 12px;
    color: var(--accent-bright);
    text-decoration: none;
    font-size: 0.9rem;
}
.back-link:hover { text-decoration: underline; }
/* antd's link-styled buttons (`type = "link"`: View raw, Raw edit, View Info, the pager) take the palette's
   link colour straight from these variables -- the same source the app's <a> links use -- rather than antd's
   colorLink token, whose dark algorithm re-derives a seed to a dimmer shade (measured: #818cf8 came out
   #717ad6, 3.8:1 against the card). Measured, not guessed: antd's stock dark link is 2.8:1 here and its derived
   hover DARKENS to 1.5:1, tuned for antd's near-black backgrounds, not this slate shell. They hover the way the
   app's own links do -- brighter, and underlined, so the affordance does not rest on colour alone.
   The rest colour needs only two classes to outrank antd's plain variant rule. The hover and active colours
   carry `!important` on purpose: antd 6 sets its state colours through NESTED rules
   (`&:not(:disabled):not(.ant-btn-disabled):hover`) whose effective specificity outranks any sane app
   selector, and a real-pointer measurement showed antd's derived dark hover (#15417e, 1.5:1) still painting
   over a plain override. Overriding a third-party state rule is what `!important` is for; without it the
   hover "fades to dark" no matter what the palette says.
   Scoped to LIVE, NON-DESTRUCTIVE link buttons (the review's two regressions): a `danger` link button keeps
   antd's red -- antd colours it by swapping a CSS variable behind a low-specificity declaration, which a plain
   colour rule would silently override, turning the table's Delete indigo -- and a disabled one keeps antd's
   dimmed colour and gets no hover, since antd's `:disabled` rule only ties this one and loses the tie. */
.ant-btn.ant-btn-link:not(:disabled):not(.ant-btn-disabled):not(.ant-btn-dangerous):not(.ant-btn-color-dangerous) {
    color: var(--accent-bright);
}
.ant-btn.ant-btn-link:not(:disabled):not(.ant-btn-disabled):not(.ant-btn-dangerous):not(.ant-btn-color-dangerous):hover {
    color: var(--accent-brighter) !important; text-decoration: underline !important;
}
.ant-btn.ant-btn-link:not(:disabled):not(.ant-btn-disabled):not(.ant-btn-dangerous):not(.ant-btn-color-dangerous):active {
    color: var(--accent) !important;
}

/* The elevated-privilege chip: an outline against the admin-tinted bar, where a fill would blend into it. */
.admin-badge {
    border-color: var(--border-admin);
    color: var(--text);
}

/* The readable-build marker (issue #230). Deliberately NOT a .bar-badge chip and quieter than the badges: it
   says which bundle is loaded, not a fact about your session. Never seen on a real deployment, which ships
   the minified build. */
.build-badge {
    padding: 2px 8px;
    border: 1px dashed var(--border);
    border-radius: 10px;
    font-size: 11px;
    color: var(--text-dim);
    letter-spacing: .02em;
}
/* `min-width: 0` lets the group shrink below its content on a narrow window (issue #1002), so the identity badge --
   the one item that can -- cuts off with an ellipsis rather than pushing the bar's controls off screen. */
.app-bar-right { position: relative; display: flex; align-items: center; gap: 8px; min-width: 0; }

/* Who is signed in, in the bar rather than only inside the menu (issue #276). Quiet by default -- it is
   context, not a call to action -- and set apart when signed out, because that is the state a reader is
   most likely to have misread as "still loading". Spelled out rather than signalled by colour alone, the
   same reasoning as `.admin-badge` above. The cap is wide enough for a short address and a sandbox user's
   qualifier -- `chief@acme.test [acme:sandbox · Admin]` (issue #1002) -- and the badge still shrinks, cutting
   off with an ellipsis, before it crowds the bar's other controls; the tooltip carries the whole label. */
.identity-badge {
    max-width: 51ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    padding: 2px 8px;
    border-radius: 10px;
    font-size: 12px;
    color: var(--text-muted);
}

.identity-badge.signed-out {
    border: 1px dashed var(--border);
    color: var(--text-dim);
    font-style: italic;
}

.hamburger {
    background: transparent;
    border: none;
    color: var(--text);
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
    padding: 4px 10px;
    border-radius: 8px;
}
.hamburger:hover { background: var(--border); }

/* Sits under the menu (but over the page) so a click anywhere outside the menu closes it. */
.app-menu-overlay { position: fixed; inset: 0; z-index: 8; }
.app-menu {
    position: absolute;
    right: 0;
    top: calc(100% + 8px);
    z-index: 30;
    background: var(--surface-raised);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: 0 12px 40px rgba(0, 0, 0, .5);
    min-width: 190px;
    overflow: hidden;
}
/* Both an anchor (a nav item) and a button (Log out, which acts rather than navigates) carry this class, so
   it resets the button chrome to make the two presentations identical. */
.app-menu-item {
    display: block;
    width: 100%;
    text-align: left;
    padding: 12px 16px;
    color: var(--text);
    text-decoration: none;
    background: none;
    border: none;
    font: inherit;
    cursor: pointer;
}
.app-menu-item:hover { background: var(--border); }
/* The page on screen (issue #554): an accent rail and a lifted tone, on the item and on the group holding it. */
.app-menu-item.is-current, .app-menu-parent.is-current {
    background: rgba(129, 140, 248, .10);
    box-shadow: inset 3px 0 0 var(--accent-bright);
}
.app-menu-item.is-current { color: var(--accent-bright); }

/* A drill-down parent (issue #517, collapsible since #540): a clickable group header that toggles its
   children, which are indented under it so the nesting reads as a group rather than a flat list. It reads like
   an ordinary menu item -- same font and colour -- with a right-hand chevron marking it expandable, the way a
   dropdown does; it is a real <button>, so the chrome is reset and it gets the same hover as an item. */
.app-menu-parent {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    text-align: left;
    padding: 12px 16px;
    color: var(--text);
    font: inherit;
    background: none;
    border: none;
    cursor: pointer;
}
.app-menu-parent:hover { background: var(--border); }
/* The right-hand expand/collapse chevron -- a dropdown-style caret, sized to be clearly visible. */
.app-menu-caret { margin-left: 12px; font-size: 0.8em; color: var(--accent-bright); opacity: 0.85; }
.app-menu-children .app-menu-item { padding-left: 32px; }

/* The identity badge as a user menu (issue #749): for a person with several users the badge is a button --
   the same look, a caret, a pointer -- and its menu drops under it like the hamburger's. The user you already
   are is marked like the current page, and inert. */
.identity-badge.has-users {
    cursor: pointer;
    font: inherit;
    background: none;
    color: inherit;
    border: 1px solid var(--border);
}
.identity-badge.has-users:hover { background: var(--border); }
.identity-badge.has-users .app-menu-caret { margin-left: 6px; font-size: 0.7em; }
.user-menu { min-width: 12rem; }
/* One line per user, however long its qualifier -- a sandbox client's id is (issue #1002). */
.user-menu .app-menu-item { white-space: nowrap; }
.user-menu .app-menu-item:disabled { cursor: default; }

/* The ↗ affordance on a menu item whose action leaves the app (issue #493) -- a quiet trailing glyph, not a
   second control. Decorative: the accessible name already says "leaves this app". */
.app-menu-external {
    font-size: 0.85em;
    opacity: 0.6;
}

/* A non-clickable menu header, e.g. "Signed in as …". */
/* Home shell: the three link presentations (top bar / left bar / inline) around the content. */
.home-shell { width: var(--content-width); }
.home-topbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.home-body { display: flex; gap: 24px; align-items: flex-start; }
.home-leftbar {
    flex: 0 0 200px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 16px;
    border-radius: 12px;
    background: var(--surface-raised);
}
.home-leftbar h2 { margin: 0 0 8px; font-size: .9rem; }
.home-main {
    flex: 1;
    min-width: 0;
    padding: 24px 28px;
    border-radius: 12px;
    background: var(--surface-raised);
}

/* The home hero: the mark at display size beside the wordmark. */
.home-hero { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; }
.home-hero-mark { display: block; width: 64px; height: 64px; flex: none; object-fit: contain; }

/* A client's web assets on its page (issue #1130): one row a slot -- the image as its people see it, in a well
   the page's own colour so a mark drawn for a dark bar is judged on one, then what is known of it and its actions. */
.web-asset { display: flex; align-items: flex-start; gap: 16px; margin-top: 14px; }
.web-asset-preview {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 88px;
    height: 88px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--surface-page);
}
.web-asset-preview img { display: block; max-width: 64px; max-height: 64px; object-fit: contain; }
.web-asset-none { color: var(--text-muted); font-size: 1.4rem; }
.web-asset-facts { display: flex; flex-direction: column; gap: 4px; min-width: 0; overflow-wrap: anywhere; }
.web-asset-name { font-weight: 600; color: var(--text); }
.web-asset-actions { display: flex; gap: 8px; margin-top: 6px; }
/* The browser's own file picker stays in the page -- a button opens it -- and out of sight (`FilePickButton`). */
.file-picker { display: none; }
/* A client's file refs (issue #1176), drawn as its web assets are: a row a ref. What is unusual about one -- a
   draft, a missing file, an image the client was shipped with -- is a small mark beside its id. */
.file-ref-tag {
    margin-left: 8px;
    padding: 1px 7px;
    border: 1px solid var(--border);
    border-radius: 999px;
    font-size: 0.75rem;
    font-weight: 500;
    color: var(--text-muted);
}
.file-ref-address code { font-size: 0.85rem; }
/* What the last change did. Always in the page, so the rows under it keep their place; with nothing to say it
   takes no room. */
.file-ref-note:empty { display: none; }
/* Rewording one in place: the field and its two buttons, where the description was. */
.file-ref-describe { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
/* Adding one, under the list: an id and what it is for, then how -- an upload, or a file the deployment keeps. */
.file-ref-add { display: flex; flex-direction: column; gap: 8px; margin-top: 22px; }
.file-ref-add-row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; }
/* Finding one among many (issue #1190): words in an id or what it is for, and a toggle per mark the list carries. */
.file-ref-find { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px 16px; margin: 4px 0 12px; }
/* A field with its name above it: what a screen reader still says once something is typed. */
.file-ref-field { display: inline-flex; flex-direction: column; gap: 2px; font-size: 0.85rem; color: var(--text-muted); }
/* What a copy button says it did, beside it: in words, not by a mark alone. */
.copy-outcome { font-size: 0.85rem; color: var(--text-muted); }
.copy-outcome:not(:empty) { margin-left: 6px; }
.home-hero-name { font-size: 2rem; font-weight: 700; letter-spacing: 0.03em; color: var(--text); }

.home-inline-links { display: flex; flex-wrap: wrap; gap: 8px; }
.link-button {
    display: inline-block;
    text-decoration: none;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text-dim);
    padding: 8px 12px;
    font-size: .9rem;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
}
.link-button:hover { background: var(--border); color: var(--text); }
.link-button.open { border-color: var(--accent); color: var(--text); }

/* Rendered Markdown (fragment copy and whole documents). */
.markdown { line-height: 1.6; overflow-wrap: anywhere; }
/* Readable measure at any width (issue #554): prose is capped at ~80 characters, on the paragraph rather than
   the container, so a table (#547) or a code block still gets the full column while a line of text does not
   run the width of a wide window. */
.markdown p, .markdown li, .markdown blockquote, .markdown h1, .markdown h2, .markdown h3 { max-width: 80ch; }
.markdown h1, .markdown h2, .markdown h3 { margin: 20px 0 8px; }
/* Inline-UI Markdown (issue #631, the `inlineUi` prop): Markdown rendered as a piece of UI -- a heading with
   its line directly beneath and no lead-in -- rather than as a document. Only the look differs from
   `.markdown`; the renderer emits the same semantic HTML. Heading and body both take the muted section-heading
   colour (`--text-muted`), so the heading matches the form's other section headings and its follow-up line
   reads as part of the same unit rather than a heading over brighter body text. */
.markdown-inline-ui > :first-child { margin-top: 0; }
.markdown-inline-ui h1, .markdown-inline-ui h2, .markdown-inline-ui h3 { margin: 0 0 1px; color: var(--text-muted); }
.markdown-inline-ui p { margin: 0; color: var(--text-muted); }
.markdown-inline-ui > :last-child { margin-bottom: 2px; }
/* `markdown-inline` is a phrase rendered inside markup the caller owns (MarkdownInline): it shares these
   inline looks, but never the block spacing above -- it has no line-height or margins of its own. */
.markdown code, .markdown-inline code {
    background: var(--surface-page);
    border: 1px solid var(--border);
    border-radius: 5px;
    padding: 1px 5px;
    font-size: .92em;
}
/* The roles a bracketed span may carry (issue #795): `[x]{.value}` a substituted value a reader copies out, set apart
   from the prose; `[x]{.code}` a code read off and typed elsewhere. The renderer's default gives a role a `md-`
   prefixed class, so `.code` here stays the block well below and an inline code value never picks it up. */
.md-value { font-weight: 600; }
.md-code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-weight: 600; letter-spacing: .08em; }
/* An inset block well: a rendered Markdown code block, or one the display engine builds by hand (`.code`).
   The same box either way -- they differ only in the text inside it. */
.markdown pre, .code {
    background: var(--surface-page);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 12px 14px;
    overflow-x: auto;
}
.markdown pre code { background: none; border: none; padding: 0; }
/* A JSON block's runs, classified by the renderer itself (issue #1227), so the colour travels with the HTML. */
.md-tok-key { color: #7dd3fc; }
.md-tok-string { color: #86efac; }
.md-tok-number { color: #fcd34d; }
.md-tok-literal { color: #c4b5fd; }
.md-tok-punct { color: var(--text-muted); }
/* A copyable block (issue #1233): the button sits in the block's corner, shown on hover or focus, and reads
   "Copied" for a moment after a click. */
.md-codeblock { position: relative; }
.md-codeblock .md-copy {
    position: absolute;
    top: 8px;
    right: 8px;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 6px;
    font-size: .75rem;
    line-height: 1;
    color: var(--text-muted);
    background: var(--surface-raised);
    border: 1px solid var(--border);
    border-radius: 6px;
    cursor: pointer;
    opacity: 0;
    transition: opacity .15s;
}
.md-codeblock:hover .md-copy, .md-codeblock .md-copy:focus-visible, .md-codeblock .md-copy.md-copied { opacity: 1; }
.md-codeblock .md-copy:hover { color: var(--text); border-color: var(--accent-bright); }
.md-codeblock .md-copy.md-copied { color: var(--success); }
.md-codeblock .md-copy svg { width: 14px; height: 14px; }
.markdown a, .markdown-inline a { color: var(--accent-bright); }
.markdown blockquote {
    margin: 12px 0;
    padding-left: 12px;
    border-left: 3px solid var(--border);
    color: var(--text-muted);
}
.markdown hr { border: none; border-top: 1px solid var(--border); }

/* Markdown pipe tables (issue #547): the same look as an operator `.op-table` -- collapsed borders, left-aligned
   header/cells, a row rule, top-aligned cells -- and wrapped in an overflow-x box so a wide table scrolls inside
   the page rather than pushing it sideways. Emitted by the renderer as `<div class="md-table-scroll"><table>`. */
.md-table-scroll { overflow-x: auto; max-width: 100%; margin: 12px 0; }
.markdown table { border-collapse: collapse; width: 100%; }
.markdown th, .markdown td {
    text-align: left;
    padding: 6px 10px;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
    /* `.markdown` sets `overflow-wrap: anywhere`, which zeroes a cell's min-content width so table auto-layout
       collapses a column below its longest word and then splits the word (issue #547). `break-word` wraps only
       when a word genuinely cannot fit, so an ordinary word stays whole. */
    overflow-wrap: break-word;
}
.markdown th { color: var(--text-dim); font-weight: 600; }
/* A delimiter-driven alignment (`:--`/`--:`/`:-:`) rides an inline `style` on the cell, which already wins over
   the left default above -- no extra rule needed. */

.card {
    width: min(560px, 92vw);
    padding: 32px 36px;
    border-radius: 16px;
    background: var(--surface-raised);
    box-shadow: 0 20px 60px rgba(0, 0, 0, .45);
}

/* Vertical rhythm for the hand-written widget-groups (the auth flow, the profile page), whose rows would
   otherwise stack flush against each other -- `.row` only spaces its contents horizontally. Applied through
   `.card` rather than to `.row` itself, because `.row` is also the display engine's (SchemaForm, the endpoint
   catalog) and density is right there: those list many fields at once, these ask for two or three. */
.card > * + * { margin-top: 14px; }
/* Headings keep the section spacing they have everywhere else: a clear break above the new section (which the
   rhythm rule above would otherwise flatten), and tight to the block they introduce. */
.card > h2 { margin-top: 28px; }
.card > h1 + *, .card > h2 + * { margin-top: 8px; }
/* A hint captions the block above it, so in a card it takes the rhythm rather than `.type-hint`'s own 2px --
   which is meant for the display engine's dense field rows, and which ties with the rule above on specificity
   (both one class) and so would otherwise win on source order alone. Consecutive hints stay a tight group. */
.card > .type-hint { margin-top: 14px; }
.card > .type-hint + .type-hint { margin-top: 2px; }
/* A subtitle after a control row -- the raw editor's page note under its back link -- takes the rhythm too, for
   the same specificity tie as the hint above: `.subtitle` zeroes its own margin and, being one class like the
   rhythm rule, wins on source order. Matches only after a `.row`, so a subtitle under a heading keeps the
   heading's tight spacing (issue #694 review). */
.card > .row + .subtitle { margin-top: 14px; }
/* The endpoint browser needs room for the table and wide responses. */
.card.wide { width: var(--content-width); }
/* A table-first page: the content is a grid of many columns, so it takes the window rather than a chosen
   width. Separate from `.wide` rather than widening it, because `--content-width` is a deliberate figure for a
   content page -- a form reads badly at 1600, and a ten-column table reads badly at 1400 (issue #462).
   `calc` rather than a flat cap so a large monitor is actually used; the cap keeps a line of text readable.

   This only pays off because the controls inside declare their own widths. While they did not, every pixel
   this rule adds went to the search boxes -- one reached 816px -- and the columns stayed exactly as cramped. */
.card.full { width: min(1700px, calc(100vw - 48px)); }
/* ...and when the window is narrower than the table wants, the table scrolls **inside** the card rather than
   spilling over its edge -- which is what it did, rows running past the rounded corner and off the screen.
   Pre-existing (ten columns never fitted a phone) and made more visible by the three columns #462 added, so
   it is fixed here rather than left as the cost of them. `auto`, so the scrollbar exists only when needed. */
.card .ant-table-wrapper { overflow-x: auto; }

h1 { margin: 0 0 4px; font-size: 1.6rem; }
h2 { margin: 28px 0 12px; font-size: 1.05rem; color: var(--text-muted); }
.subtitle { margin: 0; color: var(--text-muted); }
.row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
/* The per-row action buttons in the forms list (issue #417): kept on one line and tightly spaced, since a
   table cell is narrow and the confirm ("Delete? Yes Cancel") must read as one group. */
.row-actions { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }

button {
    cursor: pointer;
    border: none;
    border-radius: 10px;
    padding: 10px 18px;
    font-size: 1rem;
    font-weight: 600;
    background: var(--accent);
    color: white;
}
button:hover { background: var(--accent-bright); }
button.ghost { background: transparent; color: var(--text-muted); border: 1px solid var(--border); }

.count { min-width: 48px; text-align: center; font-size: 1.5rem; font-weight: 700; }
/* A text box, not a checkbox or radio: antd draws those over an invisible native input stretched across the box,
   and this rule's min-width once stretched it 180px wide, so a click beside a checkbox -- in the label field of
   Design View's "Choices on this form" -- toggled the box instead (issue #1048). Inside `:where` so the rule keeps a
   bare `input`'s specificity, which antd's own `.ant-input` styles are meant to beat. */
input:where(:not([type=checkbox]):not([type=radio])) {
    flex: 1;
    min-width: 180px;
    padding: 10px 14px;
    border-radius: 10px;
    border: 1px solid var(--border);
    background: var(--surface-page);
    color: var(--text);
    font-size: 1rem;
}
/* An expected, user-facing error: plain red copy, styled as part of the page. Named for what it is rather
   than where it started -- it was `.todo-error` until the Todo demo it was written for went away, by which
   point every page in the app was using it. */
.error-text { color: var(--danger); margin-top: 12px; }

/* A raw / internal error (issue #111): unlike an expected `.error-text`, it is deliberately set apart -- boxed
   and monospaced -- so it reads as an unexpected, developer-facing message rather than designed copy. */
.internal-error {
    color: var(--text-dim);
    margin-top: 12px;
    padding: 8px 12px;
    border: 1px dashed var(--danger);
    border-radius: 8px;
    background: rgba(248, 113, 113, .08);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 0.85rem;
}

/* A page that failed to render (issue #223). Marked out with the danger border so it is plainly a fault
   rather than content, but still a card inside the shell -- the navigation above it is deliberately intact,
   so the page is what was lost, not the app. */
.error-panel {
    border: 1px solid var(--danger);
    background: rgba(248, 113, 113, .06);
}
/* The fallback's escape actions (issue #517 follow-up): Home (primary) to leave the broken page, Reload (ghost)
   to retry it. A row so they sit together beneath the copy. */
.error-panel-actions { display: flex; gap: 12px; margin-top: 16px; }
/* An <a> that reads as the primary button -- Home is a navigation, so it stays a link, but looks actionable. */
.button-link {
    display: inline-block;
    padding: 10px 18px;
    border-radius: 10px;
    border: 1px solid transparent;
    background: var(--accent);
    color: white;
    font-weight: 600;
    text-decoration: none;
}
.button-link:hover { background: var(--accent-bright); }

/* The schema display engine: one field per row, label + widget or read-only value. */
.schema-form { width: 100%; }
.field-label { font-weight: 600; min-width: 140px; color: var(--text-dim); }
.field-required { color: var(--danger); }
.field-value { color: var(--text); }
.field-type { color: var(--text-faint); font-style: italic; font-size: .85rem; }
/* A supplied default not yet accepted (issue #710): a quiet chip on the field container marking that the value
   came from the caller's account, not from them. Muted rather than shouting -- it is a nudge to confirm, not an
   error -- and on the container so it reads the same beside a text box, a checkbox or a date. */
.field-suggested {
  flex: 0 0 auto; align-self: center;
  font-size: .7rem; font-weight: 600; letter-spacing: .02em;
  padding: 1px 6px; border-radius: 8px;
  color: var(--text-dim); background: var(--surface-raised); border: 1px solid var(--border);
}
.json-value { flex: 1 1 100%; margin: 4px 0 0; white-space: pre; }
.nested { margin: 4px 0 8px 16px; padding-left: 12px; border-left: 2px solid var(--border); }

/* Friendly forms (issue #694 review): a field's description captions it and stays tight (0), but the NEXT field
   must not run straight into that description -- give each field group room to read as a group. Scoped to the
   friendly (data-entry / read) form through the `friendly` class SchemaForm sets on its root, so the
   wire-documenting catalog keeps the density the rhythm note on `.card` defends; and applied at any depth (a
   descendant, not a child, combinator), because the raw editor's fields sit inside nested entry blocks and a
   description-then-row pair is a group boundary wherever it occurs. A row straight after a row (no
   description) stays flush. */
.schema-form.friendly .subtitle + .row,
.schema-form.friendly .type-hint + .row { margin-top: 12px; }

/* The survey workflow form's sections (issue #694 review). Nothing inside a task is a direct child of the card,
   so none of it took the card's rhythm: the task's Save sat flush under its last field, a trait's layout label
   sat 2px under the task label where an h2 heading gets 28, and one task ran into the next at an ordinary 14.
   A task is a section (28, like an h2); a layout label heads its trait like the h2 it replaces (28/12); the
   Save takes the rhythm the raw editor's Save, a direct card child, already has (14). */
.wf-task + .wf-task { margin-top: 28px; }
.wf-trait > .markdown { margin: 28px 0 12px; }
.wf-task > .row { margin-top: 14px; }

/* The workflow form's one header line (issue #719): the back link, the title beside it in slightly smaller
   type, and the actions (Edit / Done, Raw edit, the saved note) right-aligned -- three bands become one row, so
   a large form starts higher. The back link drops its own bottom margin (it is a line item here, not a band),
   and the row that follows takes the card's ordinary rhythm. Wraps on a narrow card: the actions fall under
   the title rather than off the edge. */
.wf-header { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.wf-header .back-link { margin: 0; }
.wf-header h1 { margin: 0; font-size: 1.25rem; }
.wf-header .wf-actions { margin-left: auto; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
/* The rail's panel opens on its trait heading (issue #719): the task label is the rail entry the user just
   clicked, so the panel does not repeat it, and the first heading needs no section gap under the header line. */
.wf-panel .wf-task > :first-child { margin-top: 0; }
.wf-panel .wf-trait:first-of-type > h2, .wf-panel .wf-trait:first-of-type > .markdown { margin-top: 0; }

/* The survey's task rail (issue #700): a multi-task survey shows ONE task at a time, chosen from a left rail
   whose entries carry the task's status mark and, orthogonally, an unsaved badge. Single-task surveys and the
   creation form keep the single panel. The items are real buttons (keyboard reachable, `aria-current` on the
   open one), restyled from the global `button`. */
.wf-layout { display: flex; gap: 28px; align-items: flex-start; }
.wf-rail { flex: 0 0 220px; display: flex; flex-direction: column; gap: 4px; }
.wf-panel { flex: 1 1 auto; min-width: 0; }
.wf-rail-item {
    display: flex; align-items: center; gap: 10px; width: 100%;
    padding: 8px 10px; border-radius: 8px; text-align: left; font: inherit;
    background: transparent; color: var(--text-muted); border: 1px solid transparent; cursor: pointer;
}
.wf-rail-item:hover { background: rgba(255, 255, 255, .05); color: var(--text); }
.wf-rail-item[aria-current="true"], .wf-rail-item.active { background: rgba(255, 255, 255, .07); color: var(--text); border-color: var(--border); }
.wf-rail-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The status mark, in the status chip's colours. "Incomplete" is a hollow ring in a faint colour, not a warning
   colour, so a task that has not been started does not read as an error. */
.wf-mark { flex: 0 0 auto; font-weight: 700; width: 1.1em; text-align: center; }
.wf-mark-complete { color: var(--success); }
.wf-mark-invalid { color: var(--border-admin); }
.wf-mark-incomplete { color: var(--text-faint); }
/* Unsaved edits: an overlay badge beside the mark -- a task can be both incomplete and unsaved. */
.wf-unsaved { flex: 0 0 auto; color: var(--border-admin); font-size: .7rem; }

/* Operator diagnostic pages (issue #540): the schema-driven presentation hints. */
.op-verdict { font-weight: 600; margin: 8px 0 2px; }
.op-asof { color: var(--text-faint); font-size: .85rem; margin: 0 0 12px; }

/* presentation:identifier -- an id/hash/path/env-var name, scanned character by character. */
.op-identifier { font-family: monospace; }

/* A read-only scalar array whose items carry a hint (issue #540): the hinted elements laid out inline. */
.op-list { display: inline-flex; flex-wrap: wrap; gap: 6px; align-items: center; }

/* presentation:status -- a verdict chip, coloured by its PSTAT value. */
.op-status {
    display: inline-block;
    padding: 1px 8px;
    border-radius: 10px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .04em;
}
.op-status.ok { background: rgba(74, 222, 128, .16); color: var(--success); }
.op-status.info { background: rgba(129, 140, 248, .16); color: var(--accent-bright); }
.op-status.warning { background: rgba(217, 119, 6, .18); color: var(--border-admin); }
.op-status.error { background: rgba(248, 113, 113, .16); color: var(--danger); }
/* A status chip that is also a link (issue #694): the forms list's Needs Info / Invalid chip goes straight into
   the survey editor. Keeps the chip look; underlines on hover so it reads as a link. */
a.op-status { text-decoration: none; cursor: pointer; }
a.op-status:hover { text-decoration: underline; }
/* A status chip that opens something (issue #789): the forms list's Needs Review / Finished chip is a button that
   pops up the workflows behind it. The button's own chrome is reset so it keeps the chip look. */
button.op-status { border: none; font-family: inherit; cursor: pointer; }
button.op-status:hover { text-decoration: underline; }
button.op-status:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 1px; }
/* The popover's list: one workflow per line, its name then what it asks of you. */
.chip-workflows { margin: 0; padding-left: 18px; max-width: 360px; }
.chip-workflows li { margin: 2px 0; }

/* The forms list's workflow column (issue #791): one workflow per line, its name as a link and its category dim
   beside it; an ineligible one's name is a button opening its reasons, styled as the same link. */
.wf-cell-item { line-height: 1.35; margin: 1px 0; }
.wf-cell-link, button.wf-cell-link, button.wf-cell-more {
    background: none; border: none; padding: 0; font: inherit; cursor: pointer;
    color: var(--accent-bright); text-decoration: none;
}
.wf-cell-link:hover, button.wf-cell-more:hover { color: var(--accent-brighter); text-decoration: underline; }
.wf-cell-link:focus-visible, button.wf-cell-more:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 1px; }
.wf-cell-cat { color: var(--text-dim); font-size: .8rem; }
/* A user's forms and client (issue #1081), on a row of the Users list and in the editor: one line, never wrapped --
   two short links read as a pair only side by side. */
.user-links { white-space: nowrap; }
button.wf-cell-more { font-size: .8rem; margin-top: 2px; }
.wf-cell-all { max-width: 320px; }
.wf-reasons { margin: 0; padding-left: 18px; }
/* A workflow's help under its title, and the home page's note under its intro (issue #1221): copy, read as such. */
.wf-help { color: var(--text-muted); margin: 4px 0 8px; }
.wf-help p, .home-note p { margin: 4px 0; }
.home-note { margin-top: 12px; }
.wf-reasons li { margin: 4px 0; }

/* presentation:table -- an array rendered as rows/columns. */
.op-table { width: 100%; border-collapse: collapse; margin-top: 8px; }
.op-table th, .op-table td {
    text-align: left;
    padding: 6px 10px;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
}
.op-table th { color: var(--text-dim); font-weight: 600; font-size: .85rem; }
/* A numeric column (issue #905): its header and cells right-aligned, so counts line up by magnitude. */
.op-table th.op-num, .op-table td.op-num { text-align: right; }
/* A long value kept to one line in a cell (the Clients page's copy tables, issue #917): clamped after the Markdown
   is rendered, with the whole in the cell's title, so no cut ever lands inside a link or an emphasis. */
.op-table .cell-clamp { display: inline-block; max-width: 360px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
.op-table .cell-clamp p { display: inline; margin: 0; }
/* A cell shows the value, not the wire's "(type)" annotation the catalog outline wants. */
.op-table td .field-type { display: none; }
/* A wide/tall table scrolls inside this box rather than pushing past the card (issue #540). The height is
   capped so a tall table's horizontal scrollbar stays on screen -- otherwise it sits far below the fold and a
   table wider than the box just looks truncated. The scrollbar is kept visible (not the platform's
   hide-until-scroll overlay) so "there is more to the right/below" is advertised rather than hidden. */
.op-table-scroll {
    overflow: auto;
    max-width: 100%;
    max-height: 70vh;
    scrollbar-width: thin;
    scrollbar-gutter: stable;
}
.op-table-scroll::-webkit-scrollbar { height: 10px; width: 10px; }
.op-table-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 5px; }
.op-table-scroll::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
.op-table-scroll::-webkit-scrollbar-track { background: rgba(255, 255, 255, .04); border-radius: 5px; }
/* The header stays put while a tall table scrolls vertically within its box. Needs a solid background to cover
   the rows sliding under. */
.op-table-scroll > .op-table > thead > tr > th {
    position: sticky;
    top: 0;
    background: var(--surface-raised);
    z-index: 1;
}
/* But a *nested* sub-table's header must NOT stick: its scroll box has `overflow: visible`, so its sticky
   header resolves against the outer scroll box and would pin over the main header while scrolling. Higher
   specificity (the extra `.op-table` ancestor) than the rule above, so it wins for nested tables only. */
.op-table .op-table-scroll > .op-table > thead > tr > th { position: static; }
/* A nested sub-table (a cell's columns/indexes) gets no box of its own: no height cap, no inner scrollbar --
   it just widens its cell, and the outer box does the scrolling. */
.op-table .op-table-scroll { max-height: none; overflow: visible; margin: 0; }
/* A JSON cell (a free-form value) wraps and is width-capped, so one blob does not force the table enormously
   wide before the scroll box even helps. */
.op-table td .json-value { white-space: pre-wrap; max-width: 300px; margin: 0; }
/* A nested sub-table (a structured-array cell, e.g. a table's columns/indexes) is a touch tighter. */
.op-table td .op-table { margin-top: 0; font-size: .92em; }

/* A master-detail row (issue #540): a `presentation: detail` array shown as its own labelled sub-table on a
   full-width row beneath the main row, so a heavy nested array does not force the main row very wide. */
.op-detail-row > td { padding: 4px 10px 14px 24px; background: rgba(255, 255, 255, .02); }
.op-detail { margin-top: 8px; }
.op-detail:first-child { margin-top: 2px; }
.op-detail-label { color: var(--text-dim); font-size: .85rem; font-weight: 600; margin-bottom: 2px; }
.type-hint { color: var(--text-faint); font-style: italic; margin: 2px 0; }

/* A listing page's index (the Operator overview, the Documents page; issues #540, #554): a list of readable
   accent links, each with a line on what it opens -- the same shape as the debug index. */
.index-list { margin: 8px 0 0; padding-left: 22px; }
.index-list li { margin: 6px 0; }
.index-list a { color: var(--accent-bright); text-decoration: none; }
.index-list a:hover { text-decoration: underline; }

/* A free-form operator object (issue #540): titled sections and key/value rows. */
.op-section-label { font-weight: 600; color: var(--text-dim); margin: 10px 0 2px; }
.op-kv { display: flex; gap: 10px; padding: 2px 0; }
.op-kv-key { min-width: 180px; color: var(--text-dim); }
.op-kv-val { color: var(--text); word-break: break-word; }

/* Cache-state page (issue #540): the node line and the small reload buttons. */
.op-node { color: var(--text-dim); margin: 2px 0 0; }
.op-reload {
    font-size: .82rem;
    padding: 3px 12px;
    background: var(--surface-raised);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 6px;
    cursor: pointer;
}
/* Set `background` explicitly in both states: the global `button:hover` (0,1,1) otherwise outranks the base
   `.op-reload` (0,1,0) and paints every reload button accent-bright on hover -- a disabled one included. */
.op-reload:hover:not(:disabled) { background: var(--border); border-color: var(--accent-bright); }
.op-reload:disabled { opacity: .5; cursor: default; background: var(--surface-raised); }
/* A per-row "reload pending" chip sits after the shared date, so give it a little separation. */
.op-table td .op-status { margin-left: 8px; }
.form-ok { color: var(--success); margin-top: 12px; }
/* "Edited since the last check": neither pass nor fail but unknown, so neither the success green nor the
   danger red -- a dim tone that reads as a prompt rather than a verdict, in the same slot as the other two so
   the three states swap in place. */
.form-stale { color: var(--text-muted); font-style: italic; margin-top: 12px; }
/* The supplied-defaults prompt above a task's Save (issue #710): a quiet caption, not a verdict -- it asks the
   reader to confirm values filled from their account, so it reads as a nudge rather than success or error. */
.form-prefill-summary { color: var(--text-dim); margin-top: 12px; }

/* A field that failed validation (issue #197): the message sits under the field, and the row it belongs to is
   marked so the label and the control can pick the state up. The message carries the meaning on its own --
   colour is the fast signal, not the only one, so the state survives being read by someone who cannot use it. */
.field-error { color: var(--danger); margin: 2px 0 6px 0; font-size: .85rem; }
/* An entry in the failure listing, which jumps to its field. A real <button> for the keyboard and screen
   reader, stripped back to read as the line of text it replaced -- the affordance is the underline on hover,
   not a button chrome that would make the list look like a toolbar. */
.failure-jump {
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}
.failure-jump:hover { text-decoration: underline; }
/* A row is focusable only so a jump can land on one that has no control of its own; without a visible ring
   the focus would move somewhere the eye cannot follow. */
.row:focus-visible, .failure-jump:focus-visible {
    outline: 2px solid var(--accent-bright);
    outline-offset: 2px;
}
.field-invalid .field-label { color: var(--danger); }
/* Reaching into antd's rendered controls by class is the only handle they offer from outside; `:where` keeps
   these at zero specificity so antd's own focus/hover states still win while the control is being used. */
.field-invalid :where(input, textarea, .ant-select-selector, .ant-picker) {
    border-color: var(--danger);
}
/* The box is shared with `.markdown pre` above; only its text differs. */
.code { font-size: .85rem; color: var(--text-dim); }

/* The editable request JSON: the same inset well as `.code`, but a textarea -- so it carries a caret, a focus
   ring and a resize grip, and reads as something you can type in rather than a rendering you cannot. Full
   text colour (not `.code`'s dim) for the same reason: this is input, not output. */
textarea.json-edit {
    display: block;
    width: 100%;
    min-height: 8rem;
    resize: vertical;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    color: var(--text);
    line-height: 1.45;
}
textarea.json-edit:focus {
    outline: none;
    border-color: var(--accent-bright);
}
/* A piece of copy being edited (the Clients page, issue #918): the same well (`.code .json-edit`, both classes --
   `.code` is what draws it), in the page's own face rather than monospace, since the value is prose -- Markdown
   someone reads -- not code. */
textarea.copy-edit { font-family: inherit; min-height: 5rem; }
/* The copy, file by file (issue #1062). A file is a panel of its own, so its keys read apart from the page around them:
   a shade darker than the card (an inset, like a document opened inside the page), edged and headed in a quiet tint of
   the indigo accent, the header naming the file, where it is shown and how many keys it has. Inside, each key is a
   block -- its address, what the client's configuration did, its actions on one line, then its value in a box that
   scrolls on its own past a few lines, so a whole file reads down the page without one long value taking it over.
   The value boxes take the card's own surface, lighter than the panel, so they stand off it. A pending change is
   marked down its left edge in the accent, a key a refused save names in the danger red. */
.copy-view-controls { margin: 8px 0 4px; }
.copy-file {
    --copy-panel: color-mix(in oklab, var(--surface-raised), var(--surface-page) 75%);
    margin-top: 14px;
    border: 1px solid color-mix(in oklab, var(--accent-bright) 50%, var(--border));
    border-radius: 10px;
    background: var(--copy-panel);
    overflow: hidden;
}
.copy-file-head {
    display: flex;
    align-items: baseline;
    gap: 12px;
    flex-wrap: wrap;
    padding: 10px 16px;
    background: color-mix(in oklab, var(--accent) 16%, var(--surface-raised));
    border-bottom: 1px solid color-mix(in oklab, var(--accent-bright) 35%, var(--border));
}
.copy-file-name { font-weight: 600; color: var(--text); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.copy-file-where { color: var(--text-dim); font-size: .9rem; }
.copy-file-count { margin-left: auto; color: var(--text-muted); font-size: .85rem; white-space: nowrap; }
.copy-file-note { margin: 10px 16px 0; }
.copy-namespace { padding: 0 16px 6px 10px; }
.copy-namespace > h4 {
    margin: 14px 0 4px 6px;
    font-size: .8rem;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.copy-key { padding: 8px 0 10px 10px; border-top: 1px solid var(--border); border-left: 3px solid transparent; }
.copy-key-pending { border-left-color: var(--accent-bright); }
.copy-key-refused { border-left-color: var(--danger); }
.copy-key-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 4px; }
.copy-key-head > code { font-size: .9rem; }
.copy-key-status { font-size: .8rem; color: var(--text-muted); }
.copy-key-status-refused { color: var(--danger); }
.copy-key-actions { margin-left: auto; display: inline-flex; align-items: center; gap: 2px; }
.copy-value {
    max-height: 12rem;
    overflow-y: auto;
    padding: 6px 10px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface-raised);
}
.copy-value > .markdown > :first-child { margin-top: 0; }
.copy-value > .markdown > :last-child { margin-bottom: 0; }
pre.copy-value-raw { margin: 0; white-space: pre-wrap; word-break: break-word; font-size: .85rem; }
.copy-value-empty { margin: 0; }
.copy-key-was { margin-top: 6px; font-size: .85rem; }
.copy-key-was > summary { cursor: pointer; color: var(--text-muted); }
.copy-key-was > p { margin: 6px 0 2px; }
.copy-key > textarea.copy-edit { margin-top: 4px; }
.copy-key .type-hint { margin: 4px 0; }
/* Pending changes are saved from a bar that stays in view at the bottom of the window while the file scrolls. */
.copy-save-bar {
    position: sticky;
    bottom: 0;
    z-index: 5;
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    margin-top: 12px;
    padding: 10px 14px;
    border: 1px solid var(--accent-bright);
    border-radius: 8px;
    background: var(--surface-raised);
    box-shadow: 0 -8px 24px rgba(0, 0, 0, .35);
}
.copy-save-bar > .type-hint, .copy-save-bar > .error-text, .copy-save-bar > .internal-error { flex-basis: 100%; margin: 0; }
/* Unapplied edits: the well is visibly pending until the change is loaded into the form. */
textarea.json-edit.edited { border-color: var(--accent-bright); }
/* The same well inside a form row (issue #251). A free-form map field is usually a handful of keys, so it
   opens at a few lines rather than the request panel's full height -- still resizable for the ones that are
   not. */
textarea.json-edit.json-field { min-height: 4.5rem; }

/* The forms list's scope bar (issue #562) -- whose forms an administrator is looking at -- takes the
   elevated-privilege tint the app bar uses for the same condition, so it reads as the administrative control it
   is and not as one more filter. */
.forms-scope {
    padding: 8px 12px;
    border: 1px solid var(--border-admin);
    border-radius: 8px;
    background: var(--surface-admin);
}

.forms-scope-label { color: var(--text-muted); white-space: nowrap; }
/* Who a scope given as an id is (issue #1081): a line of its own under the bar's controls, in the page's own text
   colour -- it is the answer to "whose forms", not a hint. */
.forms-scope-who { flex-basis: 100%; color: var(--text); }

/* The collapsible filter shell the listing pages share (issues #562, #683; see FilterPanel.kt): an inset well
   of controls, closed by default, and -- while it is closed -- the filters in force summarized as chips, which
   borrow the .bar-badge shape but not its shouting uppercase, since they quote what the person typed. */
.filter-chips { display: flex; flex-wrap: wrap; gap: 6px; }

.filter-chip {
    padding: 2px 8px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface-page);
    font-size: 12px;
    color: var(--text-muted);
    white-space: nowrap;
}

.filter-well {
    display: grid;
    /* `min(…, 100%)` keeps a track from exceeding the well on a narrow viewport: a fixed minimum wider than the
       well pushed the whole page wide on a phone (measured, issue #683). A from-to pair wider than its track
       wraps instead -- see .filter-range. */
    grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
    gap: 12px 24px;
    padding: 12px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--surface-page);
}
.filter-well .filter-actions { grid-column: 1 / -1; }

.filter-group { display: flex; flex-direction: column; gap: 4px; }
/* A group that begins a new row of the well (`filterGroup(startsRow = true)`): the first of the taller date ranges. */
.filter-well .filter-row-start { grid-column-start: 1; }

.filter-label { font-size: 12px; color: var(--text-muted); }

/* A from-to pair wraps rather than overflowing its track when the window is narrower than both ends want. */
.filter-range { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }

/* The User column's second line: the email under the name, quieter so the name leads. */
.owner-email { display: block; font-size: 12px; color: var(--text-muted); }
/* Which of the person's users owns the form (issue #1097): a quiet pill after the cell's first line, the look of the
   form's own suggested-value chip, so it reads as a label about the user rather than part of their name. */
.owner-persona {
  display: inline-block; margin-left: 2px; vertical-align: 1px; white-space: nowrap;
  font-size: .7rem; font-weight: 600; letter-spacing: .02em;
  padding: 1px 6px; border-radius: 8px;
  color: var(--text-dim); background: var(--surface-raised); border: 1px solid var(--border);
}

/* The just-saved form, flashed once on arrival back at the list from the edit page (issue #592): the row's
   cells fade from the accent tint to nothing, so the eye is drawn to it without leaving a persistent mark.
   One-shot -- the page clears the highlight after the animation, so it never repeats on a reload or a page. */
@keyframes forms-row-flash {
    from { background-color: var(--accent); }
    to { background-color: transparent; }
}
.forms-row-highlight > td { animation: forms-row-flash 2s ease-out; }

/* A configuration row that wants attention (issue #1026): one offered Publish, which a note above the table sends
   people to. The warning chip's tint, so it reads as "look here", not as an error. */
.op-table tr.op-row-attention > td { background: rgba(217, 119, 6, .16); }

/* The workflow pages (issue #792): counts right-aligned under their headings, so the columns read as numbers. */
.wf-aggregate .wf-count { text-align: right; width: 90px; }
.wf-aggregate th.wf-count { text-align: right; }

/* The raw editor's trait-lock notice (issue #857): which traits are locked for you and by what, with the override
   control beneath it for someone a lock allows. Set apart so it reads before the form it constrains. */
.lock-notice { border-left: 3px solid var(--accent-bright); padding: 6px 10px; margin: 8px 0 12px; }
.lock-notice p { margin: 0 0 6px; }
/* The override control, by Save (issue #857 UI pass), where the refusal it answers is drawn. */
.lock-override { margin-top: 12px; }
.lock-override .ant-input { max-width: 480px; margin-top: 6px; display: block; }
/* A note beside a list element's header -- "Locked by Audit review" on a raw-editor section the caller cannot change. */
.element-note { color: var(--accent-bright); font-size: 0.9em; margin-left: 10px; }

/* Task displays and approvals (issue #832). A disabled display greys the step and its rail entry, which is no way
   in; a text display stands in for the step's rendering; an approval shows its prompt above the button. */
.wf-task-disabled { opacity: .55; }
.wf-rail-item.disabled, .wf-rail-item:disabled { opacity: .5; cursor: not-allowed; }
.wf-rail-item:disabled:hover { background: none; color: inherit; }
.wf-display-text { margin: 4px 0 8px; }
.wf-approval > .row { margin-top: 12px; }

/* --- Design View (issue #972) ---------------------------------------------------------------------------------
   An administrator's mode that marks each part of a workflow form with the definition behind it. Its own hue --
   teal -- so it reads as neither the indigo accent (an action) nor the amber admin cue (a standing privilege): it is
   a lens over the page, and the marks have to stay quiet enough that the form being looked at still reads as itself.
   Outlines appear on hover, or all at once with "show all ids"; the page makes room for the inspector rather than
   being covered by it. */
:root {
    --design: #2dd4bf;
    --design-soft: rgba(45, 212, 191, .14);
    --dv-panel-width: 380px;
}

/* The Shadow Sandbox (issue #931). Outside a sandbox, "Open sandbox" is a quiet outline chip beside the admin cue: a
   door, not an alarm. Inside one, the "Acme Sandbox" chip takes its place, filled and dashed so it reads unlike any
   other badge -- and in words, not colour alone. The bar is fixed, so it is on screen however far a page scrolls.
   Clicked, it opens a popover saying what the sandbox is, with the way back. */
.sandbox-open {
    background: transparent;
    border-color: var(--border-admin);
    color: var(--text);
    cursor: pointer;
}
.sandbox-chip {
    background: var(--surface-admin);
    border: 1px dashed var(--border-admin);
    color: var(--text);
    cursor: pointer;
}
.sandbox-info {
    min-width: 18rem;
    max-width: 24rem;
    padding: 12px 14px;
    font-size: 13px;
    line-height: 1.45;
}
.sandbox-info p { margin: 0 0 10px; }
.sandbox-back {
    padding: 2px 10px;
    border: 1px solid var(--border-admin);
    border-radius: 10px;
    background: var(--surface-raised);
    color: var(--text);
    font-size: 12px;
    cursor: pointer;
}

.design-badge {
    border-color: var(--border);
    border-style: dashed;
    background: transparent;
    color: var(--text-dim);
    cursor: pointer;
}
.design-badge.on {
    border-style: solid;
    border-color: var(--design);
    background: var(--design-soft);
    color: var(--design);
}

/* A selectable part of the page. The outline is drawn on the innermost part under the pointer only, so hovering a
   field inside a section outlines the field and not the section around it as well. */
.dv-target { position: relative; border-radius: 6px; outline: 1px dashed transparent; outline-offset: 3px;
    transition: outline-color .12s ease; cursor: pointer; }
.dv-target:hover:not(:has(.dv-target:hover)), .dv-target.dv-pinned { outline-color: rgba(45, 212, 191, .55); }
.dv-target.dv-selected { outline: 2px solid var(--design); }
/* Fields sit close together on a form; in Design View each gets a little air so outlines and badges do not collide. */
.schema-form .dv-target { margin: 4px 0; }
/* With every id shown, each outlined part takes more room inside and out: 3px of padding, so a part's children sit
   clear of its own outline, and a couple of pixels more outside on every side -- every boundary, parents included --
   so neighbouring outlines separate and a child stands off its parent's left and right walls too. A ghost keeps its
   own, larger padding. */
.dv-target.dv-pinned:not(.dv-ghost) { padding: 3px; }
.dv-target.dv-pinned { margin: 11px 2px; }

/* The badge naming the part: hidden until its part is hovered, focused or pinned, so the form keeps its own look. */
.dv-badge {
    position: absolute;
    top: -10px;
    right: 8px;
    z-index: 2;
    padding: 0 6px;
    border: 1px solid var(--design);
    border-radius: 8px;
    background: var(--surface-page);
    color: var(--design);
    font: 600 10px/16px ui-monospace, SFMono-Regular, Menlo, monospace;
    cursor: pointer;
    opacity: 0;
    transition: opacity .12s ease;
}
.dv-target:hover:not(:has(.dv-target:hover)) > .dv-badge, .dv-pinned > .dv-badge, .dv-badge:focus-visible,
.dv-selected > .dv-badge { opacity: 1; }
.dv-selected > .dv-badge { background: var(--design); color: var(--surface-page); }

/* A field the form leaves out, drawn only in Design View: hatched and dimmed, so it never reads as a disabled field
   (which is greyed for real users) -- a ghost is something they do not see at all. */
.dv-ghost {
    margin: 6px 0;
    padding: 4px 8px;
    outline: 1px dashed var(--text-faint);
    background-image: repeating-linear-gradient(135deg, rgba(148, 163, 184, .07) 0 6px, transparent 6px 12px);
    opacity: .72;
}
.dv-ghost-reason { margin: 0 0 2px; color: var(--text-muted); font-size: 12px; font-style: italic; }

/* Room for the inspector: the page narrows instead of sliding under the panel. */
body.dv-open { --content-width: min(1400px, calc(95vw - var(--dv-panel-width))); }
body.dv-open .app-content { padding-right: calc(var(--dv-panel-width) + 16px); }

.dv-inspector {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 15;
    width: var(--dv-panel-width);
    padding: 76px 18px 24px;
    overflow-y: auto;
    background: var(--surface-raised);
    border-left: 1px solid var(--border);
    box-shadow: -8px 0 24px rgba(0, 0, 0, .3);
    font-size: 13px;
}
.dv-inspector h2 { margin: 12px 0 8px; font-size: 18px; }
.dv-inspector h3 { margin: 18px 0 6px; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted); }
.dv-inspector-head { display: flex; align-items: center; justify-content: space-between; }
.dv-title { color: var(--design); font-size: 11px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; }
.dv-clear { border: 0; background: none; color: var(--text-muted); font-size: 18px; cursor: pointer; }
.dv-clear:hover { color: var(--text); }
.dv-switches { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 8px; }
.dv-fact { display: grid; grid-template-columns: 96px 1fr; gap: 8px; padding: 2px 0; }
.dv-fact-name { color: var(--text-muted); }
.dv-fact-value { overflow-wrap: anywhere; }
.dv-inspector .mono, .dv-address { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.dv-address { margin: 0 0 6px; color: var(--text-dim); overflow-wrap: anywhere; }
.dv-origin { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 8px; color: var(--text-dim); }
.dv-pill { padding: 0 8px; border: 1px solid var(--border); border-radius: 10px; font-size: 11px; color: var(--text-muted); }
.dv-pill-own { border-color: var(--design); color: var(--design); }
.dv-note, .dv-caption { color: var(--text-muted); }
.dv-caption { margin: 10px 0 4px; font-size: 12px; }
.dv-callout { padding: 6px 10px; border-left: 3px solid var(--text-faint); background: rgba(148, 163, 184, .08); color: var(--text-dim); }
.dv-json {
    max-height: 320px;
    margin: 0;
    padding: 10px;
    overflow: auto;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--surface-page);
    font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
}
.dv-inspector details { margin-top: 12px; }
.dv-inspector summary { cursor: pointer; color: var(--text-dim); }

/* A narrow window cannot spare a column: the inspector becomes a sheet along the bottom instead. */
@media (max-width: 900px) {
    .dv-inspector { top: auto; left: 0; width: auto; height: 45vh; padding-top: 14px; border-left: 0;
        border-top: 1px solid var(--border); }
    body.dv-open { --content-width: min(1400px, 95vw); }
    body.dv-open .app-content { padding-right: 16px; padding-bottom: calc(45vh + 16px); }
}

/* Design View editing (issue #984). A part whose copy the workflow overrides carries a filled dot on its badge, so
   overrides show without opening each field; the badge itself shows on hover as before. */
.dv-altered > .dv-badge::after { content: " ●"; }
.dv-altered.dv-target:not(:hover) > .dv-badge { opacity: .85; }
.dv-edit { margin-top: 6px; }
.dv-edit-row { display: grid; grid-template-columns: 96px 1fr; gap: 4px 8px; align-items: center; margin: 6px 0; }
.dv-shared, .dv-fallback { grid-column: 2; margin: 0; color: var(--text-faint); font-size: 12px; overflow-wrap: anywhere; }
.dv-choice-label { display: flex; gap: 4px; align-items: center; }
.dv-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.dv-json-edit { width: 100%; }

/* The Reports page (issue #1007): the client's reports down the left, the open one run as a table beside them. */
.reports-head { justify-content: space-between; }
.reports-head h1 { margin: 0 auto 0 0; }
.reports-layout { display: grid; grid-template-columns: minmax(200px, 260px) minmax(0, 1fr); gap: 28px; margin-top: 14px; }
.reports-list ul:not(.wf-reasons) { list-style: none; margin: 0; padding: 0; }
.reports-item { display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border-left: 2px solid transparent; }
.reports-item.current { border-left-color: var(--accent-bright); background: rgba(255, 255, 255, .04); }
.reports-issues-lead { margin: 16px 0 4px; color: var(--text-dim); font-size: 13px; }
.reports-run h2 { margin-top: 0; }
.reports-controls { margin: 10px 0 4px; }
.reports-paging { margin-top: 10px; }
.reports-download { margin: 6px 0 2px; }
.reports-download p { margin: 0; }

/* The Reports page's History view (issue #1037): a report's snapshots as a bar chart over days.
   The series colours are a categorical set in a FIXED order -- a group's colour is its place among the groups, never
   its rank -- validated as a set against the card surface (--surface-raised) for colour-vision separation and
   contrast: a re-order or a new hue has to be validated again, not eyeballed. Slot 6 sits just under 3:1 on the
   card, which is why the table under the chart is always there. Text never wears a series colour; a swatch beside
   it does. */
.reports-run {
    --chart-1: #3987e5;
    --chart-2: #d95926;
    --chart-3: #199e70;
    --chart-4: #c98500;
    --chart-5: #d55181;
    --chart-6: #008300;
    --chart-7: #9085e9;
    --chart-8: #e66767;
}
.rh-c0 { --rh-color: var(--chart-1); }
.rh-c1 { --rh-color: var(--chart-2); }
.rh-c2 { --rh-color: var(--chart-3); }
.rh-c3 { --rh-color: var(--chart-4); }
.rh-c4 { --rh-color: var(--chart-5); }
.rh-c5 { --rh-color: var(--chart-6); }
.rh-c6 { --rh-color: var(--chart-7); }
.rh-c7 { --rh-color: var(--chart-8); }
/* The groups past the chart's limit, folded into one: grey, since it is not a series of its own. */
.rh-other { --rh-color: var(--text-faint); }
.rh-title { margin: 14px 0 6px; font-size: 14px; font-weight: 600; color: var(--text); }
.rh-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 0 0 8px; color: var(--text-dim); font-size: 13px; }
.rh-legend-item { display: inline-flex; align-items: center; gap: 6px; }
.rh-swatch { width: 10px; height: 10px; border-radius: 2px; background: var(--rh-color); flex: none; }
/* Sized in pixels by the component, to the pane's measured width: laid out at its real size, never scaled to it. */
.rh-chart { position: relative; max-width: 100%; transition: opacity .15s; }
/* A refetch keeps the frame: the last chart stays, dimmed, until its replacement arrives. */
.rh-chart.rh-refetching { opacity: .55; }
.rh-svg { display: block; overflow: visible; }
.rh-bar { fill: var(--rh-color); }
.rh-bar.rh-hot { filter: brightness(1.3); }
/* Recessive: hairlines one step off the surface, solid; the zero line a step firmer. */
.rh-grid { stroke: var(--border); stroke-width: 1; opacity: .55; }
.rh-baseline { stroke: var(--text-faint); stroke-width: 1; }
.rh-tick { fill: var(--text-muted); font-size: 11px; font-variant-numeric: tabular-nums; }
.rh-hit { fill: transparent; cursor: default; outline: none; }
.rh-hit:focus-visible { stroke: var(--accent-bright); stroke-width: 1; }
.rh-tip {
    position: absolute; transform: translate(-50%, calc(-100% - 8px)); pointer-events: none; z-index: 2;
    padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface-page);
    color: var(--text-dim); font-size: 12px; white-space: nowrap; box-shadow: 0 4px 12px rgba(0, 0, 0, .35);
}
.rh-tip strong { display: block; color: var(--text); font-size: 14px; }
.rh-tip-series { display: flex; align-items: center; gap: 6px; }
/* A short stroke of the series colour keys the readout's row, where a filled box would be a mark's weight of ink. */
.rh-key { width: 12px; height: 3px; border-radius: 2px; background: var(--rh-color); flex: none; }
.rh-tip-date { color: var(--text-muted); }
.rh-table { margin-top: 12px; }
/* One line per row: a report has many narrow columns, and wrapped timestamps make each row four lines tall. The table
   scrolls sideways inside `.op-table-scroll` instead. */
.reports-run .op-table th, .reports-run .op-table td { white-space: nowrap; }
/* A narrow window stacks the two: the list first, the run under it. */
@media (max-width: 900px) {
    .reports-layout { grid-template-columns: minmax(0, 1fr); }
}

/* The Simulations page (issue #997): one card per simulation, its run's sign-in buttons beneath. */
.sim-card { border-top: 1px solid var(--border); padding: 12px 0 4px; margin-top: 12px; }
.sim-card:first-of-type { border-top: none; }
.sim-users .row { align-items: center; gap: 12px; }
/* The runs this browser remembers (issue #1099): a panel of their own above the simulations, so a run's other users
   are a click away after signing in as one. A run from data since reset is dimmed, and offers no sign-in. */
.sim-recent {
    margin: 12px 0 18px;
    padding: 4px 16px 10px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: color-mix(in oklab, var(--surface-raised), var(--surface-page) 40%);
}
.sim-recent > .row > h2 { margin: 10px 0 4px; }
.sim-recent .sim-card > h3 { margin: 0 0 4px; font-size: 1rem; }
.sim-recent-stale { margin-top: 10px; opacity: .7; }
.sim-recent-stale > p { margin: 2px 0; }
