/* ================================================================
 * Reset & Base
 * ================================================================ */

/* Reduce-motion is answered once here for the whole app: every animation and
   transition is neutralised. A progress indicator overrides with !important. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
        scroll-behavior: auto !important;
    }
}

@media (max-width: 30rem) {
    html {
        font-size: 87.5%;
    }
}

*,
*::before,
*::after {
    box-sizing: border-box;

    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;

    letter-spacing: -.0175rem;

    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

body {
    margin: 0;
    padding: 0;
    font-family: var(--font-sans);
    background: var(--bg-body);
    color: var(--text-body);
    min-height: 100vh;
    text-align: left;
    transition: background-color 0.2s ease, color 0.2s ease;
}

/* Weightless, like every other context default: these are the browser's
   margins being cleared for unskinned content, so a component class and the
   docs flow rules below both still win. At (0,0,1) it silently beat every
   `:where()` rule that positions a block, and a paragraph after a sample or a
   table sat flush against it. */
:where(h1, h2, h3, h4, h5, h6, p, ul, ol) {
    margin: 0;
    padding: 0;
}

button {
    font-family: inherit;
}

/* ================================================================
 * Scrollbars
 * ================================================================ */
.os-scrollbar.os-theme-app {
    box-sizing: border-box;
    --os-size: var(--scrollbar-size, 10px);
    --os-padding-perpendicular: 2px;
    --os-padding-axis: 2px;
    --os-track-bg: transparent;
    --os-track-bg-hover: transparent;
    --os-track-bg-active: transparent;
    --os-handle-interactive-area-offset: 4px;
    --os-handle-border-radius: 999px;
    --os-handle-bg: var(--scrollbar-thumb);
    --os-handle-bg-hover: var(--text-muted);
    --os-handle-bg-active: var(--text-muted);
    --os-handle-min-size: 33px;
}

* {
    scrollbar-width: var(--scrollbar-native-width, thin);
    scrollbar-color: var(--scrollbar-thumb) transparent;
}


/* ================================================================
 * Theme
 * ================================================================ */

:root {
    /* ============ Typography tokens ============
     * Sizes and line-heights travel as a pair (the way M3 / Tailwind ship
     * them); tracking has one caps step. Sizes never appear as literals in
     * rules — a rule reads the token, so a step moves everywhere at once. */
    --font-sans: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

    --fs-caption: 0.6875rem;  --lh-caption: 1.45;
    --fs-sm: 0.75rem;         --lh-sm: 1.5;
    --fs-body: 0.8125rem;     --lh-body: 1.5;
    --fs-base: 0.875rem;      --lh-base: 1.55;
    --fs-lead: 0.9375rem;     --lh-lead: 1.55;
    --fs-title: 1rem;         --lh-title: 1.4;
    --fs-heading: 1.25rem;    --lh-heading: 1.3;
    --fs-display: 1.75rem;    --lh-display: 1.2;

    --ls-caps: 0.05em;

    /* The product accent. Never a store's green: store colors exist only as
       content identity (logos, banners) in constants/stores.ts. */
    --accent: var(--brand-accent);
    --accent-light: var(--brand-accent-light);

    /* The product's own accent, not a store's: used where the page is about the
       service itself. Mirrored by LANDING_ACCENT in constants/landing.ts. */
    --brand-accent: #5b6570;
    --brand-accent-light: #5b657022;

    --gray-50: #f8f9fb;
    --gray-100: #f3f4f6;
    --gray-200: #e5e7eb;
    --gray-300: #d1d5db;
    --gray-400: #9ca3af;
    --gray-500: #6b7280;
    --gray-700: #374151;
    --gray-800: #1f2937;
    --gray-900: #111827;

    --star-on: #f59e0b;
    --star-off: #d1d5db;
    --error-bg: #fef2f2;
    --error-text: #dc2626;

    /* Solid status fills, always under white text — distinct from --error-text
       and the --ui-*-ink text colours; one value in both themes. */
    --fill-info: #2563eb;
    /* Teal, not pure green: keeps a blue cast under red-green colour vision so
       "good" stays distinct from the red "bad". */
    --fill-good: #0d9488;
    --fill-warn: #f59e0b;
    --fill-caution: #b45309;
    --fill-bad: #dc2626;

    --bg-body: var(--gray-50);
    --bg-panel: #ffffff;
    --bg-elev: var(--gray-50);
    /* The ground a field is carved into: below the panel and below the body,
       so an input reads as a well on either. */
    --bg-well: var(--gray-100);
    --bg-elev-hover: var(--gray-100);
    --border-subtle: var(--gray-200);
    --border-muted: var(--gray-100);

    --text-strong: var(--gray-900);
    --text-body: #4b5563;
    --text-muted: #5e6673;
    --text-faint: var(--gray-500);

    --shadow-panel: 0 1px 12px rgba(0, 0, 0, 0.06);
    --shadow-tab: 0 -2px 8px rgba(0, 0, 0, 0.06);
    --shadow-logo: 0 4px 16px rgba(0, 0, 0, 0.18);
    --shadow-media: 0 1px 6px rgba(0, 0, 0, 0.1);
    --shadow-dtp-mark: 0 0 2px 0 var(--ui-panel);
    --shadow-dtp-mark--dot: 0 0 2px 1px var(--ui-panel);

    --overlay-banner: linear-gradient(45deg, rgba(32, 33, 36, 1) 0%, rgba(32, 33, 36, 1) 30%, rgba(0, 0, 0, 0) 100%);
    --scrollbar-thumb: var(--gray-300);

    /* Frosted-glass chrome: one token set so every floating bar (nav, toolbar,
       overlays) reads as the same material. */
    --glass-bg: color-mix(in srgb, var(--bg-body) 62%, transparent);
    --glass-blur: 18px;
    --glass-saturate: 1.6;
    --glass-border: color-mix(in srgb, var(--text-strong) 8%, transparent);
    --glass-highlight: rgba(255, 255, 255, 0.6);
    --glass-shadow: 0 6px 24px -14px rgba(15, 23, 42, 0.35);

    color-scheme: light;
}

:root[data-theme="dark"] {
    --gray-50: #15181f;
    --gray-100: #1c2028;
    --gray-200: #2a2f3a;
    --gray-300: #3a4050;
    --gray-400: #7a8294;
    --gray-500: #9ba3b3;
    --gray-700: #c9cdd6;
    --gray-800: #e1e4ea;
    --gray-900: #f1f3f7;

    --star-on: #fbbf24;
    --star-off: #3a4050;
    --error-bg: #3f1a1a;
    --error-text: #fca5a5;

    --bg-body: #0b0d12;
    --bg-panel: #12151c;
    --bg-elev: #232834;
    --bg-well: #232834;
    /* A visible step up from --bg-elev; anything darker disappears on it. */
    --bg-elev-hover: #2b3140;
    --border-subtle: #262c38;
    --border-muted: #1e232d;

    --text-strong: #f1f3f7;
    --text-body: #c0c6d2;
    --text-muted: #a6adbc;
    --text-faint: #98a1b2;

    --shadow-panel: 0 1px 16px rgba(0, 0, 0, 0.45);
    --shadow-tab: 0 -2px 8px rgba(0, 0, 0, 0.35);
    --shadow-logo: 0 4px 20px rgba(0, 0, 0, 0.55);
    --shadow-media: 0 1px 6px rgba(0, 0, 0, 0.4);
    --shadow-dtp-mark: 0 0 3px 0 var(--ui-panel);
    --shadow-dtp-mark--dot: 0 0 3px 1px var(--ui-panel);

    --overlay-banner: linear-gradient(45deg, rgba(10, 12, 16, 0.95) 0%, rgba(10, 12, 16, 0.85) 30%, rgba(10, 12, 16, 0) 100%);
    --scrollbar-thumb: #3a4050;

    /* On dark: slightly more opaque ground, sheen instead of a white top edge,
       deeper shadow for the same lift. */
    --glass-bg: color-mix(in srgb, var(--bg-body) 58%, transparent);
    --glass-border: color-mix(in srgb, #ffffff 9%, transparent);
    --glass-highlight: rgba(255, 255, 255, 0.1);
    --glass-shadow: 0 8px 30px -16px rgba(0, 0, 0, 0.75);

    color-scheme: dark;
}

/* The dark set is the default: `:root` carries it, and the light theme replaces
   it below, the same way the rest of the palette works. */
:root {
    /* States on the root, not `.surface`: the same dialogs and buttons open from
       the dashboard, where a surface-scoped token resolves to nothing at all. */
    --ui-bad-line: #47242a;
    --ui-bad-line-strong: #6d3239;
    --ui-bad-ground: #1b1214;
    --ui-bad-ink: #e79aa2;

    --ui-warn-line: #3d2f10;
    --ui-warn-line-strong: #5c451a;
    --ui-warn-ground: #1c1608;
    --ui-warn-ink: #ecb96a;

    --ui-good-line: #2c4a3a;
    --ui-good-ink: #7fcaa2;

    --ui-note-line: #2c3a45;
    --ui-note-ground: #101820;
    --ui-note-ink: #8fa9b8;

    --ui-focus-line: #8b93a5;
    /* The border a field wears: one step above the separators, at the weight
       Bootstrap / Ant / Primer ship. The 3:1 line lives under prefers-contrast. */
    --ui-field-line: #39414f;
    --ui-mark: #e7eaec;
    --ui-underline: #454c52;
    /* Where a filled button goes on hover: brighter on a dark ground, darker
       on a light one. A token because the rule has to serve both. */
    --ui-ink-strong: #ffffff;
}

/* ================================================================
 * Layout
 * ================================================================ */

/* Wide enough for screenshots, a rating breakdown and a review list. The embed
   is untouched: it takes whatever width its host gives it (rule below). */
.container {
    max-width: 1150px;
    width: 100%;
    margin: 0 auto;
    padding: 0 clamp(1rem, 4vw, 2rem);
    box-sizing: border-box;
}

/* Outside `.surface` the shared `--ui-*` skin maps to dashboard tokens. On :root,
   not a page wrapper: portalled menus and dialogs on <body> must still resolve them. */
:root {
    --ui-ground: var(--bg-body);
    --ui-ink: var(--text-strong);
    --ui-dim: var(--text-muted);
    --ui-soft: var(--text-body);
    --ui-line: var(--border-subtle);

    --ui-panel: var(--bg-panel);
    --ui-input: var(--bg-well);
    --ui-hover: var(--bg-elev);
    --ui-sunken: var(--bg-elev);
    --ui-rule: var(--border-muted);
    --ui-bone: var(--bg-elev);
    --ui-quiet: var(--text-faint);
    --ui-line-strong: var(--gray-400);
}

.container:not(.embed) {
    max-width: none;
    padding: 0;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

.container:not(.embed) > .panel {
    flex: 1;
    width: 100%;
    max-width: 1150px;
    margin-left: auto;
    margin-right: auto;
    padding-left: clamp(1rem, 4vw, 2.5rem);
    padding-right: clamp(1rem, 4vw, 2.5rem);
    padding-bottom: 3rem;
    box-sizing: border-box;
}

.container.embed {
    margin: 0;
    padding: 0;
    width: 100%;
    max-width: 100%;
    min-height: 100vh;
}

/* Inside an embed this is the surface the results are drawn on. On the dashboard
   it carries nothing: chrome sits above, results keep their own ground. */
.panel {
    background: var(--bg-panel);
    border-radius: 0.75rem;
    box-shadow: var(--shadow-panel);
    padding: 1.5rem;
    min-height: 25rem;
    width: 100%;
    box-sizing: border-box;
    overflow: hidden;
}

.container:not(.embed) .panel {
    background: none;
    box-shadow: none;
    border-radius: 0;
    /* Only the top. The shorthand was wiping the inset the rule above sets, so
       the content ran to both edges of anything narrower than the column. */
    padding-top: 1.25rem;
    min-height: 0;
    overflow: visible;
}

/* The results keep the surface. Same ground as inside an embed, so nothing about
   them reads differently here. */
.container:not(.embed) .panel > .results-surface {
    background: var(--bg-panel);
    border: 1px solid var(--border-subtle);
    border-radius: 10px;
    padding: 1.5rem;
    min-height: 22rem;
}

.embed .panel {
    border-radius: 0;
    min-height: 100vh;
    height: 100%;
}

/* ================================================================
 * Tabs
 * ================================================================ */


/* ================================================================
 * Mode switcher
 * ================================================================ */


/* ================================================================
 * Embed back button
 * ================================================================ */

.embed-header {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    margin-bottom: 0.75rem;
    gap: 0.5rem;
}

.embed-back-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.375rem;
    margin: -0.625rem 0 0 -0.625rem;
    font-size: var(--fs-body);
    font-weight: 600;
    border: none;
    border-radius: 0.5rem;
    cursor: pointer;
    background: none;
    color: #fff;
    transition: opacity 0.2s;
}

.embed-back-btn:hover {
    opacity: 0.85;
}

.embed-back-btn svg {
    width: 1rem;
    height: 1rem;
    display: block;
    flex-shrink: 0;
}

/* ================================================================
 * Form area
 * ================================================================ */

/* No card and no tint: it is part of the content now, not a panel resting on
   one. The border is the only thing separating it from the results below. */
.form-area {
    padding: 1.25rem 0 1.4rem;
}

:root[data-theme="dark"] .container.embed .form-area {
    background: var(--bg-elev) !important;
    --ui-input: var(--bg-panel);
}

/* ================================================================
 * Typography scale — the one type system. Size + weight + tone, so a text
 * node is described by what it is, not a per-place class. Tone uses `--ui-*`
 * (aliased to the dashboard text colours outside `.surface`).
 * ================================================================ */
.ty { margin: 0; font-family: inherit; }

.ty-caption { font-size: var(--fs-caption); line-height: var(--lh-caption); }
.ty-sm      { font-size: var(--fs-sm); line-height: var(--lh-sm); }
.ty-body    { font-size: var(--fs-body); line-height: var(--lh-body); }
.ty-base    { font-size: var(--fs-base); line-height: var(--lh-base); }
.ty-lead    { font-size: var(--fs-lead); line-height: var(--lh-lead); }
.ty-title   { font-size: var(--fs-title); line-height: var(--lh-title); }
.ty-heading { font-size: var(--fs-heading); line-height: var(--lh-heading); }

.fw-medium   { font-weight: 500; }
.fw-semibold { font-weight: 600; }
.fw-bold     { font-weight: 700; }

.tone-strong { color: var(--ui-ink); }
.tone-body   { color: var(--ui-soft); }
.tone-muted  { color: var(--ui-dim); }
.tone-faint  { color: var(--ui-quiet); }

.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mr-auto { margin-right: auto; }

.form-row {
    display: flex;
    gap: 0.625rem;
    flex-wrap: wrap;
    align-items: flex-end;
}

/* ================================================================
 * Layout utilities — the one shared vocabulary for rows and stacks.
 * A feature composes these instead of minting its own flex class;
 * a residual class holds only what no utility can say.
 * ================================================================ */
.row { display: flex; align-items: center; flex-flow: row wrap; }
/* Every screen that fills the window: a column that reaches the bottom, so a
   footer sits at the end of the page rather than under short content. */
.page { min-height: 100vh; display: flex; flex-direction: column; }
/* min-width so a row inside a narrow parent can shrink: without it an
   unbreakable child (a URL, a long token) pushes everything beside it out. */
.row-inline { display: inline-flex; align-items: center; min-width: 0; }

/* The one tick box, on the control itself so an unwrapped checkbox is covered.
   The mark is drawn here too: engines pick its colour from accent luminance. */
input[type="checkbox"] {
    appearance: none;
    -webkit-appearance: none;
    flex: none;
    width: 1rem;
    height: 1rem;
    margin: 0;
    border: 1px solid var(--ui-field-line);
    border-radius: 4px;
    background: var(--ui-input);
    cursor: pointer;
}

/* The glyph's colour is baked into the image, which no token can reach: these
   are the two panel colours the themes already carry. */
input[type="checkbox"]:checked,
input[type="checkbox"]:indeterminate {
    border-color: var(--ui-mark);
    background-color: var(--ui-mark);
    background-position: center;
    background-repeat: no-repeat;
    background-size: 100%;
}

input[type="checkbox"]:checked {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.6 8.4l2.9 2.9 5.9-6.6' fill='none' stroke='%23101315' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

input[type="checkbox"]:indeterminate {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.2 8h7.6' fill='none' stroke='%23101315' stroke-width='2.1' stroke-linecap='round'/%3E%3C/svg%3E");
}

:root[data-theme="light"] input[type="checkbox"]:checked {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.6 8.4l2.9 2.9 5.9-6.6' fill='none' stroke='%23ffffff' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

:root[data-theme="light"] input[type="checkbox"]:indeterminate {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.2 8h7.6' fill='none' stroke='%23ffffff' stroke-width='2.1' stroke-linecap='round'/%3E%3C/svg%3E");
}

input[type="checkbox"]:focus-visible {
    outline: 2px solid var(--ui-focus-line);
    outline-offset: 2px;
}

input[type="checkbox"]:disabled {
    cursor: default;
    opacity: .55;
}


/* The segmented control: one strip wherever a screen offers a small set of choices. */
.seg {
    display: inline-flex;
    align-self: flex-start;
    background: var(--ui-sunken);
    border-radius: 9px;
    padding: 3px;
    gap: 2px;
}

.seg button {
    border: 0;
    background: transparent;
    color: var(--ui-dim);
    font-size: var(--fs-sm);
    font-weight: 600;
    padding: .32rem .8rem;
    border-radius: 7px;
    cursor: pointer;
}

/* The three ways a strip says "this one": a tab, a toggle, and a link to the
   page you are on. */
.seg button[aria-selected="true"],
.seg button[aria-pressed="true"],
.seg button[aria-current="page"] {
    background: var(--ui-panel);
    color: var(--ui-ink-strong);
    box-shadow: 0 1px 3px rgba(0, 0, 0, .12);
}

/* The preview's switches sit in a caption row, so they run smaller. */
.seg--sm button {
    padding: .2rem .55rem;
    font-size: var(--fs-caption);
}

@media (max-width: 560px) {
    /* Tighter, so a phone holds the whole control rather than scrolling it. */
    .seg--sm button {
        padding: .2rem .4rem;
    }
}

@media (max-width: 400px) {
    .seg--sm {
        padding: 2px;
        gap: 1px;
    }

    .seg--sm button {
        padding: .2rem .32rem;
    }
}

/* A row that runs past its edge and scrolls in its own box, for groups that
   must stay one line (a segmented control) on narrow widths. */
.row-scroll { display: flex; align-items: center; flex-wrap: nowrap; min-width: 0; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; }
.row-scroll::-webkit-scrollbar { display: none; }
.row-scroll > * { flex: none; }

/* Present to assistive tech only; a canvas alone reads as nothing. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}
/* Where a strip runs past its edge the content fades out there, so a tab cut in
   half never reads as the last one. `data-edge` comes from useScrollEdges. */
.row-scroll[data-edge="end"] {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent);
}
.row-scroll[data-edge="start"] {
    -webkit-mask-image: linear-gradient(to right, transparent, #000 1.5rem);
    mask-image: linear-gradient(to right, transparent, #000 1.5rem);
}
.row-scroll[data-edge="both"] {
    -webkit-mask-image: linear-gradient(to right, transparent, #000 1.5rem, #000 calc(100% - 1.5rem), transparent);
    mask-image: linear-gradient(to right, transparent, #000 1.5rem, #000 calc(100% - 1.5rem), transparent);
}
.row--start { align-items: flex-start; }
.row--end-y { align-items: flex-end; }
.row--baseline { align-items: baseline; }

.stack { display: flex; flex-direction: column; }

.wrap { flex-wrap: wrap; }
.end { justify-content: flex-end; }
.center-x { justify-content: center; }
.between { justify-content: space-between; }

/* Gap scale: the five values the app actually uses most. */
.gap-xs { gap: 0.25rem; }
.gap-sm { gap: 0.4rem; }
.gap-md { gap: 0.5rem; }
.gap-lg { gap: 0.7rem; }
.gap-xl { gap: 1rem; }

/* A flex child that takes the remaining width and may shrink. */
.grow { flex: 1; min-width: 0; }

/* The one text input. `--ui-*` alias dashboard tokens outside `.surface` and swap
   inside it; the classes below add only position or state, never a second look. */
.input {
    display: block;
    width: 100%;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--ui-field-line);
    border-radius: 0.5rem;
    background: var(--ui-input);
    color: var(--ui-ink);
    font-family: inherit;
    font-size: var(--fs-body);
    outline: none;
    box-sizing: border-box;
    transition: border-color 0.15s;
}

.input:focus {
    border-color: var(--ui-focus-line);
    /* The border is the indicator (>=3:1 alone); the halo is decoration. */
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--ui-focus-line) 25%, transparent), 0 0 8px 3px color-mix(in srgb, var(--ui-focus-line) 14%, transparent);
    background: var(--ui-panel);
}

/* The textarea twin (TextArea): same skin, multi-line sizing. */
textarea.input {
    line-height: 1.5;
    resize: vertical;
    min-height: 4.5rem;
}

.input:disabled,
.sel-trigger:disabled,
.color-picker-trigger:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

/* A disabled group fades once; its children must not fade again inside it. */
fieldset:disabled :is(.input, .sel-trigger, .color-picker-trigger, .option-toggle input, .option-toggle-text) {
    opacity: 1;
}

.input::placeholder {
    color: var(--ui-dim);
}

.input[aria-invalid="true"],
.sel-trigger[aria-invalid="true"] {
    border-color: var(--error-text);
}

.input[aria-invalid="true"]:focus,
.sel-trigger[aria-invalid="true"]:focus,
.sel-trigger[aria-invalid="true"][aria-expanded="true"] {
    border-color: var(--error-text);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--error-text) 25%, transparent), 0 0 8px 3px color-mix(in srgb, var(--error-text) 14%, transparent);
}

/* The message sits under its own field, not in a callout at the form's end,
   with air between it and the control it judges. */
.field-error {
    /* An inline span swallows vertical margins in block flow; block makes the
       gap real wherever the message lands. */
    display: block;
    margin-top: 0.3rem;
    font-size: var(--fs-sm);
    color: var(--error-text);
}

/* The API tester's URL and param fields hold code, so they go monospace and a
   touch tighter. */
/* A contenteditable wearing the .input skin: wraps and grows with its content
   (no inner scroll), a placeholder while empty. Finished tokens are chips. */
.token-field {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    min-height: 2.25rem;
    line-height: var(--lh-base);
}

.token-field:empty::before {
    content: attr(data-placeholder);
    color: var(--ui-dim);
}

/* A resolved token. Its paint lives on the blob layer beneath, where a wrapped
   chip's line fragments fuse into one droplet; the inline box only spaces text. */
.token-chip {
    padding: 0.08em 0.35em;
    margin: 0 2px;
    color: var(--ui-ink);
    font-weight: 600;
}

.token-field {
    position: relative;
    /* Its own stacking context, so the blob layer's negative z-index sits
       above this element's background but below its text and caret. */
    isolation: isolate;
}

.token-blob-layer {
    position: absolute;
    inset: 0;
    z-index: -1;
    overflow: visible;
    pointer-events: none;
    user-select: none;
}

/* A filled-in value lifts to the panel, the same step focus takes, so it stays
   distinct from the well on both themes. */
.token-blob {
    fill: var(--ui-panel);
    stroke: var(--ui-line-strong);
    stroke-width: 1;
}

/* An empty resolved value still needs something to grab. */
.token-chip:empty::before {
    content: "";
    display: inline-block;
    width: 0.6em;
}

/* Monospace alone; the field variants below add their sizing. */
.mono {
    font-family: var(--font-mono);
}

.input.mono,
.sel-trigger.mono {
    padding: 0.4rem 0.55rem;
    font-family: var(--font-mono);
    user-select: text;
}

/* Validation error border, held through focus. */
.input--bad,
.input--bad:focus {
    border-color: var(--ui-bad-line-strong);
}

.form-field {
    flex: 1;
    min-width: 8.75rem;
    font-size: var(--fs-sm);
    color: var(--ui-soft);
    font-weight: 500;
}

.form-field--small {
    flex: 0 0 3.75rem;
    min-width: 3.75rem;
}

.form-field label {
    display: block;
    margin-bottom: 4px;
    text-align: left;
}

/* The input wears the shared `.input` skin; here it only needs to stand the
   same height as the select trigger beside it. */
.form-field .input {
    height: 2.25rem;
}

/* The token field grows with its content; min-height keeps the single-line
   case level with the fixed-height siblings. */
.form-field .input.token-field {
    height: auto;
}

.form-field select {
    display: block;
    width: 100%;
    height: 2.25rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--ui-field-line);
    border-radius: 0.5rem;
    font-size: var(--fs-body);
    outline: none;
    transition: border-color 0.2s, box-shadow 0.2s;
    background: var(--ui-input);
    color: var(--ui-ink);
    font-family: inherit;
}

.form-field select:focus {
    border-color: var(--ui-focus-line);
}

/* The accent is the embed's, shown inside the frame; the fetch button is part
   of the tool, so it keeps the tool's own colours. */
.fetch-button {
    background: var(--text-strong);
    color: var(--bg-panel);
    width: 7.5rem;
    padding: 0.5rem 1.5rem;
    font-size: var(--fs-body);
    font-weight: 600;
    border: none;
    border-radius: 0.5rem;
    cursor: pointer;
    height: 2.375rem;
    white-space: nowrap;
    transition: opacity 0.2s;
}

.fetch-button:disabled {
    opacity: 0.6;
    cursor: wait;
}

.action-button {
    border: 1px solid var(--accent);
    background: none;
    padding: .375rem 1.125rem;
    font-size: var(--fs-sm);
    font-weight: 600;
    border-radius: 9999px;
    cursor: pointer;
    color: var(--text-strong);
    white-space: nowrap;
    transition: opacity 0.2s;
}

/* A trigger that is a word with a rule under it rather than a box, for the
   places where a bordered pill reads as a second control standing beside the
   thing it belongs to. The rule is the whole skin: nothing is spent on a
   colour until something is picked. */
.underline-button {
    padding: .1rem 0;
    border: 0;
    /* The rule has to be seen at rest, or there is nothing there to press: on
       the header's raised ground `--ui-line` is three units off the background
       in dark and vanishes. Being quiet is the type's job, not the rule's. */
    border-bottom: 1px solid var(--ui-line-strong);
    border-radius: 0;
    background: none;
    font: inherit;
    /* No larger than the label it hangs off, and lighter: it is what the
       column is filtered by, not what the column is. */
    font-size: var(--fs-caption);
    line-height: var(--lh-caption);
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    color: var(--ui-quiet);
    white-space: nowrap;
    cursor: pointer;
}

.underline-button:hover {
    border-bottom-color: var(--ui-ink);
    color: var(--ui-ink);
}

/* Something is picked: the rule thickens and takes the mark's colour, which is
   the same signal the checkboxes in its own panel carry. */
.underline-button.on {
    border-bottom-width: 2px;
    border-bottom-color: var(--ui-mark);
    color: var(--ui-ink-strong);
}@media (max-width: 750px) {

    .form-field {
        flex: 1 1 100%;
        min-width: 0;
    }

    .form-field--small {
        flex: 1 1 calc(20% - 5px);
        min-width: 0;
    }

    .fetch-button {
        width: 100%;
    }
}

/* ================================================================
 * States
 * ================================================================ */

/* One callout, three colours: box shape is shared, the variant only swaps the
   token set; the alert/status role is set on the element by the component. */
.callout {
    display: flex;
    align-items: center;
    gap: .5rem 1rem;
    /* Symmetric so the box never floats toward one neighbour; margins collapse
       in block flow, the container's gap governs in flex/grid. */
    margin: 1.1rem 0;
    padding: .8rem 1rem;
    border: 1px solid var(--callout-line);
    border-left: 2px solid var(--callout-strong);
    border-radius: .5rem;
    background: var(--callout-ground);
    color: var(--callout-ink);
    font-size: var(--fs-body);
    line-height: var(--lh-body);
    text-align: left;
}

.callout--error { --callout-ground: var(--ui-bad-ground);  --callout-line: var(--ui-bad-line);  --callout-strong: var(--ui-bad-line-strong);  --callout-ink: var(--ui-bad-ink); }
.callout--warn  { --callout-ground: var(--ui-warn-ground); --callout-line: var(--ui-warn-line); --callout-strong: var(--ui-warn-line-strong); --callout-ink: var(--ui-warn-ink); }
.callout--info  { --callout-ground: var(--ui-hover);       --callout-line: var(--ui-line);      --callout-strong: var(--ui-line-strong);      --callout-ink: var(--ui-soft); }

/* The message takes the room; an action or the close x sits beside it. */
.callout-msg { flex: 1; min-width: 0; white-space: pre-line; }

/* The way out of a failure: underlined, in the message's own colour, so it
   belongs to the box rather than competing with it. */
.callout-action {
    flex: none;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    font-size: var(--fs-sm);
    line-height: var(--lh-sm);
    font-weight: 600;
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
    opacity: .85;
}

.callout-action:hover { opacity: 1; }

/* Opt-in dismissal (Callout onClose). */
.callout-close {
    flex: none;
    width: 1.35rem;
    height: 1.35rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin: -.15rem -.2rem -.15rem 0;
    padding: 0;
    border: 0;
    border-radius: 5px;
    background: none;
    color: inherit;
    opacity: .6;
    cursor: pointer;
}

.callout-close:hover { opacity: 1; background: color-mix(in srgb, var(--callout-ink) 12%, transparent); }
.callout-close svg { width: .8rem; height: .8rem; }

/* Swipe rail: the box slides under the finger; a release past halfway throws it
   out and the rail collapses so anything below rises to fill the gap. */
/* The rail carries the outer spacing; a margin on the box would be trapped
   inside the clip, so the box resets it. */
.callout-track { overflow: hidden; margin: 1.1rem 0; }
.callout--dismissible { touch-action: pan-y; margin: 0; }
.callout--dismissible:not(.callout--held) { transition: transform .22s ease, opacity .22s ease; }

.callout-track--closing {
    transition: max-height .25s ease, margin .25s ease, opacity .2s ease;
    max-height: 0 !important;
    margin: 0 !important;
    opacity: 0;
}

/* AlertChannelPrefs: which alerts reach which channel. Wide screens get a grid
   with a column per channel; the mobile layout lives further down, folded. */
.chan-row {
    display: grid;
    grid-template-columns: 1fr repeat(var(--chan-count, 1), 3.4rem);
    align-items: center;
    gap: 0 .25rem;
    padding: .55rem 0;
    border-bottom: 1px solid var(--ui-rule);
}

.chan-row:last-child { border-bottom: 0; }

.chan-row--head {
    padding: 0 0 .5rem;
    border-bottom: 1px solid var(--ui-line);
}

.chan-colhead {
    text-align: center;
    font-size: var(--fs-caption);
    line-height: var(--lh-caption);
    font-weight: 600;
    letter-spacing: var(--ls-caps);
    text-transform: uppercase;
    color: var(--ui-quiet);
}

.chan-row--child .chan-label { padding-left: 1.65rem; }
.chan-row--master { background: color-mix(in srgb, var(--ui-hover) 55%, transparent); border-radius: 6px; }

.chan-label b {
    display: block;
    font-size: var(--fs-body);
    line-height: var(--lh-body);
    font-weight: 600;
}

.chan-label .quiet { display: block; margin-top: .1rem; }
.chan-cell { display: flex; justify-content: center; }

/* Mobile: each kind folds into its own disclosure. The list itself is a
   `.stack gap-md`; only the disclosure chrome lives here. */
.chan-item {
    border: 1px solid var(--ui-line);
    border-radius: 8px;
    overflow: hidden;
}

.chan-sum {
    display: flex;
    align-items: center;
    gap: .6rem;
    width: 100%;
    padding: .7rem .8rem;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.chan-sum .chan-label { flex: 1; min-width: 0; }

.chan-state {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    font-size: var(--fs-sm);
    line-height: var(--lh-sm);
    color: var(--ui-quiet);
    white-space: nowrap;
}

/* Folded-summary channel glyphs: which channels this kind is on, at a glance. */
.chan-state-icon {
    display: inline-flex;
    width: .95rem;
    height: .95rem;
    color: var(--ui-soft);
}

.chan-state-icon svg { width: 100%; height: 100%; }

.chan-item-body {
    display: flex;
    flex-direction: column;
    padding: .1rem .8rem .5rem;
    border-top: 1px solid var(--ui-rule);
}

.chan-switch {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: .55rem 0;
    font-size: var(--fs-body);
    line-height: var(--lh-body);
}

.chan-switch + .chan-switch { border-top: 1px solid var(--ui-rule); }

.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 3.75rem 1.25rem;
    color: var(--text-faint);
    font-size: var(--fs-base);
    text-align: center;
}

.no-reviews {
    text-align: center;
    padding: 2.5rem;
    color: var(--text-faint);
    font-size: var(--fs-base);
    border-bottom: 1px solid var(--border-subtle);
}

.no-reviews:nth-last-child(1) {
    border-bottom: none;
}

.spinner-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 2.5rem;
    gap: 0.75rem;
}

/* Waiting inside a stack of sections: a rule under it holds the section's place
   until content arrives; the last section has nothing below it. */
.spinner-container--ruled {
    border-bottom: 1px solid var(--border-subtle);
    margin-bottom: 1.25rem;
}

.spinner-container--ruled.last {
    border-bottom: none;
    margin-bottom: 0;
}

.spinner {
    width: 2rem;
    height: 2rem;
    border: 3px solid var(--border-subtle);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

/* ================================================================
 * App Header
 * ================================================================ */

.app-header-wrap {
    position: relative;
    border-radius: 1rem;
    overflow: visible;
    margin-bottom: 1.25rem;
}

.app-header-banner {
    position: absolute;
    inset: 0;
    z-index: 0;
    /* The children compose their own layers, which the parent's overflow does
       not always clip cleanly at the rounded corners. */
    border-radius: inherit;
    overflow: hidden;
}

.app-header-banner img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: inherit;
}

.app-header-banner--blur {
    position: absolute;
    inset: 0;
    backdrop-filter: blur(100px) saturate(1.5);
    -webkit-backdrop-filter: blur(100px) saturate(1.5);
    border-radius: inherit;
}

.app-header-banner-overlay {
    position: absolute;
    inset: 0;
    background: var(--overlay-banner);
    border-radius: inherit;
}

.app-header {
    position: relative;
    z-index: 1;
    display: flex;
    /* Against the logo, not its top: any of the parts below can be off, and one
       line of text must sit level with a 100px logo. */
    align-items: center;
    gap: 1.25rem;
    padding: 0;
    text-align: left;
}

/* Room for the picture: the banner is absolutely placed and takes its height
   from this row, so a banner-only header must not collapse to the padding. */
.app-header--has-banner {
    padding: 1.75rem 1.5rem;
    min-height: 9rem;
}

.app-header--has-banner .app-title {
    color: #fff;
    text-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}

.app-header--has-banner .app-developer {
    text-decoration: underline;
    color: rgba(255, 255, 255, 0.9);
}

.app-header--has-banner .app-rating .rating-score {
    color: #fff;
}

.app-header--has-banner .rating-count {
    color: rgba(255, 255, 255, 0.8);
}

.app-header--has-banner .version-badge {
    background: rgba(255, 255, 255, 0.25);
    color: #fff;
}

.app-header--has-banner .price-tag {
    color: #fff;
}

.app-logo {
    width: 6.25rem;
    height: 6.25rem;
    object-fit: cover;
    box-shadow: var(--shadow-logo);
    flex-shrink: 0;
}

.app-logo--play {
    border-radius: 1.25rem;
}

.app-logo--apple {
    border-radius: 1.375rem;
}

.app-logo--ms {
    border-radius: 0.5rem;
}

.app-info {
    flex: 1;
    min-width: 0;
    text-align: left;
    /* A column that centres what it holds, so the name is level with the logo
       whatever follows it. No gap: the children carry their own spacing. */
    display: flex;
    flex-direction: column;
    justify-content: center;
}

/* Nothing selected inside it, so it should take no room and leave the row to
   the logo or the banner. */
.app-info:empty {
    display: none;
}

.app-title {
    font-size: var(--fs-heading);
    font-weight: 700;
    color: var(--text-strong);
    line-height: 1.15;
}

.app-developer {
    text-decoration: none;
    font-size: var(--fs-body);
    font-weight: 500;
    display: inline-block;
    margin-top: 2px;
}

.app-meta {
    display: flex;
    align-items: center;
    gap: 0.875rem;
    margin-top: 0.75rem;
    flex-wrap: wrap;
}

.app-rating { gap: 2px; }

.rating-score {
    font-size: var(--fs-title);
    font-weight: 700;
    color: var(--text-strong);
    padding-right: 4px;
}

.rating-count {
    font-size: var(--fs-sm);
    color: var(--text-muted);
    padding-left: 3px;
}

.version-badge {
    font-size: var(--fs-sm);
    color: var(--text-muted);
    background: var(--gray-100);
    padding: 2px 0.625rem;
    border-radius: 0.75rem;
}

/* Top-chart placement, e.g. "🏆 #4 Entertainment": accent-tinted badge, label
   ellipsized so a long localized chart name never pushes the meta row. */
.rank-badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    max-width: 16rem;
    font-size: var(--fs-sm);
    color: var(--accent);
    background: var(--accent-light);
    padding: 2px 0.625rem;
    border-radius: 0.75rem;
}

.rank-badge b {
    font-weight: 700;
}

.rank-badge .rank-label {
    min-width: 0;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.app-header--has-banner .rank-badge {
    background: rgba(255, 255, 255, 0.25);
    color: #fff;
}

.app-header--has-banner .rank-badge .rank-label {
    color: rgba(255, 255, 255, 0.85);
}

/* Against the panel by default, white only where a banner is behind it. */
.price-tag {
    font-size: var(--fs-body);
    font-weight: 600;
    color: var(--text-strong);
}

.app-header--has-banner .price-tag {
    color: #fff;
}

/* Same reasoning: with no banner these take the page's own ink. */
.app-developer {
    color: var(--text-body);
}

.app-header--has-banner .rating-count {
    color: rgba(255, 255, 255, .75);
}

.price-original {
    font-size: var(--fs-sm);
    color: var(--text-faint);
    text-decoration: line-through;
    margin-right: 4px;
}

.stars {
    position: relative;
    display: inline-block;
    line-height: 1;
    letter-spacing: 1px;
    white-space: nowrap;
    vertical-align: middle;
}

.stars__background {
    color: var(--star-off);
    display: inline-block;
}

.stars__foreground {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    overflow: hidden;
    color: var(--star-on);
    pointer-events: none;
}

.stars__foreground-inner {
    display: inline-block;
    white-space: nowrap;
}

.app-short-description {
    font-size: var(--fs-body);
    color: var(--text-muted);
    margin-top: 0.375rem;
    line-height: 1.4;
}

.app-header--has-banner .app-short-description {
    color: rgba(255, 255, 255, 0.85);
}

/* ================================================================
 * Media section
 * ================================================================ */

.media-section {
    padding: 1.25rem 0;
}


.media-scroll {
    display: flex;
    gap: 0.625rem;
    overflow-x: auto;
    padding-bottom: 0.5rem;
    scroll-snap-type: x mandatory;
}

.media-scroll::-webkit-scrollbar {
    height: 6px;
}

.media-scroll::-webkit-scrollbar-thumb {
    background: var(--scrollbar-thumb);
    border-radius: 3px;
}

/* A shot opens the detail viewer. */
.media-shot {
    cursor: zoom-in;
}

/* ---- the preview detail viewer ---------------------------------------- */
/* Always the dark room, whatever the page theme: media reads best on black. */
.media-viewer {
    position: fixed;
    inset: 0;
    z-index: 80;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgb(8 9 11 / .8);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
}

/* Fullscreen paints a black ::backdrop by default, which would turn the
   translucent overlay opaque; clearing it keeps the page visible. */
.media-viewer::backdrop {
    background: transparent;
}

/* One slide per item on a sliding track: the neighbour peeks in from the
   edge mid-pan, and a button turn is the same slide. */
.media-viewer-track {
    position: absolute;
    inset: 0;
    touch-action: none;
    will-change: transform;
}

.media-viewer-track.snap {
    transition: transform .18s ease;
}

.media-viewer-slide {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: grab;
}

.media-viewer-slide:active {
    cursor: grabbing;
}

.media-viewer-slide img {
    max-width: min(92vw, 60rem);
    max-height: 88vh;
    border-radius: 14px;
    box-shadow: 0 24px 80px rgb(0 0 0 / .5);
    user-select: none;
    -webkit-user-drag: none;
}

.media-viewer-slide video,
.media-viewer-slide iframe {
    width: min(92vw, 60rem);
    aspect-ratio: 16 / 9;
    border: 0;
    border-radius: 14px;
    background: #000;
    box-shadow: 0 24px 80px rgb(0 0 0 / .5);
}

/* A neighbouring video with no still to show. */
.media-viewer-blank {
    width: min(92vw, 60rem);
    aspect-ratio: 16 / 9;
    border-radius: 14px;
    background: #000;
    display: flex;
    align-items: center;
    justify-content: center;
    color: rgb(255 255 255 / .3);
}

.media-viewer-close,
.media-viewer-arrow {
    position: absolute;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 50%;
    background: rgb(255 255 255 / .12);
    color: #fff;
    cursor: pointer;
}

.media-viewer-close:hover,
.media-viewer-arrow:hover {
    background: rgb(255 255 255 / .22);
}

.media-viewer-close {
    top: max(.9rem, env(safe-area-inset-top));
    right: max(.9rem, env(safe-area-inset-right));
    width: 2.4rem;
    height: 2.4rem;
}

.media-viewer-close svg {
    width: 0.875rem;
    height: 0.875rem;
}

.media-viewer-arrow {
    top: 50%;
    transform: translateY(-50%);
    width: 2.6rem;
    height: 2.6rem;
}

.media-viewer-arrow--prev {
    left: max(.9rem, env(safe-area-inset-left));
}

.media-viewer-arrow--next {
    right: max(.9rem, env(safe-area-inset-right));
}

.media-viewer-count {
    position: absolute;
    top: max(1.1rem, env(safe-area-inset-top));
    left: 50%;
    transform: translateX(-50%);
    color: rgb(255 255 255 / .6);
    font-size: var(--fs-sm);
    letter-spacing: var(--ls-caps);
    font-variant-numeric: tabular-nums;
}

.media-scroll img {
    height: 13.75rem;
    border-radius: 0.75rem;
    scroll-snap-align: start;
    flex-shrink: 0;
    box-shadow: var(--shadow-media);
}

.video-card {
    position: relative;
    flex-shrink: 0;
    scroll-snap-align: start;
    border-radius: 0.75rem;
    overflow: hidden;
    background: #000;
    box-shadow: var(--shadow-media);
    cursor: pointer;
    height: 13.75rem;
}

.video-card iframe,
.video-card video {
    width: 100%;
    height: 100%;
    border: none;
    display: block;
}

/* Opens the detail viewer; sits clear of the player's own controls. */
.video-expand {
    position: absolute;
    top: .45rem;
    right: .45rem;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.8rem;
    height: 1.8rem;
    border: 0;
    border-radius: 50%;
    background: rgb(0 0 0 / .55);
    color: #fff;
    cursor: pointer;
}

.video-expand:hover {
    background: rgb(0 0 0 / .75);
}

/* Stands in for a poster when autoplay is off and the video has none. */
.video-poster-blank {
    height: 100%;
    background: #000;
}

.video-poster-image {
    height: 100%;
    width: 100%;
    display: block;
    object-fit: cover;
}

.video-play-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.15);
    transition: background 0.2s;
}

.video-card:hover .video-play-overlay {
    background: rgba(0, 0, 0, 0.3);
}

.video-play-circle {
    width: 3.25rem;
    height: 3.25rem;
    background: rgba(0, 0, 0, 0.6);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
}

.video-play-circle::after {
    content: "";
    width: 0;
    height: 0;
    border-style: solid;
    border-width: 10px 0 10px 18px;
    border-color: transparent transparent transparent #fff;
    margin-left: 3px;
}

/* ================================================================
 * Info sections
 * ================================================================ */

.info-section {
    padding: 1.25rem 0;
    border-bottom: 1px solid var(--border-subtle);
    text-align: left;
}

/* The builds search: as wide as it needs, not the whole row, so it sits at the
   heading's end rather than pushing the title away. */
.build-search {
    width: auto;
    max-width: 14rem;
}

/* A build's problems in its expanded row: each a title with its sentence
   under it, so a non-version problem reads as its own thing. */
.build-problems {
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: 0.6rem;
}

.build-problems li {
    display: grid;
    gap: 0.15rem;
}

.build-problems span {
    color: var(--text-muted);
    font-size: var(--fs-sm);
}

.info-section:nth-last-child(1) {
    border-bottom: none;
}


.info-description {
    font-size: var(--fs-body);
    color: var(--text-body);
    line-height: 1.5;
    overflow-wrap: anywhere;
    margin: 0;
}

.expand-button {
    background: none;
    border: none;
    color: var(--accent);
    cursor: pointer;
    font-size: var(--fs-sm);
    padding: 4px 0;
    font-weight: 500;
}

.info-dates {
    display: flex;
    gap: 1.5rem;
    margin-top: 1rem;
    flex-wrap: wrap;
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

.release-notes {
    margin-top: 1rem;
}

.release-notes-title {
    font-size: var(--fs-body);
    font-weight: 600;
    color: var(--text-body);
    margin-bottom: 4px;
}

.release-notes-body {
    font-size: var(--fs-sm);
    color: var(--text-body);
    line-height: 1.4;
    overflow-wrap: anywhere;
    margin: 0;
}

/* ================================================================
 * Reviews
 * ================================================================ */

.reviews-header {
    margin-bottom: 1rem;
    padding-top: 1.25rem;
}


.sort-group {
    display: flex;
    gap: 4px;
    background: var(--gray-100);
    border-radius: 0.5rem;
    padding: 3px;
}

.sort-button {
    padding: 0.375rem 0.875rem;
    font-size: var(--fs-sm);
    font-weight: 500;
    border: none;
    border-radius: 0.375rem;
    cursor: pointer;
    background: transparent;
    color: var(--text-muted);
    transition: all 0.2s;
}

.review-card {
    padding: 1rem 0;
    border-bottom: 1px solid var(--border-muted);
    text-align: left;
}

.review-stars-row {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-bottom: 4px;
}

.review-version-label {
    font-size: var(--fs-caption);
    color: var(--text-faint);
    padding-left: 2px;
}

.review-title {
    font-size: var(--fs-body);
    font-weight: 600;
    color: var(--text-strong);
    margin-bottom: 2px;
}

.review-content {
    font-size: var(--fs-sm);
    color: var(--text-body);
    line-height: 1.4;
    overflow-wrap: anywhere;
    margin: 0;
}

.review-meta {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.375rem;
    font-size: var(--fs-caption);
    color: var(--text-faint);
}

.review-meta .profile-image {
    width: 1.875rem;
    height: 1.875rem;
    border-radius: 50%;
    box-shadow: 0 1px 12px rgba(0, 0, 0, 0.06);
}



.review-helpful svg {
    width: 0.8125rem;
    height: 0.8125rem;
    display: block;
    flex-shrink: 0;
}

.reviews-end {
    text-align: center;
    padding: 1.25rem;
    color: var(--text-faint);
    font-size: var(--fs-sm);
}

.load-more {
    width: 100%;
    height: 7rem;
    display: flex;
    justify-content: center;
    align-items: center;
    border-bottom: 1px solid var(--border-subtle);
    margin-bottom: 1.25rem;
}

.load-more.last {
    border-bottom: none;
    margin-bottom: 0;
}

/* ================================================================
 * Histogram
 * ================================================================ */

.histogram {
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
    margin-top: 0.5rem;
}

.histogram-row {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    font-size: var(--fs-sm);
}

.histogram-label {
    width: 0.75rem;
    color: var(--text-muted);
    font-weight: 600;
    text-align: right;
}

.histogram-track {
    flex: 1;
    height: 0.5rem;
    background: var(--gray-100);
    border-radius: 4px;
    overflow: hidden;
}

.histogram-fill {
    height: 100%;
    background: var(--star-on);
    border-radius: 4px;
    transition: width 0.4s ease;
}

.histogram-count {
    width: 3.75rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    text-align: left;
}

/* ================================================================
 * Chips
 * ================================================================ */

/* A monospace run of lines (a stack trace, breadcrumbs, a log): wraps rather
   than scrolling sideways, since a frame is read from its left. */
/* A numbered run of monospace lines: a trace, breadcrumbs, a log. The numbers
   are how a reader refers to a line, so they are a gutter an editor's width,
   not a list's "1." — the rule beside them says where the line itself starts,
   and a wrapped frame stays inside that start. Counted rather than marked:
   ::marker cannot be given a rule, and the count has to survive a line that
   wraps to two rows. */
.code-lines {
    display: block;
    margin: 0;
    padding-left: 0;
    list-style: none;
    counter-reset: code-line;
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    line-height: var(--lh-sm);
    overflow-wrap: anywhere;
}

/* A block, not a grid or a flex row: a line is one run of text that happens
   to hold several elements (a breadcrumb is its timestamp and its message),
   and any track-based box would break each of those onto a column of its own.
   The gutter is therefore taken out of the flow, and the indent is what keeps
   a wrapped line clear of it. No gap between the rows: the rules are per line,
   and a gap would cut the gutter into dashes. */
.code-lines > li {
    counter-increment: code-line;
    position: relative;
    padding-left: 3.5rem;
    user-select: text;
    -webkit-user-select: text;
}

/* Pinned top to bottom, so the rule runs the height of a line that wrapped to
   three rows while the number stays on the first. Not selectable: a trace is
   copied to be pasted somewhere that wants the frames, not the counting. */
.code-lines > li::before {
    content: counter(code-line);
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 2.75rem;
    padding-right: 0.55rem;
    border-right: 1px solid var(--border-subtle);
    text-align: right;
    color: var(--text-faint);
    user-select: none;
    -webkit-user-select: none;
}

/* Content with no natural length gets a viewport of its own, so whatever
   follows it stays reachable. One box per section, never nested: a scrollport
   inside a scrollport is a trap for the wheel. Scrolling chains by default and
   must keep doing so — at the last frame the wheel carries on down the page. */
.scroll-box {
    max-height: 26rem;
    overflow-y: auto;
}

.chip-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin-top: 0.375rem;
}

.chip-row a {
    text-decoration: none;
}

.chip-row a:hover .chip {
    background: var(--gray-300);
}

.chip {
    font-size: var(--fs-caption);
    font-weight: 500;
    color: var(--text-body);
    background: var(--gray-200);
    padding: 4px 0.625rem;
    border-radius: 0.75rem;
}

/* The chip as a button: a tappable suggestion. Carries the hairline and ground
   every real control wears, so it reads as pressable without an accent. */
button.chip {
    border: 1px solid var(--ui-line);
    background: var(--bg-panel);
    color: var(--text-strong);
    font-family: inherit;
    text-align: left;
    cursor: pointer;
    transition: background .15s, border-color .15s;
}

button.chip:hover {
    background: var(--bg-elev-hover);
    border-color: var(--ui-line-strong);
}

/* ================================================================
 * Detail grid
 * ================================================================ */

.detail-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(11.25rem, 1fr));
    gap: 0.625rem;
}

.detail-item {
    display: flex;
    align-items: flex-start;
    gap: 0.625rem;
    padding: 0.625rem 0.75rem;
    background: var(--bg-elev);
    border-radius: 0.625rem;
    transition: background 0.15s;
    text-align: left;
}

.detail-item:hover {
    background: var(--bg-elev-hover);
}

.detail-item-icon {
    width: 1rem;
    height: 1rem;
    flex-shrink: 0;
    color: var(--text-faint);
    margin-top: 1px;
}

.detail-item-icon svg {
    width: 100%;
    height: 100%;
    display: block;
}


.detail-label {
    font-size: var(--fs-caption);
    color: var(--text-faint);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: var(--ls-caps);
    margin-bottom: 3px;
    /* A key can be longer than its tile (memoryLimitRemainingBytes); it wraps
       rather than running out over the tile beside it. */
    overflow-wrap: anywhere;
}

.detail-value {
    font-size: var(--fs-caption);
    color: var(--text-strong);
    font-weight: 500;
    word-break: break-word;
    line-height: 1.35;
}

/* Where a value can be a dump the app did not choose the shape of (a crash's
   register set), it scrolls in place rather than stretching its row of tiles
   to the height of the longest one. */
.detail-grid--capped .detail-value {
    max-height: 7rem;
    overflow-y: auto;
}



.detail-chip {
    font-size: var(--fs-caption);
    font-weight: 500;
    color: var(--text-body);
    background: var(--gray-200);
    padding: 2px 0.5rem;
    border-radius: 4px;
    line-height: 1.5;
    white-space: nowrap;
}

/* The figure inside a chip whose text is a label: without a weight and a
   ground of its own, "3.5.18 (2599) 1" reads as one string. */
.chip-count {
    margin-left: 0.4rem;
    padding: 0 0.3rem;
    border-radius: 3px;
    background: var(--gray-100);
    color: var(--text-strong);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.detail-chip--more {
    cursor: pointer;
    background: var(--bg-elev);
    border: 1px dashed var(--border-subtle);
    color: var(--text-muted);
    user-select: none;
    transition: background 0.15s ease, color 0.15s ease;
}

.detail-chip--more:hover {
    background: var(--bg-elev-hover);
    color: var(--text-body);
}

.detail-chip--more:active {
    background: var(--accent-light);
    color: var(--accent);
    border-color: var(--accent);
}

.chips-popover {
    z-index: 9999;
    max-width: 17.5rem;
    background: var(--bg-panel);
    border: 1px solid var(--border-subtle);
    border-radius: 0.625rem;
    box-shadow: var(--shadow-panel);
    padding: 0.625rem;
}

.chips-popover__inner {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    max-height: 12.5rem;
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--scrollbar-thumb) transparent;
}

.chips-popover__inner.single {
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--text-body);
    padding: 2px 0.5rem;
    border-radius: 4px;
    line-height: 1.5;
    white-space: nowrap;
}

.chips-popover__inner::-webkit-scrollbar {
    width: 4px;
}

.chips-popover__inner::-webkit-scrollbar-track {
    background: transparent;
}

.chips-popover__inner::-webkit-scrollbar-thumb {
    background: var(--scrollbar-thumb);
    border-radius: 99px;
}

/* ================================================================
 * Similar section
 * ================================================================ */

.similar-section-title {
    font-size: var(--fs-title);
    font-weight: 600;
    color: var(--text-strong);
    margin-bottom: 0.75rem;
    cursor: pointer;
}

.similar-scroll {
    display: grid;
    gap: 1.875rem;
    overflow-x: auto;
    padding-bottom: 1.25rem;
    scroll-snap-type: x mandatory;
    grid-template-rows: repeat(2, auto);
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
}

.similar-scroll::-webkit-scrollbar {
    height: 6px;
}

.similar-scroll::-webkit-scrollbar-thumb {
    background: var(--scrollbar-thumb);
    border-radius: 3px;
}




/* ================================================================
 * Developer links
 * ================================================================ */

.dev-links {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.75rem;
}

.dev-link {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: var(--fs-caption);
    font-weight: 500;
    color: var(--text-muted);
    text-decoration: none;
    padding: 5px 0.625rem;
    border: 1px solid var(--border-subtle);
    border-radius: 0.5rem;
    transition: all 0.15s;
}

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

/* ================================================================
 * Search results
 * ================================================================ */



.result-title {
    font-size: var(--fs-title);
    font-weight: 500;
    padding: 0 0 0.6875rem 0;
}

/* One result card. A row of them fills a list; a tile of them scrolls across a
   shelf. Everything but the box is the same, so only the box is stated twice. */
.result-card {
    display: flex;
    align-items: center;
    gap: 0.875rem;
    border-radius: 0.75rem;
    cursor: pointer;
    transition: background 0.15s;
    border: 1px solid transparent;
    text-align: left;
}

.result-card--row {
    padding: 0.875rem 1rem;
    background: var(--bg-elev);
}

.result-card--tile {
    width: 12.5rem;
    scroll-snap-align: start;
}

.result-card:hover {
    background: var(--bg-elev-hover);
    border-color: var(--border-subtle);
}

.result-logo {
    width: 3.5rem;
    height: 3.5rem;
    object-fit: cover;
    flex-shrink: 0;
}

.result-logo--play {
    border-radius: 0.75rem;
}

.result-logo--apple {
    border-radius: 0.875rem;
}

.result-logo--ms {
    border-radius: 0.375rem;
}


.result-name {
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--text-strong);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.result-meta {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-top: 3px;
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

.result-price {
    font-size: var(--fs-sm);
    font-weight: 600;
    flex-shrink: 0;
}




@keyframes fadeUp {
    from {
        opacity: 0;
        transform: translateY(6px) scale(0.88);
    }
    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}


/* ================================================================
 * Modal
 * ================================================================ */

/* The theme colour belongs to the embed being built, set on the root for the
   preview and results. The dialog's own chrome must not take it. */
.dash-nav,
.dash-bar,
.dash-foot,
.modal-overlay {
    --accent: var(--brand-accent);
    --accent-light: var(--brand-accent-light);
}

.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    /* Frost the page behind the dialog, so the panel reads as glass over a
       softened scene rather than a card on a flat scrim. */
    backdrop-filter: blur(6px) saturate(1.1);
    -webkit-backdrop-filter: blur(6px) saturate(1.1);
    z-index: 300;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    animation: fadeIn 0.15s ease;
}

@keyframes fadeIn {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

/* Column layout so .modal-body is the only scroller and the footer stays put. */
.modal {
    background: var(--bg-panel);
    border: 1px solid var(--border-subtle);
    border-radius: 0.75rem;
    box-shadow: var(--shadow-panel);
    width: 100%;
    max-width: 30rem;
    max-height: 85vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    animation: slideUp 0.18s ease;
}

@keyframes slideUp {
    from {
        opacity: 0;
        transform: translateY(0.75rem) scale(0.97);
    }
    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1.1rem 1.25rem 0.9rem;
    border-bottom: 1px solid var(--border-muted);
    flex-shrink: 0;
}

.modal-title {
    font-size: var(--fs-lead);
    font-weight: 600;
    color: var(--text-strong);
}

.modal-close {
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    border: 1px solid var(--border-subtle);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s, color 0.15s;
}

.modal-close svg {
    width: 0.875rem;
    height: 0.875rem;
}

.modal-close:hover {
    background: var(--bg-elev-hover);
    color: var(--text-strong);
}

.modal-body {
    padding: 1.25rem;
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
    flex: 1;
    min-height: 0;
    overflow-y: auto;
}

.modal-footer {
    padding: 1.25rem;
    border-top: 1px solid var(--border-muted);
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    flex-shrink: 0;
}



.label-required::after {
    content: " *";
    color: var(--error-text);
    font-weight: 700;
}

.modal-label {
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--text-muted);
    letter-spacing: var(--ls-caps);
    text-transform: uppercase;
}

/* ================================================================
 * Custom select (components/Select.tsx)
 * ================================================================ */

.sel {
    position: relative;
    width: 100%;
    min-width: 0;
}

/* The one closed-select look, wherever a Select sits — a form field, a modal,
   the date picker. Context classes may tweak width or spacing, never re-skin. */
.sel-trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    width: 100%;
    height: 2.25rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--ui-field-line);
    border-radius: 0.5rem;
    background: var(--ui-input);
    color: var(--ui-ink);
    font-family: inherit;
    font-size: var(--fs-body);
    text-align: left;
    box-sizing: border-box;
    cursor: pointer;
    outline: none;
    transition: border-color 0.2s, box-shadow 0.2s;
}

.sel-trigger:focus,
.sel-trigger[aria-expanded="true"] {
    border-color: var(--ui-focus-line);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--ui-focus-line) 25%, transparent), 0 0 8px 3px color-mix(in srgb, var(--ui-focus-line) 14%, transparent);
    background: var(--ui-panel);
}

.sel-value {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The icon slot at a trigger's end; the open-state flip belongs to the
   chevron alone — a calendar or clock in the same slot must not rotate. */
.sel-icon,
.sel-chevron {
    flex: none;
    color: var(--ui-quiet);
}

.sel-chevron {
    transition: transform 0.15s;
}

.sel-trigger[aria-expanded="true"] .sel-chevron {
    transform: rotate(180deg);
}

/* Rendered in a portal on <body> and placed with inline viewport coordinates,
   so no modal, sticky bar or scroll container can clip or cover it. */
.sel-menu {
    position: fixed;
    z-index: 1000;
    width: max-content;
    max-width: min(20rem, calc(100vw - 2rem));
    max-height: 14rem;
    overflow-y: auto;
    padding: 0.25rem;
    border: 1px solid var(--ui-line);
    border-radius: 0.45rem;
    background: var(--ui-panel);
    box-shadow: 0 8px 24px rgba(0, 0, 0, .18);
    box-sizing: border-box;
}

.sel-option {
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
    width: 100%;
    padding: 0.4rem 0.5rem;
    border: none;
    border-radius: 0.3rem;
    background: none;
    color: var(--ui-ink);
    font: inherit;
    font-size: var(--fs-body);
    text-align: left;
    cursor: pointer;
}

/* One highlight for a walked-to option, via real focus (Select) or
   aria-activedescendant (TokenInput). Bold marks the chosen value only. */
.sel-option:hover,
.sel-option:focus-visible,
.sel-option--active {
    background: color-mix(in srgb, var(--ui-soft) 8%, transparent);
    outline: none;
}

.sel-option--on {
    font-weight: 600;
}

.sel-option-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Stats filter pickers (MultiPick): a counting button over a checkbox panel.
   Shared chrome, so it lives here rather than in one route's stylesheet. */
.pick {
    position: relative;
}

.pick-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: .6rem;
    height: .6rem;
    margin-left: .3rem;
    transform: rotate(90deg);
    transition: transform .15s ease;
}

.pick-mark svg {
    width: 100%;
    height: 100%;
}

/* Turned back up while the panel is out. */
.pick-mark.open {
    transform: rotate(270deg);
}

.pick .action-button.on {
    border-color: var(--ui-ink-strong);
    color: var(--ui-ink-strong);
    background: var(--ui-hover);
}

/* On <body> and against the viewport, not inside whatever box the trigger sits
   in: a column heading's filter lives in the table's own scrolling area, and a
   panel in flow there widens the scroll instead of opening over it. */
.pick-panel {
    position: fixed;
    z-index: 1000;
    min-width: 13rem;
    max-height: 16rem;
    overflow-y: auto;
    padding: 0.4rem;
    border: 1px solid var(--ui-line);
    border-radius: 0.5rem;
    background: var(--ui-panel);
    box-shadow: 0 8px 24px rgb(0 0 0 / .14);
}

.pick-row {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.3rem 0.4rem;
    border-radius: 0.375rem;
    font-size: var(--fs-body);
    color: var(--ui-ink);
    cursor: pointer;
}

.pick-row:hover {
    background: var(--ui-hover);
}

.pick-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.pick-empty,
.pick-count {
    font-size: var(--fs-sm);
    color: var(--ui-dim);
}

/* The small underlined link-button (Button.Type.AdminLink). Lives here, not in
   the admin bundle: MultiPick's clear wears it on every stats toolbar. */
.admin-link {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    font-size: var(--fs-sm);
    text-decoration: underline;
    cursor: pointer;
}

.admin-link:disabled {
    opacity: 0.5;
    cursor: default;
}

.pick-clear {
    display: block;
    margin: 0.35rem 0.4rem 0.15rem;
}

.sel-option-hint {
    margin-left: auto;
    flex: none;
    color: var(--ui-quiet);
    font-size: var(--fs-sm);
}

/* Custom date / time / datetime picker. The trigger wears the shared
   `.sel-trigger` skin; the popup is a portal panel like `.sel-menu`. */
.dtp {
    position: relative;
    width: 100%;
    min-width: 0;
}

.dtp-placeholder {
    color: var(--ui-quiet);
}

.dtp-pop {
    position: fixed;
    z-index: 1000;
    padding: 0.6rem;
    border: 1px solid var(--ui-line);
    border-radius: 0.5rem;
    background: var(--ui-panel);
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22);
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

/* Keep horizontal touch moves for the page-turn swipe. */
.dtp-cal {
    touch-action: pan-y;
}

.dtp-cal-head {
    margin-bottom: 0.4rem;
}

.dtp-month {
    font-size: var(--fs-body);
    font-weight: 600;
    color: var(--ui-ink);
}

.dtp-nav {
    width: 1.7rem;
    height: 1.7rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 0.4rem;
    background: none;
    color: var(--ui-dim);
    cursor: pointer;
}

/* One caret drawn once; the way it points is a rotation. */
.dtp-nav svg,
.dtp-clear svg {
    width: 0.9rem;
    height: 0.9rem;
}

.dtp-nav--prev svg {
    transform: rotate(180deg);
}

.dtp-nav:hover {
    background: var(--ui-hover);
    color: var(--ui-ink);
}

.dtp-grid {
    display: grid;
    grid-template-columns: repeat(7, 2rem);
    gap: 1px;
}

/* Paging the calendar: the arriving month or year fades up from the side it
   was asked for, so the two pages read as one continuous move. */
.dtp-grid[data-slide],
.dtp-unit-grid[data-slide] {
    animation: dtp-page-next .2s cubic-bezier(.22, .61, .36, 1);
}

.dtp-grid[data-slide="-1"],
.dtp-unit-grid[data-slide="-1"] {
    animation-name: dtp-page-prev;
}

@keyframes dtp-page-next {
    from {
        opacity: .35;
        transform: translateX(.55rem);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

@keyframes dtp-page-prev {
    from {
        opacity: .35;
        transform: translateX(-.55rem);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

/* Months or years, four to a row, on the same footprint as the day grid. */
.dtp-unit-grid {
    display: grid;
    grid-template-columns: repeat(4, 3.5rem);
    gap: 1px;
}

.dtp-unit-grid .dtp-day {
    width: auto;
    height: 2.2rem;
}

/* A header unit is a step into its own panel, so it reads as pressable. */
.dtp-head-unit {
    background: none;
    border: 0;
    padding: 0 0.15rem;
    font: inherit;
    color: inherit;
    cursor: pointer;
    border-radius: 5px;
}

.dtp-head-unit:hover {
    background: var(--ui-hover);
}

.dtp-wd {
    height: 1.6rem;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--fs-caption);
    color: var(--ui-quiet);
}

.dtp-day {
    position: relative;
    height: 2rem;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 0.4rem;
    background: none;
    color: var(--ui-ink);
    font-size: var(--fs-body);
    font-variant-numeric: tabular-nums;
    cursor: pointer;
    transition: background 0.12s;
    /* A long press previews the day; iOS must not answer it with its own
       callout and magnifier. */
    -webkit-touch-callout: none;
}

.dtp-day:hover {
    background: var(--ui-hover);
}

.dtp-day--out {
    color: var(--ui-quiet);
}

/* A public holiday keeps the calendar's red-letter convention. Selection ink
   (.on) still wins. */
.dtp-day--holiday {
    color: var(--ui-bad-ink);
}

/* Today: a ring rather than a mark or padding, so nothing moves. Colourless,
   so a holiday keeps its red and a weekend its ink. */
.dtp-day--today {
    font-weight: 700;
    box-shadow: inset 0 0 0 1px var(--ui-line-strong);
}

/* Picked, the filled cell is the emphasis; a ring inside it only muddies. */
.dtp-day--today.on {
    box-shadow: none;
}

.dtp-foot {
    margin: 0.15rem .125rem 0;
    width: 0;
    min-width: calc(100% - .25rem);
}

.dtp-day-note {
    margin: 0;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: var(--fs-caption);
    line-height: var(--lh-caption);
    color: var(--ui-quiet);
}

/* One named schedule in the caption, keyed by its mark color. */
.dtp-note-item {
    margin-left: 0.45rem;
}

.dtp-note-dot {
    display: inline-block;
    width: 5px;
    height: 5px;
    margin-right: 0.25rem;
    border-radius: 50%;
    background: var(--ui-mark);
    vertical-align: 1px;
}

.dtp-foot-btn {
    padding: 0;
    border: 0;
    background: none;
    font-family: inherit;
    font-size: var(--fs-caption);
    line-height: var(--lh-caption);
    color: var(--ui-soft);
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
    min-width: fit-content;
}

.dtp-foot-btn:hover {
    color: var(--ui-ink);
}

/* Days strictly between a range's ends: a wash of the selection ink, stronger
   than hover so the band reads at a glance. Selection ink (.on) outranks it. */
.dtp-day--mid {
    background: color-mix(in srgb, var(--ui-ink) 9%, transparent);
}

/* Week mode: the selected week reads as one continuous bar. Opaque mixes and a
   1px shadow bleed into the grid gaps; only the row ends keep outer radius. */
.dtp-day--wk,
.dtp-day--wk-hover {
    border-radius: 0;
    background: var(--wk-bar);
    box-shadow: 1px 0 0 var(--wk-bar);
}

.dtp-day--wk {
    --wk-bar: color-mix(in srgb, var(--ui-ink) 14%, var(--ui-panel));
}

.dtp-day--wk-hover {
    --wk-bar: color-mix(in srgb, var(--ui-ink) 7%, var(--ui-panel));
}

.dtp-day--wk-first {
    border-radius: 0.4rem 0 0 0.4rem;
}

.dtp-day--wk-last {
    border-radius: 0 0.4rem 0.4rem 0;
    box-shadow: none;
}

/* Schedule marks: a dot for one day, a bar rounded at its true ends for a span;
   middle segments bleed into the 1px gaps. Lanes stack upward, 5px apart. */
.dtp-grid--marked .dtp-day {
    height: 2.25rem;
    padding-bottom: 12px;
}

.dtp-container {
    position: absolute;
    pointer-events: none;
    left: -1px;
    right: -1px;
    bottom: 3px;
    overflow-x: hidden;
    overflow-y: visible;
    padding: 3px 0;
}

.dtp-mark {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 2px;
    width: calc(100% + 2px);
    height: 2px;
    background: var(--ui-mark);
    pointer-events: none;
    box-shadow: var(--shadow-dtp-mark);
}

.dtp-mark--dot {
    left: 50%;
    width: 3px;
    height: 3px;
    border-radius: 50%;
    transform: translateX(-50%);
    box-shadow: var(--shadow-dtp-mark--dot);
}

.dtp-mark--start {
    left: 6px;
    right: -1px;
    border-radius: 999px 0 0 999px;
}

.dtp-mark--end {
    left: -1px;
    right: 6px;
    border-radius: 0 999px 999px 0;
}

.dtp-mark--mid {
    left: -1px;
    right: -1px;
}

/* A day carrying more entries than the lanes can hold. Pressing it opens the
   day's full list, so it wears a small chip to read as pressable. */
.dtp-mark-extra {
    position: absolute;
    top: 1px;
    right: 1px;
    padding: 1px 3px 2px;
    border-radius: 0.25rem;
    background: color-mix(in srgb, var(--ui-ink) 12%, transparent);
    font-size: 8px;
    font-style: normal;
    line-height: 1;
    color: var(--ui-ink);
    cursor: pointer;
}

.dtp-mark-extra:hover {
    background: color-mix(in srgb, var(--ui-ink) 22%, transparent);
}

/* Everything on one day, opened from its "+N": a small anchored panel whose
   scroll absorbs any number of entries. */
.dtp-day-list {
    position: absolute;
    z-index: 1;
    width: 11rem;
    max-height: 9rem;
    overflow-y: auto;
    padding: 0.45rem 0.55rem;
    border: 1px solid var(--ui-line);
    border-radius: 0.4rem;
    background: var(--ui-panel);
    box-shadow: 0 8px 24px rgb(0 0 0 / .18);
}

.dtp-day-list-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.4rem;
    margin: 0 0 0.25rem;
    font-size: var(--fs-caption);
    line-height: var(--lh-caption);
    color: var(--ui-dim);
}

/* The preview's way out: a keyboard's Escape is no help on a touch screen. */
.dtp-day-list-close {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.4rem;
    height: 1.4rem;
    margin: -0.15rem -0.2rem -0.15rem 0;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--ui-dim);
    cursor: pointer;
    transition: background .12s, color .12s;
}

.dtp-day-list-close svg {
    width: 0.75rem;
    height: 0.75rem;
}

.dtp-day-list-close:hover {
    background: var(--ui-hover);
    color: var(--ui-ink);
}

.dtp-day-list-item {
    margin: 0;
    padding: 0.1rem 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: var(--fs-caption);
    line-height: var(--lh-caption);
    color: var(--ui-ink);
}

.dtp-day.on {
    background: var(--ui-ink);
    color: var(--ui-panel);
}



.dtp-time-input {
    width: 2.5rem;
    text-align: center;
    padding: 0.4rem 0.3rem;
    border: 1px solid var(--ui-field-line);
    border-radius: 0.4rem;
    background: var(--ui-input);
    color: var(--ui-ink);
    font-size: var(--fs-body);
    font-variant-numeric: tabular-nums;
    outline: none;
    box-sizing: border-box;
    transition: border-color 0.15s;
}

.dtp-time-input:focus {
    border-color: var(--ui-focus-line);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--ui-focus-line) 25%, transparent), 0 0 8px 3px color-mix(in srgb, var(--ui-focus-line) 14%, transparent);
}

.dtp-colon {
    color: var(--ui-dim);
    font-weight: 600;
}

.dtp-clear {
    margin-left: auto;
    width: 1.7rem;
    height: 1.7rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 0.4rem;
    background: none;
    color: var(--ui-dim);
    cursor: pointer;
}

.dtp-clear:hover {
    background: var(--ui-hover);
    color: var(--ui-ink);
}

.dtp-clear--row {
    margin: 0 0 0 auto;
}

/* ================================================================
 * Embed Modal
 * ================================================================ */

.embed-code-wrap {
    position: relative;
    border-radius: 0.5rem;
    overflow: hidden;
    border: 1px solid var(--border-subtle);
}

.embed-iframe-wrap {
    position: relative;
    border-radius: 0.5rem;
    overflow: hidden;
    border: 1px solid var(--border-subtle);
}

.embed-footer-head {
    margin-bottom: 0.2rem;
}

/* The section's name doubles as its fold, so there is one thing to press and it
   is the thing being folded. */
.embed-fold {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: 0;
    border: 0;
    background: none;
    /* Only the family: the size, weight and caps are `.modal-label`'s, and a
       shorthand here would reset them to the row's. */
    font-family: inherit;
    cursor: pointer;
}

.embed-fold:hover .chevron {
    color: var(--accent);
}



.embed-code-wrap {
    border: 0;
}

.embed-iframe-wrap {
    height: 280px;
}

/* The panes swap in place, so the hidden one must not reserve space. */
.embed-code-wrap[hidden],
.embed-iframe-wrap[hidden] {
    display: none;
}

/* Matches the preview iframe so swapping does not resize the modal. */
.modal-footer .embed-code-wrap {
    height: 280px;
}

.embed-code-wrap pre {
    height: 280px;
    margin: 0;
    font-size: var(--fs-sm);
    line-height: 1.6;
    overflow-x: auto;
    background: var(--gray-100) !important;
}

.embed-code-wrap code {
    height: 100%;
    background: transparent !important;
    font-family: var(--font-mono);
}

.embed-desc {
    font-size: var(--fs-sm);
    color: var(--text-muted);
    line-height: 1.5;
    display: flex;
    flex-direction: column;
    gap: .25rem
}

/* ================================================================
 * Section chips
 * ================================================================ */

/* Its own scroll. A long list used to widen the dialog, and through it the page
   behind — a row of chips is not a reason for the whole screen to slide. */

.quiet kbd {
    padding: .05rem .3rem;
    border: 1px solid var(--border-subtle);
    border-radius: 3px;
    background: var(--bg-elev);
    font: inherit;
    font-size: .95em;
}

.section-chips {
    overflow-x: auto;
    overscroll-behavior-x: contain;
    /* Room for the focus ring, which the scroll clip would otherwise cut. */
    padding: 4px 4px .35rem;
    display: flex;
    align-content: flex-start;
    justify-content: flex-start;
    flex-wrap: wrap;
    gap: 0.375rem;
    min-height: calc(5.25rem + 14px);
}

.section-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.25rem 0.5rem 0.25rem 0.625rem;
    border-radius: 999px;
    border: 1px solid var(--accent);
    background: var(--accent-light);
    color: var(--accent);
    font-size: var(--fs-sm);
    font-weight: 500;
    height: 1.5rem;
}

.section-chip-label {
    line-height: 1;
}

.section-chip-remove {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1rem;
    height: 1rem;
    border-radius: 50%;
    border: none;
    background: transparent;
    color: var(--accent);
    cursor: pointer;
    padding: 0;
    transition: background 0.15s, color 0.15s;
    flex-shrink: 0;
}

.section-chip-remove svg {
    width: 0.625rem;
    height: 0.625rem;
}

.section-chip-remove:hover {
    background: var(--accent);
    color: #fff;
}

.section-presets {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin-top: 0.5rem;
}

.section-preset-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.25rem 0.625rem;
    border-radius: 999px;
    border: 1px solid var(--border-subtle);
    background: var(--bg-elev);
    color: var(--text-muted);
    font-size: var(--fs-sm);
    font-weight: 500;
    cursor: pointer;
    transition: border-color 0.15s, color 0.15s, background 0.15s;
    height: 1.5rem;
}

/* The add mark takes its size from the button, not from an attribute on the
   drawing: the drawing is the site's one and carries none. */
.section-preset-btn svg {
    width: .625rem;
    height: .625rem;
    flex: none;
}

.section-preset-btn:hover {
    border-color: var(--accent);
    color: var(--accent);
    background: var(--accent-light);
}

/* One checkbox-and-label control: the list select-all, the record toggle, any
   inline option. accent-color follows the mark token per the house style. */
.option-toggle,
.select-bar .select-check {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
    user-select: none;
}

.option-toggle input,
.select-bar .select-check input {
    flex-shrink: 0;
}

.option-toggle-text,
.select-bar .select-check span {
    font-size: var(--fs-body);
    color: var(--ui-dim);
}

/* ================================================================
 * Embed playground — a live frame beside its message traffic
 * ================================================================ */

.pg-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin: 0.75rem 0 1rem;
}

.pg-btn {
    padding: 0.35rem 0.7rem;
    font-size: var(--fs-sm);
    font-weight: 600;
    border: 1px solid var(--border-subtle);
    border-radius: 0.4rem;
    background: var(--bg-panel);
    color: var(--text-body);
    cursor: pointer;
    transition: border-color 0.15s, color 0.15s;
}

.pg-btn:hover {
    border-color: var(--accent);
    color: var(--accent);
}

/* One column: the frame is the thing being read, and the traffic sits under it
   rather than stealing half the width from it. */
.pg-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0.75rem;
}

/* How many messages are waiting behind the fold. */
.pg-count {
    font-size: var(--fs-caption);
    color: var(--text-faint);
}

.pg-pane {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    min-width: 0;
}



.pg-pane-title {
    font-size: var(--fs-caption);
    font-weight: 700;
    letter-spacing: var(--ls-caps);
    text-transform: uppercase;
    color: var(--text-faint);
}

/* A quiet text button that throws something away: the playground's log, the
   assistant's conversation. Named for what it does, not where it first was. */
.clear-btn {
    font-size: var(--fs-caption);
    border: none;
    background: none;
    color: var(--text-faint);
    cursor: pointer;
    padding: 0;
}

.clear-btn:hover {
    color: var(--accent);
}

/* The live embed, wherever one is shown: the reference's playground and the
   preview under an assistant's answer are the same frame. */
.embed-frame-wrap {
    border: 1px solid var(--border-subtle);
    border-radius: 0.5rem;
    overflow: hidden;
    margin-bottom: .5rem;
}

.embed-frame {
    display: block;
    width: 100%;
    /* Until the first `appatlas:height` arrives there is nothing to measure. */
    height: 12rem;
    border: 0;
    transition: height 0.3s ease;
}

.pg-log {
    list-style: none;
    margin: 0;
    padding: 0.4rem;
    border: 1px solid var(--border-subtle);
    border-radius: 0.5rem;
    background: var(--bg-elev);
    height: 16rem;
    overflow: auto;
    font-size: var(--fs-sm);
}

.pg-empty {
    color: var(--text-faint);
    padding: 0.4rem;
}

.pg-entry {
    display: grid;
    grid-template-columns: 0.9rem 4.2rem auto;
    gap: 0.3rem;
    align-items: baseline;
    padding: 0.28rem 0.3rem;
    border-bottom: 1px solid var(--border-muted);
}

.pg-entry:last-child {
    border-bottom: none;
}

.pg-dir {
    color: var(--text-faint);
}

.pg-dir svg {
    width: 0.85rem;
    height: 0.85rem;
    display: block;
}

/* The same arrow, turned: down is what arrived, up is what this page sent. */
.pg-entry.out .pg-dir {
    color: var(--accent);
}

.pg-entry.out .pg-dir svg {
    transform: rotate(180deg);
}

.pg-at {
    color: var(--text-faint);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.pg-type {
    font-weight: 700;
    color: var(--text-strong);
}

.pg-body {
    grid-column: 1 / -1;
    color: var(--text-muted);
    word-break: break-all;
    font-size: var(--fs-caption);
}@media (max-width: 60rem) {

    .pg-log {
        height: 16rem;
    }
}

/* Collapsible group of embed options. */
.option-group {
    border-top: 1px solid var(--border-muted);
    padding-top: 0.75rem;
}

.option-group:first-child {
    border-top-width: 0 !important;
    padding-top: 0 !important;
}

.option-group-head {
    cursor: pointer;
    user-select: none;
    list-style: none;
}

.option-group-head::-webkit-details-marker {
    display: none;
}

.option-group-head .modal-label {
    cursor: pointer;
}

/* The box turns; the path inside is the site's one caret, which carries no
   size of its own. */
.option-group-mark {
    display: inline-flex;
    width: .625rem;
    height: .625rem;
    color: var(--text-faint);
    transition: transform 0.15s ease;
    flex-shrink: 0;
}

.option-group-mark svg {
    width: 100%;
    height: 100%;
}

.option-group[open] .option-group-mark {
    transform: rotate(90deg);
}

.option-group-body {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    padding-top: 0.6rem;
}

/* One analytics block's charts and window, inside the embed form. */
.analytics-block {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    padding: 0.6rem 0.7rem;
    border: 1px solid var(--border-subtle);
    border-radius: 0.5rem;
}



.analytics-block-id {
    width: 8rem;
    flex: none;
    font-size: var(--fs-sm);
    padding: 0.2rem 0.4rem;
}

.analytics-block-title {
    font-family: var(--font-mono);
    font-size: var(--fs-caption);
    color: var(--text-faint);
}



.analytics-window .modal-select {
    width: auto;
}

.analytics-window .sel {
    width: auto;
}

.analytics-window .input,
.analytics-window .dtp {
    width: auto;
    min-width: 0;
    flex: 1 1 7rem;
}

/* A chart the block draws, as opposed to one it could add. */
.section-preset-btn.on {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}

/* What the selected option does, under the control that chose it. */
.option-hint {
    white-space: pre-line;
    /* Sentence-length Korean copy reads one rung above the caption sizes. */
    font-size: var(--fs-body);
    color: var(--text-faint);
    line-height: var(--lh-body);
    margin-top: 0.3rem;
    display: flex;
    flex-direction: column;
    gap: .25rem;
}

/* Disabled controls should read as unavailable, not merely unstyled. */
.modal-select:disabled,
.option-toggle input:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

.option-toggle:has(input:disabled) {
    cursor: not-allowed;
}

.option-toggle:has(input:disabled) .option-toggle-text {
    opacity: 0.45;
}

/* Result cards that cannot open another app should not look clickable. */
.result-card--static,
.result-card--static:hover {
    cursor: default;
    /*background: var(--bg-elev);*/
    border-color: transparent;
}

/* ================================================================
 * Color picker
 * ================================================================ */

.color-picker-trigger {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--ui-field-line);
    border-radius: 0.5rem;
    background: var(--ui-input);
    color: var(--ui-ink);
    font-size: var(--fs-base);
    cursor: pointer;
    transition: border-color 0.15s;
    box-sizing: border-box;
}

.color-picker-trigger:hover,
.color-picker-trigger:focus-visible,
.color-picker-trigger[aria-expanded="true"] {
    border-color: var(--ui-focus-line);
    outline: none;
}

.color-picker-trigger:focus-visible,
.color-picker-trigger[aria-expanded="true"] {
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--ui-focus-line) 25%, transparent), 0 0 8px 3px color-mix(in srgb, var(--ui-focus-line) 14%, transparent);
    background: var(--ui-panel);
}

.color-picker-value {
    font-family: var(--font-mono);
    font-size: var(--fs-body);
}

/* Checkerboard shows through wherever --swatch is translucent. */
.color-swatch {
    width: 1.125rem;
    height: 1.125rem;
    border-radius: 0.25rem;
    flex-shrink: 0;
    box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18);
    background-image: linear-gradient(var(--swatch), var(--swatch)),
    conic-gradient(#d0d0d0 0 25%, #fff 0 50%, #d0d0d0 0 75%, #fff 0);
    background-size: auto, 8px 8px;
}

/* Keeps .chips-popover's z-index so it clears the modal overlay. */
.color-picker-popover {
    padding: 0.75rem;
    width: 17rem;
    max-width: none;
    display: flex;
    flex-direction: column;
    gap: 0.625rem;
}

.color-area {
    position: relative;
    width: 100%;
    height: 8.5rem;
    border-radius: 0.375rem;
    cursor: crosshair;
    touch-action: none;
    overflow: hidden;
}

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

.color-area-saturation,
.color-area-value {
    position: absolute;
    inset: 0;
}

.color-area-saturation {
    background: linear-gradient(to right, #fff, transparent);
}

.color-area-value {
    background: linear-gradient(to top, #000, transparent);
}

.color-marker {
    position: absolute;
    width: 0.75rem;
    height: 0.75rem;
    border-radius: 50%;
    border: 2px solid #fff;
    box-shadow: 0 0 0 1px rgb(0 0 0 / 0.35);
    transform: translate(-50%, -50%);
    pointer-events: none;
    transition: left .18s cubic-bezier(.22, .61, .36, 1),
                top .18s cubic-bezier(.22, .61, .36, 1),
                background-color .18s linear;
}

/* Under the finger, not chasing it. */
.color-marker.dragging,
.color-slider-thumb.dragging {
    transition: none;
}



.color-slider {
    position: relative;
    height: 0.625rem;
    border-radius: 999px;
    cursor: pointer;
    touch-action: none;
}

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

.color-slider--hue {
    background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
}

/* Only the alpha track needs the checkerboard behind its gradient. */
.color-slider--alpha {
    background-image: conic-gradient(#d0d0d0 0 25%, #fff 0 50%, #d0d0d0 0 75%, #fff 0);
    background-size: 8px 8px;
}

.color-slider-track {
    position: absolute;
    inset: 0;
    border-radius: inherit;
}

.color-slider-thumb {
    position: absolute;
    top: 50%;
    width: 0.875rem;
    height: 0.875rem;
    border-radius: 50%;
    border: 2px solid #fff;
    box-shadow: 0 0 0 1px rgb(0 0 0 / 0.35);
    transform: translate(-50%, -50%);
    pointer-events: none;
    transition: left .18s cubic-bezier(.22, .61, .36, 1),
                background-color .18s linear;
}



.color-format-btn,
.color-reset-btn {
    padding: 0.3rem 0.5rem;
    border: 1px solid var(--border-subtle);
    border-radius: 0.3rem;
    background: var(--bg-elev);
    color: var(--text-muted);
    font-size: var(--fs-caption);
    font-weight: 500;
    cursor: pointer;
    flex-shrink: 0;
    transition: border-color 0.15s, color 0.15s;
}

.color-format-btn {
    text-transform: uppercase;
    width: 2.5rem;
    text-align: center;
}

.color-format-btn:hover,
.color-reset-btn:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.color-input {
    flex: 1;
    min-width: 0;
    padding: 0.3rem 0.5rem;
    border: 1px solid var(--ui-field-line);
    border-radius: 0.5rem;
    background: var(--ui-input);
    color: var(--ui-ink);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    outline: none;
    transition: border-color 0.15s;
}

.color-input:focus {
    border-color: var(--accent);
}

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

.color-swatches {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 0.3rem;
}

.color-swatch--btn {
    width: 100%;
    aspect-ratio: 1;
    height: auto;
    border: none;
    padding: 0;
    cursor: pointer;
}

.color-swatch--btn.selected {
    box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18), 0 0 0 2px var(--accent);
}

.color-swatch--btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

/* ================================================================
 * Snapshot Charts
 * ================================================================ */

.snapshot-header {
    margin-bottom: 1rem;
}

.window-caption {
    font-size: var(--fs-sm);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* In the header row a period control is one control among peers: sized to its
   text, the stats-toolbar way. The picker slot fits a two-date range. */
.snapshot-header .sel {
    width: 9rem;
    flex: none;
}

.snapshot-header .dtp {
    width: 13rem;
    flex: none;
}


/* What an expanded card stands at, named because the reveal has to know it
   before the card has finished growing into it. */
:root {
    --chart-expand-h: 300px;
}

.chart-card {
    /* Two bars are pinned above the dashboard's content — the nav, and the
       dashboard's own beneath it at `top: 3.6rem` — so a card scrolled to has
       to clear both. Clearing a little too much only seats it lower; too
       little puts its head under a bar, which is what this is for. */
    scroll-margin-top: 8.5rem;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    position: relative;
    height: 140px;
    background: var(--bg-elev);
    border-radius: 0.625rem;
    transition: height 0.5s cubic-bezier(0.25, 1, 0.5, 1);
    cursor: pointer;
}

.chart-container {
    display: grid;
    flex-direction: row;
    gap: 0.625rem;
    padding-bottom: 1.25rem;
    grid-template-columns: repeat(auto-fill, minmax(11.25rem, 1fr));
}

.chart-container::-webkit-scrollbar {
    height: 6px;
}

.chart-container::-webkit-scrollbar-thumb {
    background: var(--scrollbar-thumb);
    border-radius: 3px;
}

.chart-floating {
    color: var(--text-muted);
    display: flex;
    flex-direction: column;
    gap: .25em;
    top: 12px;
    left: 0;
    padding: 12px 12px 0;
    transition: left 0.3s linear;
    font-size: var(--fs-lead);
}

/* The toggle itself: a button wearing the card's own typography. */
/* Pinned to the card's own corner rather than laid beside the figures: inside
   their block it took a column of its own and floated at whatever height the
   rows left it, belonging to none of them. */
.chart-note {
    position: absolute;
    top: 12px;
    right: 12px;
    max-width: 45%;
    margin: 0;
    text-align: right;
    font-size: var(--fs-sm);
    line-height: var(--lh-sm);
    color: var(--text-faint);
    pointer-events: none;
}

.chart-floating .title {
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
    font-size: 0.675em;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: var(--ls-caps);
}

.chart-floating .title:focus-visible {
    outline: 2px solid var(--ui-focus-line);
    outline-offset: 2px;
    border-radius: 2px;
}

/* The drawn range, and what the window amounts to. */
.window-line {
    font-size: 0.675em;
    color: var(--text-faint);
    font-variant-numeric: tabular-nums;
}

.chart-floating .value {
    display: flex;
    align-items: flex-end;
    gap: .5em;
}

.chart-floating .value .absolute {
    font-size: 1.375em;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: var(--ls-caps);
}

.chart {
    position: relative;
    height: 80px;
    padding: 0 0 10px;
}

.chart-container:has(.expand) {
    display: block;
}

.chart-container:has(.expand) .chart-card:not(.expand) {
    display: none;
}

.chart-card.expand {
    width: 100%;
    height: var(--chart-expand-h);
}

.expand .chart {
    flex: 1;
    min-height: 0;
    height: auto;
    padding: 0 12px 10px;
}

.chart-legend {
    padding: 0 12px 10px;
    font-size: var(--fs-caption);
    color: var(--text-muted);
}

.chart-swatch {
    width: 0.6rem;
    height: 0.6rem;
    border-radius: 0.2rem;
}

.chart-swatch-line {
    width: 1rem;
    border-top: 2px solid;
}

.chart-swatch-line.dashed {
    border-top-style: dashed;
}

/* Requested metrics neither channel reports for this app. */
.series-note {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

/* Hung under `body` rather than inside the card, and so fixed to the viewport
   rather than to a box that changes width when the card opens and closes. A
   card's own overflow is then none of its business, and a hidden tooltip can
   no longer widen the page. The layer is the one menus and pickers use. */
.chart-hover-tooltip {
    position: fixed;
    pointer-events: none;
    z-index: 1000;
    background: var(--bg-panel);
    border: 1px solid var(--border-subtle);
    border-radius: 0.5rem;
    padding: 0.4rem 0.65rem;
    box-shadow: var(--shadow-panel);
    white-space: nowrap;
    transition: opacity 0.1s ease;
}

.chart-hover-sections {
    margin-bottom: 0.3rem;
    padding-bottom: 0.3rem;
    border-bottom: 1px solid var(--border-muted);
}

.chart-hover-sections:nth-last-child(1) {
    margin-bottom: 0;
    border-bottom: none;
}

.chart-hover-header { gap: .05rem; }

.chart-hover-date {
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

.chart-hover-version {
    font-size: var(--fs-caption);
    color: var(--text-faint);
}



.chart-hover-star-container {
    display: grid;
    grid-template-columns: repeat(3, auto);
    gap: .75rem 2rem;
    justify-items: start;
}

.chart-hover-value {
    font-size: var(--fs-base);
    font-weight: 700;
    color: var(--text-strong);
    font-variant-numeric: tabular-nums;
    position: relative;
    display: flex;
    align-items: center;
    gap: .2rem;
}

.chart-hover-value::before {
    content: ' ';
    width: .5rem;
    height: .5rem;
    border-radius: .25rem;
    background-color: var(--color);
    display: var(--color, none);
}

.type-1 .chart-hover-sub-value {
    font-size: var(--fs-caption);
    font-weight: 500;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.chart-hover-sub-value {
    font-weight: 500;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.chart-hover-sub-value.type-1 {
    font-size: var(--fs-caption);
}

.chart-hover-sub-value.type-2 {
    font-size: var(--fs-sm);
}

.chart-hover-sub-value.type-3 {
    font-weight: 700;
    color: var(--text-strong);
    font-size: var(--fs-sm);
}

.chart-hover-sub-value.positive {
    color: var(--ui-good-ink);
}

.chart-hover-sub-value.negative {
    color: var(--ui-bad-ink);
}

.chart-floating .value .relative {
    font-size: 0.875em;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: var(--ls-caps);
    height: 1em;
    overflow: hidden;
    line-height: 1;
    position: relative;
    top: -.2rem;
}

.chart-floating .value .relative.positive {
    color: var(--ui-good-ink);
}

.chart-floating .value .relative.negative {
    color: var(--ui-bad-ink);
}

.chart-floating .value .relative-roll {
    display: flex;
    flex-direction: column;
    transform: translateY(0);
}

.chart-floating .value .relative-roll.rolling {
    transform: translateY(-1em);
    transition: transform 0.75s cubic-bezier(0.25, 1, 0.5, 1);
}

.chart-floating .value .relative-item {
    height: 1em;
    line-height: 1;
    display: flex;
    align-items: center;
}

/* One channel's note sits above another channel's cards, so it needs room to
   read as its own block rather than as a caption for the charts. */
.request-observe-container + .chart-container,
.request-observe-container + .request-observe-container {
    margin-top: 1.1rem;
}

.request-observe-container {
    font-size: var(--fs-body);
    font-weight: 600;
    color: var(--text-body);
    margin-bottom: 4px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .75rem;
}

.collecting-app-icon {
    width: 1.5rem;
    height: 1.5rem;
    animation: spin 5s linear infinite;
}

/* ================================================================
 * News section
 * ================================================================ */

.news-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.625rem;
}

.news-list-item {
    background: var(--bg-elev);
    border: 1px solid transparent;
    border-radius: 0.75rem;
    overflow: hidden;
    transition: background 0.15s, border-color 0.15s;
}

.news-list-item:hover {
    background: var(--bg-elev-hover);
    border-color: var(--border-subtle);
}

.news-link {
    display: flex;
    align-items: stretch;
    gap: 0.875rem;
    padding: 0.875rem 1rem;
    text-decoration: none;
    color: inherit;
}

.news-thumbnail,
.news-thumbnail-placeholder { flex: 0 0 auto; width: 7.5rem; height: 5rem; border-radius: 0.5rem; object-fit: cover; background: var(--gray-100); box-shadow: var(--shadow-media); }



.news-thumbnail-fallback-icon {
    width: 1.75rem;
    height: 1.75rem;
    opacity: 0.4;
    border-radius: 4px;
}

.news-content {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 0.375rem;
}

.news-meta {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    font-size: var(--fs-caption);
    color: var(--text-faint);
    min-height: 1rem;
}

.news-source-icon {
    width: 0.875rem;
    height: 0.875rem;
    border-radius: 3px;
    flex-shrink: 0;
}

.news-source {
    font-weight: 500;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 12.5rem;
}

.news-meta-divider {
    color: var(--text-faint);
    flex-shrink: 0;
}

.news-date {
    white-space: nowrap;
    flex-shrink: 0;
    font-variant-numeric: tabular-nums;
}

.news-card-title {
    font-size: var(--fs-body);
    font-weight: 600;
    line-height: 1.4;
    color: var(--text-strong);

    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    word-break: keep-all;
}

.no-news {
    text-align: center;
    padding: 1.25rem;
    color: var(--text-faint);
    font-size: var(--fs-sm);
}@media (max-width: 600px) {

    .news-link {
        gap: 0.75rem;
        padding: 0.75rem;
    }

    .news-thumbnail,
    .news-thumbnail-placeholder {
        width: 5.5rem;
        height: 4rem;
    }

    .news-card-title {
        -webkit-line-clamp: 3;
    }
}

.review-reply {
    margin-top: 10px;
    margin-left: 12px;
    padding: 10px 12px;
    border-left: 2px solid var(--accent);
    background: color-mix(in srgb, var(--accent) 6%, transparent);
    border-radius: 0 8px 8px 0;
}

.review-reply-meta {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: var(--fs-sm);
    opacity: 0.7;
    margin-bottom: 4px;
}

.review-reply-author {
    font-weight: 600;
    opacity: 1;
}

.review-reply-content {
    margin: 0;
    font-size: var(--fs-body);
    line-height: 1.5;
}

.similar-section-title--plain {
    cursor: default;
}

/* Zero specificity, so a class passed alongside it (.news-link, .dev-link)
   keeps its own display and padding whatever the source order. */
:where(.external-link) {
    font: inherit;
    color: inherit;
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    text-align: inherit;
    cursor: pointer;
}

.external-link-popover {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    max-width: 22rem;
}

.external-link-url {
    flex: 1 1 auto;
    user-select: all;
    -webkit-user-select: all;
    min-width: 0;
    font-size: var(--fs-sm);
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    direction: ltr;
}

.icon-btns {
    flex: 0 0 auto;
    display: flex;
    gap: 0.25rem;
}

/* A pressed icon control is the reader's standing choice, marked with the
   surface's ink rather than an accent. */
.icon-btn[aria-pressed="true"] {
    color: var(--ui-mark);
}

.icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    color: var(--ui-dim);
    background: none;
    /* No outline: a quiet action beside a value it acts on. The hover ground is
       the affordance, which is also what the app's other bare buttons use. */
    border: 0;
    border-radius: 0.375rem;
    cursor: pointer;
}

.icon-btn:hover {
    color: var(--ui-ink);
    background: var(--ui-hover);
}

.icon-btn svg {
    width: 0.875rem;
    height: 0.875rem;
}

/* ================================================================
 * API documentation
 * ================================================================ */

.docs-page {
    --accent: var(--brand-accent);
    --accent-light: var(--brand-accent-light);
}

/* The reading is capped; the bar and the footer are the screen's. */
.docs-body {
    flex: 1;
    width: 100%;
    max-width: 68rem;
    margin: 0 auto;
    padding: 5.5rem 1rem 4rem;
    box-sizing: border-box;
}


/* Which of the two references is open, at the top of the sidebar. */

/* The docs' strip stays put while the page scrolls under it, and its tabs
   share the width between them. */
.docs-tabs {
    position: sticky;
    top: 0;
    left: 0;
    width: 100%;
    margin-bottom: 1.25rem;
}

.docs-tabs button {
    flex: 1;
    /* Four tabs share the sidebar's 15rem: the seg's default side padding
       overflows the strip, so the flex share carries the width instead. */
    min-width: 0;
    padding-inline: 0.25rem;
    white-space: nowrap;
}

.embed-docs-link {
    color: var(--accent);
    font-weight: 600;
    white-space: nowrap;
}

.docs-body {
    display: grid;
    grid-template-columns: 15rem minmax(0, 1fr);
    gap: 2rem;
    align-items: start;
}

.docs-toc {
    position: sticky;
    /* Clear of the page bar, which is fixed and 3.6rem tall. */
    top: 5.5rem;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    /* The bar takes its share of the window too, or a long list runs off the
       bottom by exactly that much. */
    max-height: calc(100vh - 7rem);
    overflow-y: auto;
    /* Long entries wrap; the sidebar never scrolls horizontally. */
    overflow-x: hidden;
    padding-right: 0.5rem;
}

/* The search field, between the document tabs and the contents list. Placement
   only: the field itself is the app's one input. */
.docs-toc > .sel {
    margin-bottom: 1rem;
}

.docs-toc-title {
    font-size: var(--fs-caption);
    font-weight: 700;
    letter-spacing: var(--ls-caps);
    color: var(--text-faint);
    margin-bottom: 0.4rem;
}

/* A label over a run of rows: the rows' typography and text column, no
   hover, because there is nothing to press. */
.docs-toc-group {
    font-size: var(--fs-caption);
    font-weight: 700;
    letter-spacing: var(--ls-caps);
    color: var(--text-faint);
    margin: 0.75rem 0 0.25rem;
    padding: 0 0.5rem;
}

/* The default skin of a contents link: weightless, so a top-level row that
   wears .docs-toc-cat keeps that skin over this one. */
/* Every row in the contents list, top level or unfolded, reads in one voice:
   the group-label typography. Depth is the indent, openness is the chevron. */
:where(.docs-toc) a {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.3rem 0.5rem;
    border-radius: 0.375rem;
    font-size: var(--fs-caption);
    font-weight: 700;
    letter-spacing: var(--ls-caps);
    color: var(--text-faint);
    text-decoration: none;
    transition: background 0.15s, color 0.15s;
}

:where(.docs-toc) a:hover {
    background: var(--accent-light);
    color: var(--accent);
}

.docs-toc-sub {
    padding-left: 0.75rem !important;
}

/* A topic inside a module, one rung deeper again. */
.docs-toc-sub2 {
    padding-left: 1.5rem !important;
}

:where(.docs-toc) a {
    min-width: 0;
}

:where(.docs-toc) a .docs-method {
    flex-shrink: 0;
}

.docs-toc-label {
    min-width: 0;
    overflow-wrap: anywhere;
}

/* A top-level row of the contents list: a collapsible group's header or a
   plain link to a section. One skin for both, group-label typography, and the
   chevron alone says which one opens. */
.docs-toc-cat {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    width: 100%;
    padding: 0.3rem 0.5rem;
    margin-top: 0.2rem;
    border: none;
    border-radius: 0.375rem;
    background: none;
    cursor: pointer;
    text-align: left;
    font-size: var(--fs-caption);
    font-weight: 700;
    letter-spacing: var(--ls-caps);
    color: var(--text-faint);
    text-decoration: none;
    transition: background 0.15s, color 0.15s;
}

.docs-toc-cat:hover {
    background: var(--accent-light);
    color: var(--accent);
}

.docs-toc-cat .chevron {
    width: 0.8rem;
    height: 0.8rem;
}

.docs-toc-count {
    margin-left: auto;
    font-weight: 500;
    color: var(--text-faint);
}

.docs-main {
    min-width: 0;
}

.docs-title {
    font-size: var(--fs-display);
    font-weight: 700;
    color: var(--text-strong);
    margin-bottom: 0.35rem;
}

.docs-subtitle {
    font-size: var(--fs-lead);
    color: var(--text-muted);
    margin-bottom: 2rem;
}

.docs-h2 {
    font-size: var(--fs-heading);
    font-weight: 700;
    color: var(--text-strong);
    margin: 2.5rem 0 0.75rem;
    padding-bottom: 0.4rem;
    border-bottom: 1px solid var(--border-subtle);
    scroll-margin-top: 1.5rem;
}

/* A section inside a page section: one rung under .docs-h2, and the same
   idea — the rule under it is the boundary, so nothing needs a box. */
.docs-h3 {
    font-size: var(--fs-title);
    line-height: var(--lh-title);
    font-weight: 600;
    color: var(--text-strong);
    margin: 2rem 0 0.6rem;
    padding-bottom: 0.3rem;
    border-bottom: 1px solid var(--border-subtle);
    scroll-margin-top: 1.5rem;
}

/* A topic inside a module: named, anchored, but no rule — the module's rule
   above is the boundary, and a second one would compete with it. */
.docs-h4 {
    /* One step over body copy: at the body size it read as a bold sentence
       rather than the start of a topic, and it carries no rule to say so. */
    font-size: var(--fs-lead);
    line-height: var(--lh-lead);
    font-weight: 600;
    color: var(--text-strong);
    margin: 1.5rem 0 0.5rem;
    scroll-margin-top: 1.5rem;
}

.docs-subhead {
    font-size: var(--fs-caption);
    font-weight: 700;
    letter-spacing: var(--ls-caps);
    text-transform: uppercase;
    color: var(--text-faint);
    margin: 1.25rem 0 0.5rem;
}

.prose {
    font-size: var(--fs-base);
    line-height: 1.7;
    color: var(--text-body);
    margin-bottom: 0.75rem;
}

/* A guide feature's where / access / shortcut, composed from .stack .gap-sm;
   this only left-aligns so the badge and link keep their own width. */
.docs-feature-meta {
    align-items: flex-start;
    margin: 0.3rem 0 0.9rem;
}

/* One line of a UI string (see emphasize); the gap must read as a paragraph
   break beside the surrounding leading. */
.text-line {
    display: block;
    line-height: 1.425;
}

.text-line + .text-line {
    margin-top: 0.675em;
}

/* A run of lines a tab split in two (see emphasize): the form and what it
   means, each column starting where the one above it does. The rows sit
   closer than paragraphs do, because they read as one list. */
.text-grid {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 0.2em 0.9em;
    line-height: 1.425;
}

.text-line + .text-grid {
    margin-top: 0.675em;
}

.prose--muted {
    color: var(--text-muted);
}

.docs-endpoint {
    padding: 1.25rem;
    margin-top: 1.25rem;
    border: 1px solid var(--border-subtle);
    border-radius: 0.75rem;
    background: var(--bg-panel);
    scroll-margin-top: 1.5rem;
}

.docs-endpoint-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-bottom: 0.6rem;
}

.docs-method {
    font-size: var(--fs-caption);
    font-weight: 700;
    letter-spacing: var(--ls-caps);
    padding: 0.15rem 0.4rem;
    border-radius: 0.25rem;
    color: #fff;
    flex-shrink: 0;
}

.docs-method--get {
    background: var(--fill-info);
}

.docs-method--post {
    background: var(--fill-good);
}

.docs-method--put {
    background: var(--fill-caution);
}

.docs-method--delete {
    background: var(--fill-bad);
}

.docs-path {
    font-family: var(--font-mono);
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--text-strong);
    /* A path has no spaces; without this it runs off a narrow screen. */
    min-width: 0;
    overflow-wrap: anywhere;
}


.docs-schema {
    padding: 1.25rem;
    margin-top: 1.25rem;
    border: 1px solid var(--border-subtle);
    border-radius: 0.75rem;
    background: var(--bg-panel);
}

/* ================================================================
 * The one table (components/ui/Table.tsx)
 *
 * Wide: a plain grid. Narrow: either one card per row — the first column
 * becomes the card's heading and every other cell carries its column name,
 * stamped by the component so no cell can lose its meaning — or the same grid
 * left to scroll sideways, for logs read a column at a time.
 * ================================================================ */
/* A container, so an expanded row can size itself to what the reader can see
   rather than to how wide the grid happens to be (`100cqw` below). */
.ui-table-wrap {
    container-type: inline-size;
    overflow-x: auto;
    border: 1px solid var(--border-subtle);
    border-radius: 0.5rem;
}

.ui-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--fs-body);
}

.ui-table caption {
    padding: 0.5rem 0.75rem;
    text-align: left;
    font-size: var(--fs-caption);
    letter-spacing: var(--ls-caps);
    text-transform: uppercase;
    color: var(--text-faint);
}

.ui-table th {
    text-align: left;
    font-weight: 600;
    font-size: var(--fs-caption);
    letter-spacing: var(--ls-caps);
    text-transform: uppercase;
    color: var(--text-faint);
    padding: 0.55rem 0.75rem;
    background: var(--bg-elev);
    border-bottom: 1px solid var(--border-subtle);
    white-space: nowrap;
}

.ui-table td {
    padding: 0.55rem 0.75rem;
    border-bottom: 1px solid var(--border-muted);
    color: var(--text-body);
    line-height: 1.6;
    vertical-align: top;
}

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

/* Top is right for a table of clipped logs, where a cut cell must start where
   the eye does. A row whose name carries a second line under it (the model
   under the device) reads as one thing, so everything beside it sits on the
   row's middle instead of on the name's first line. */
.ui-table--middle td {
    vertical-align: middle;
}

.ui-table-dash {
    color: var(--text-faint);
}

.ui-table-empty {
    color: var(--text-faint);
}

/* The stand-in spans the grid, so it is never mistaken for a first column. */
.ui-table--cards td.ui-table-empty::before {
    content: none;
}

/* Logs and counts: tighter, digits lined up, values kept on one line so a
   column can be read straight down. */
.ui-table--dense {
    font-size: var(--fs-sm);
}

.ui-table--dense th {
    padding: 0.4rem 0.6rem;
}

.ui-table--dense td {
    padding: 0.45rem 0.6rem;
    vertical-align: middle;
    font-variant-numeric: tabular-nums;
}

.ui-table code {
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    color: var(--text-strong);
    background: var(--bg-elev);
    padding: 0.1rem 0.3rem;
    border-radius: 0.25rem;
    white-space: nowrap;
}

/* ---- what a column answers about width ------------------------------- */

/* One line, always: a date or an identifier broken across lines stops being
   one thing. The table scrolls rather than wrapping these. */
.ui-table th[data-width="keep"],
.ui-table td[data-width="keep"] {
    white-space: nowrap;
}

/* Capped and cut: without a cap one long value takes the width the other
   columns needed, and they wrap in its place. */
.ui-table td[data-width="clip"] {
    max-width: 22rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---- sorting and filtering, in the header ---------------------------- */
.ui-table-sort {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    color: inherit;
    cursor: pointer;
}

.ui-table-sort:hover {
    color: var(--text-strong);
}

/* Both directions offered at rest, the one in force lit. */
.sort-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 0.8rem;
    height: 0.8rem;
    flex-shrink: 0;
}

.sort-mark svg {
    width: 100%;
    height: 100%;
}

.sort-mark polygon {
    opacity: 0.28;
}

.sort-mark--asc .sort-asc,
.sort-mark--desc .sort-desc {
    opacity: 1;
}

/* Width and spacing only: the look is the shared Select's, which is what makes
   this filter the same control as every other one on the site. */
/* A header control, not a form field: it sits under a caps label and must not
   out-weigh the column it filters. Size and spacing only — the skin is the
   Select's. */
/* The heading and its filter on one line: the filter belongs to the column
   name, and under it the header grew a second row the body never matched. */
.ui-table-head {
    gap: 0.4rem;
    max-width: 100%;
    /* Every heading stands the trigger's height, whether it carries one or not:
       a table with a filter would otherwise have a taller header row than a
       table without, and the two sit on the same screens. The trigger is a
       caption line, its own padding, and the rule under it. */
    min-height: calc(var(--lh-caption) * 1em + 0.2rem + 1px);
}

/* The filter's panel hangs off the heading, which is a positioned cell in a
   scrolling box; the trigger keeps the header's own casing off itself. */
.ui-table-head .pick-panel {
    text-transform: none;
    letter-spacing: normal;
}

/* Bones stand in for a row without moving anything: the header and the column
   widths are known before the answer is. */
.ui-table td .bone-line {
    display: block;
    width: 100%;
    min-width: 3rem;
}

/* ---- a row that has more to say than fits ---------------------------- */
.ui-table-toggle-head,
.ui-table-toggle {
    width: 1.6rem;
    padding-right: 0 !important;
}

.ui-table-open {
    display: flex;
    align-items: center;
    padding: 0.1rem;
    border: 0;
    background: none;
    cursor: pointer;
}

.ui-table-open:hover .chevron {
    color: var(--text-strong);
}

/* The panel reads as part of the row above it, so it carries no top border.
   `max-width: 0` keeps the panel's own width out of the table's column maths:
   without it a long line widens the whole table and drags the header sideways
   instead of scrolling inside the panel. */
.ui-table-detail > td {
    max-width: 0;
    padding: 0;
    border-top: 0;
    background: var(--bg-elev);
}

/* Wide as the reader's view, not as the grid: a table that scrolls sideways
   would otherwise put half the detail past the right edge, so opening a row
   would cost a second scroll to read it. Sticky holds the panel in view while
   the columns scroll underneath. */
.ui-table-panel {
    position: sticky;
    left: 0;
    width: 100cqw;
    padding: 0.7rem 0.75rem;
}

.ui-table-detail pre {
    margin: 0;
    overflow-x: auto;
    padding: 0.7rem 0.8rem;
    border-radius: 0.4rem;
    background: var(--bg-panel);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    line-height: 1.6;
    color: var(--text-body);
    white-space: pre;
}

/* A drawn yes in a table cell, at the size of the text beside it. */
.mark-yes {
    display: inline-flex;
    width: 0.9rem;
    height: 0.9rem;
    color: var(--ui-good-ink);
}

.mark-yes svg {
    width: 100%;
    height: 100%;
}

/* ---- cards: a row the grid could not hold at this width ---------- */

/* The wrap stops being a box: the cards are the boxes now. */
.ui-table-wrap:has(.ui-table[data-cards]) {
    overflow-x: visible;
    border: 0;
    border-radius: 0;
}

.ui-table[data-cards] thead {
    display: none;
}

.ui-table[data-cards],
.ui-table[data-cards] tbody,
.ui-table[data-cards] tr,
.ui-table[data-cards] td,
/* Without this the caption keeps `table-caption` inside a block parent, has
   no table box to take its width from, and shrinks to one word per line. */
.ui-table[data-cards] caption {
    display: block;
    width: 100%;
}

.ui-table[data-cards] tr {
    padding: 0.7rem 0.8rem;
    border: 1px solid var(--border-subtle);
    border-radius: 0.5rem;
    background: var(--bg-panel);
}

.ui-table[data-cards] tr + tr {
    margin-top: 0.5rem;
}

.ui-table[data-cards] td {
    padding: 0;
    border-bottom: none;
    text-align: left !important;
}

/* The row's name, reading as the card's heading. */
.ui-table[data-cards] td[data-role="title"] {
    color: var(--text-strong);
    font-weight: 600;
}

/* Long identifiers wrap here rather than pushing the card sideways. */
.ui-table[data-cards] td[data-role="title"] code {
    background: none;
    padding: 0;
    white-space: normal;
    overflow-wrap: anywhere;
}

/* Label and value facing each other on one line. */
.ui-table[data-cards] td[data-role="pair"] {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 0 0.9rem;
    margin-top: 0.3rem;
}

.ui-table[data-cards] td[data-role="pair"]::before {
    content: attr(data-label);
    flex: none;
    font-size: var(--fs-sm);
    color: var(--text-faint);
}

/* A value too long for the line takes the next one, full width. */
.ui-table[data-cards] td[data-role="pair"] > * {
    min-width: 0;
    overflow-wrap: anywhere;
}

/* A sentence: under the facts, across the card, no label. */
.ui-table[data-cards] td[data-role="prose"] {
    margin-top: 0.5rem;
    padding-top: 0.5rem;
    border-top: 1px dashed var(--border-muted);
    /* Clear of the detail panel, which is pulled up under it. */
    padding-bottom: 0.5rem;
}

.ui-table[data-cards] code {
    white-space: normal;
    overflow-wrap: anywhere;
}

/* A card has the width a wide row did not, so a capped column wraps here
   rather than cutting. */
.ui-table[data-cards] td[data-width="clip"] {
    max-width: none;
    overflow: visible;
    text-overflow: clip;
    white-space: normal;
}

.ui-table[data-cards] td[data-width="keep"] {
    white-space: normal;
}

/* A card waiting for its answer: one line per cell, no labels to show yet. */
.ui-table[data-cards] tr[aria-hidden] td[data-role="pair"] {
    display: block;
}

.ui-table[data-cards] tr[aria-hidden] td[data-role="pair"]::before {
    content: none;
}

.ui-table[data-cards] td .bone-line {
    margin-top: 0.35rem;
}

/* The toggle rides at the card's top-right rather than taking a row. */
.ui-table[data-cards] .ui-table-toggle {
    float: right;
    width: auto;
    padding: 0 !important;
}

.ui-table[data-cards] tr.ui-table-detail {
    margin-top: -0.5rem;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

/* The cell is a block here, not a table cell, so the column-maths guard
   above stops being a hint and really would clamp it to nothing. */
.ui-table[data-cards] .ui-table-detail > td {
    max-width: none;
    background: none;
}

/* Nothing scrolls here — the card is the width. */
.ui-table[data-cards] .ui-table-panel {
    position: static;
    width: auto;
    padding: 0;
}

/* A cell with nothing to say is not drawn at all. Last, because it has to
   beat the role shapes above it and carries the same specificity. */
.ui-table[data-cards] td[data-empty] {
    display: none;
}

.docs-type {
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    color: var(--text-muted);
    white-space: nowrap;
}

.docs-required {
    display: inline-block;
    margin-left: 0.35rem;
    font-size: var(--fs-caption);
    font-weight: 600;
    color: var(--error-text);
}

/* A file the reader drops or picks: the deep-link manifest, a build's debug
   file. One shape, since both are a dashed box you drop a file on. */
.dropzone {
    /* A target to drop on, not a button to aim at: it takes the width it is
       given rather than the width of its label. */
    width: 100%;
    border: 1.5px dashed var(--ui-field-line);
    border-radius: 10px;
    background: transparent;
    padding: 1.2rem 1rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .2rem;
    cursor: pointer;
    font: inherit;
}

/* The action, which is what the reader is deciding. Not `--ui-ink-strong`:
   that token is a filled button's hover ground, black on a light theme and
   white on a dark one, so as a text colour it came out white on white. */
.dropzone b {
    font-size: var(--fs-base);
    line-height: var(--lh-base);
    font-weight: 600;
    color: var(--text-strong);
}

/* What to drop, under the action: the file names read as the filenames they
   are. Lives here and not with the page that drew it first, or the second
   dropzone gets the box without the writing in it. */
.dropzone span {
    font-size: var(--fs-sm);
    color: var(--ui-quiet);
    font-family: var(--font-mono);
    white-space: pre-line;
    text-align: center;
    line-height: 1.5;
}

.dropzone:hover {
    border-color: var(--ui-line-strong);
}

.dropzone.is-drag {
    border-color: var(--ui-mark);
    border-style: solid;
    background: var(--ui-hover);
}

.docs-code {
    position: relative;
    border: 1px solid var(--border-subtle);
    border-radius: 0.5rem;
}

/* Sample and result text is there to be copied, so user-select lifts here. The
   descendants are named: the reset sets the property on every element. */
.docs-code pre,
.docs-code pre *,
.try-response,
.try-response * {
    user-select: text;
    -webkit-user-select: text;
}

/* What follows a sample keeps a paragraph's air. Weightless, so an element
   with its own top margin (a heading) or the table merge below still wins. */
:where(.docs-code + *) {
    margin-top: 0.75rem;
}

/* A table and a list close a block the way a paragraph does, so what follows
   keeps the same air. Without this the gap depended on which of the two
   neighbours happened to carry a margin, and a table followed by prose had
   none on either side: the rows and the sentence under them ran together.
   Scoped to the reference pages, where the flow is markdown; the assistant's
   turn has its own complete rhythm. */
:where(.docs-main :is(.ui-table-wrap, .docs-notes) + *) {
    margin-top: 0.75rem;
}

/* The deliberate exception, paired with the radius rules below: a table and
   the sample under it are one box, so nothing may come between them. Last of
   the weightless rules on purpose — among equals, source order decides. */
:where(.docs-code + .ui-table-wrap),
:where(.docs-main .ui-table-wrap + .docs-code) {
    margin-top: 0;
}

/* The file a sample belongs in, read from the fence's title="…". */
.docs-code-file {
    font-family: var(--font-mono);
    font-size: var(--fs-caption);
    line-height: var(--lh-caption);
    color: var(--text-muted);
    padding: 0.4rem 1rem;
    border-bottom: 1px solid var(--border-subtle);
}

/* Under a file bar the code is the box's lower half: its top corners are the
   bar's, not its own, or the inherited radius cuts a notch below the bar. */
.docs-code:has(> .docs-code-file) pre {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

/* A table and a code block sitting flush merge into one box. */
.ui-table-wrap:has(+ .docs-code),
.docs-code:has(+ .ui-table-wrap) {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

.ui-table-wrap + .docs-code,
.docs-code + .ui-table-wrap {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
    border-top: none;
}

.docs-code pre {
    /* Up under the copy strip and its stopping margin, so the block is the
       height of the code alone. */
    margin: -3.05rem 0 0;
    /* Even on both sides: the copy button floats over the corner, and reserving
       a lane for it only skewed every block. */
    padding: 0.9rem 1rem;
    overflow-x: auto;
    /* Rounds its own corners: `overflow` on the block above would become the
       copy button's scrollport and pin it to the block. */
    border-radius: inherit;
    background: var(--gray-100) !important;
    font-size: var(--fs-sm);
    line-height: 1.65;
}

.docs-code code {
    background: transparent !important;
    font-family: var(--font-mono);
    user-select: text;
}

/* Sticky copy strip: it carries the button's real height and the code is pulled
   up over it; a negative margin here would bound the sticky travel and collapse it. */
.docs-code-bar {
    position: sticky;
    top: 3.7rem;
    z-index: 1;
    display: flex;
    justify-content: flex-end;
    height: 2.15rem;
    margin-bottom: 0.9rem;
    padding: 0.4rem 0.4rem 0 0;
    /* The strip lies over the code's first line; only the button takes pointer
       events, so touch scrolling of the sample still works. */
    pointer-events: none;
}

/* No page bar over the assistant's answers: the panel's log is the scrollport. */
.chat-log .docs-code-bar {
    top: -1rem;
}

.docs-code-copy {
    /* The one thing in the strip that is here to be pressed. */
    pointer-events: auto;
    background: var(--bg-panel);
}

.docs-notes {
    list-style: none;
    margin-top: 1rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.docs-notes li {
    position: relative;
    padding-left: 0.9rem;
    font-size: var(--fs-body);
    line-height: 1.7;
    color: var(--text-muted);
}

.docs-notes li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.6rem;
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: var(--accent);
}@media (max-width: 900px) {

    .docs-body {
        grid-template-columns: minmax(0, 1fr);
    }

    .docs-toc {
        position: static;
        max-height: none;
        border-bottom: 1px solid var(--border-subtle);
        padding-bottom: 1rem;
    }
}


/* One tab per language: more than a strip's width, so they wrap. */
.docs-code-tabs button {
    padding: 0.3rem 0.7rem;
    font-size: var(--fs-caption);
    white-space: nowrap;
}

.docs-code-tabs {
    flex-wrap: wrap;
    width: fit-content;
    max-width: 100%;
    margin-bottom: 0.5rem;
}

.docs-extends {
    /* Moves to the next line whole; the type name beside it is what breaks. */
    white-space: nowrap;
    margin-left: 0.5rem;
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--text-faint);
}

/* Schema names are identifiers, so they keep the mono family; everything else
   about the row is the contents list's one voice. */
.docs-toc-schema {
    font-family: var(--font-mono);
}

.docs-schema-head {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    width: 100%;
    padding: 0;
    margin: 0 0 0.4rem -0.25rem;
    border: none;
    background: none;
    cursor: pointer;
    text-align: left;
}


/* A type name is one long unbroken word: the name breaks rather than pushing
   the page sideways on a phone; the chevron stays beside it. */
.docs-schema-head > * {
    min-width: 0;
    overflow-wrap: anywhere;
}

.docs-schema-head:hover .ty code {
    color: var(--accent);
}

.chevron {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1rem;
    height: 1rem;
    flex-shrink: 0;
    color: var(--text-faint);
    transition: transform 0.15s ease, color 0.15s;
}

.chevron svg {
    width: 0.875rem;
    height: 0.875rem;
}

.chevron.open {
    transform: rotate(90deg);
    color: var(--accent);
}

.docs-schema-summary {
    margin-bottom: 0;
}

.docs-schema-body[hidden] {
    display: none;
}

.docs-schema-body {
    margin-top: 0.9rem;
}

.docs-inline-link {
    display: inline-flex;
    align-items: center;
    gap: 0.2rem;
    color: var(--accent);
    text-decoration: none;
    vertical-align: baseline;
}

.docs-inline-link code {
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    background: var(--accent-light);
    padding: 0.1rem 0.3rem;
    border-radius: 0.25rem;
    text-decoration: underline;
    text-underline-offset: 2px;
}

.docs-inline-link svg {
    width: 0.7rem;
    height: 0.7rem;
    flex-shrink: 0;
}

.docs-inline-link:hover code {
    background: var(--accent);
    color: #fff;
}



.docs-toc-row a {
    flex: 1;
    min-width: 0;
}

.docs-toc-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    padding: 0;
    border: none;
    border-radius: 0.375rem;
    background: none;
    cursor: pointer;
    flex-shrink: 0;
}

.docs-toc-toggle:hover {
    background: var(--accent-light);
}

/* ================================================================
 * API docs — try it
 * ================================================================ */

.try-it {
    border: 1px solid var(--border-subtle);
    border-radius: 0.5rem;
    padding: 0.9rem;
    background: var(--bg-elev);
    /* This ground is the well colour in dark, so fields step up instead. */
    --ui-input: var(--bg-panel);
}

.try-it-fields {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
    gap: 0.6rem;
}

.try-field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    min-width: 0;
}

.try-field--wide {
    margin-top: 0.6rem;
}

.try-field-label {
    font-family: var(--font-mono);
    font-size: var(--fs-caption);
    color: var(--text-muted);
}

/* Worn by the tester's Select triggers and body textarea; its plain inputs use
   `.input.mono`, which this matches. */


.try-textarea {
    resize: vertical;
    line-height: 1.5;
}



.try-file-btn {
    padding: 0.4rem 0.75rem;
    border: 1px solid var(--border-subtle);
    border-radius: 0.375rem;
    background: var(--bg-panel);
    color: var(--text-strong);
    font-family: inherit;
    font-size: var(--fs-body);
    cursor: pointer;
    transition: border-color 0.15s;
    white-space: nowrap;
}

.try-file-btn:hover {
    border-color: var(--accent);
}

.try-file-name {
    color: var(--text-muted, var(--text-strong));
    font-size: var(--fs-body);
    font-family: var(--font-mono);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.try-it-bar {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.75rem;
}

.try-url {
    flex: 1;
    min-width: 0;
    padding: 0.4rem 0.55rem;
    border: 1px solid var(--border-muted);
    border-radius: 0.375rem;
    background: var(--bg-panel);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-caption);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    user-select: text;
}

.try-send {
    padding: 0.45rem 1.1rem;
    border: none;
    border-radius: 0.375rem;
    background: var(--accent);
    color: #fff;
    font-size: var(--fs-sm);
    font-weight: 600;
    cursor: pointer;
    flex-shrink: 0;
    transition: opacity 0.15s;
}

.try-send:hover {
    opacity: 0.88;
}

.try-send:disabled {
    opacity: 0.55;
    cursor: default;
}

.try-hint {
    margin-top: 0.5rem;
    font-size: var(--fs-caption);
    color: var(--text-faint);
    line-height: 1.6;
}

.try-result {
    margin-top: 0.75rem;
}

.try-result-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.4rem;
}

/* The status reads from the left, the view switch sits at the far right. */
.try-result-head .seg {
    margin-left: auto;
}

.try-raw {
    margin: 0;
    padding: 0.75rem;
    white-space: pre-wrap;
    word-break: break-all;
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    line-height: 1.55;
    color: var(--text-body);
}

.try-status {
    font-family: var(--font-mono);
    font-size: var(--fs-caption);
    font-weight: 700;
    padding: 0.15rem 0.45rem;
    border-radius: 0.25rem;
    background: var(--fill-good);
    color: #fff;
}

/* A fill behind white text, like the success state beside it — not
   --error-text, which is a text colour and turns pale on dark. */
.try-status.failed {
    background: var(--fill-bad);
}

.try-elapsed {
    font-size: var(--fs-caption);
    color: var(--text-muted);
}

.try-response {
    max-height: 22rem;
    overflow: auto;
    border: 1px solid var(--border-subtle);
    border-radius: 0.375rem;
    background: var(--bg-panel);
}

/* ================================================================
 * JSON tree viewer
 * ================================================================ */

.json-view {
    padding: 0.7rem 1.25rem;
    font-family: var(--font-mono);
    font-size: var(--fs-caption);
    line-height: 1.75;
    user-select: text;
}

.json-view--raw {
    margin: 0;
    white-space: pre-wrap;
    word-break: break-all;
    color: var(--text-body);
}

.json-row {
    white-space: nowrap;
}

.json-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.1rem;
    padding: 0;
    border: none;
    background: none;
    font: inherit;
    cursor: pointer;
    color: inherit;
}

.json-chevron {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 0.7rem;
    height: 0.7rem;
    margin-left: -0.75rem;
    color: var(--text-faint);
    transition: transform 0.12s ease;
}

.json-chevron svg {
    width: 0.6rem;
    height: 0.6rem;
}

.json-chevron.open {
    transform: rotate(90deg);
}

.json-toggle:hover .json-chevron {
    color: var(--accent);
}

.json-key {
    color: var(--text-strong);
    font-weight: 600;
}

.json-colon,
.json-comma,
.json-brace {
    color: var(--text-faint);
}

.json-count {
    margin: 0 0.25rem;
    padding: 0 0.3rem;
    border-radius: 0.2rem;
    background: var(--bg-elev-hover);
    color: var(--text-muted);
}

.json-string {
    color: #059669;
    word-break: break-all;
}

.json-number {
    color: #2563eb;
}

.json-boolean {
    color: #9333ea;
}

.json-null {
    color: var(--text-faint);
}

.json-clip {
    cursor: pointer;
    text-decoration: underline dotted;
    text-underline-offset: 2px;
}

/* An expanded value wraps instead of stretching the pane sideways. */
.json-clip.expanded {
    white-space: pre-wrap;
}

:root[data-theme="dark"] .json-string {
    color: #34d399;
}

:root[data-theme="dark"] .json-number {
    color: #60a5fa;
}

:root[data-theme="dark"] .json-boolean {
    color: #c084fc;
}

.embed-param-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
    gap: 0.6rem;
}

.embed-param {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    min-width: 0;
}

.embed-param-label {
    font-family: var(--font-mono);
    font-size: var(--fs-caption);
    color: var(--text-muted);
}

.embed-param .input,
.embed-param .modal-select {
    padding: 0.4rem 0.55rem;
    font-size: var(--fs-body);
    font-family: var(--font-mono);
}

.embed-param .input {
    user-select: text;
}

/* ================================================================
 * Landing
 *
 * Left-aligned throughout with a persistent index rail: the three stores are
 * the product's structure, not decoration, so they stay on screen as a list.
 * ================================================================ */

/* The surfaces built on the landing's design language route every colour through
   these. The landing itself keeps its fixed palette and is not in the set. */
.surface {
    --ui-ground: #0a0b0d;
    --ui-ink: #edf0f1;
    --ui-dim: #8c959c;
    /* Between the two. `--ui-dim` is pitched at labels and captions; a page read
       in long runs needs more contrast than that without going to full ink. */
    --ui-soft: #b3bcc2;
    --ui-line: #1e2226;
    --ui-field-line: #30373d;

    /* The surfaces those screens are built from. */
    --ui-panel: #101315;
    /* One step toward light, the same direction the dashboard's dark well
       takes (and Material's filled container). */
    --ui-input: #1a1f24;
    --ui-hover: #141719;
    --ui-sunken: #22272b;
    --ui-rule: #171b1e;
    --ui-bone: #1a1e22;
    --ui-quiet: #7f8993;
    --ui-line-strong: #333a41;


    background: var(--ui-ground);
    color: var(--ui-ink);
}

/* Korean breaks on syllables by default. Scoped to Korean: `keep-all` also
   forbids breaks between Han characters and would wreck Chinese paragraphs. */
.surface:lang(ko) {
    word-break: keep-all;
}

/* Set on the root by every `.surface` screen: the page scrollbar is painted
   above `.surface`. No colour-scheme here: the landing pins its own via data-theme. */
.surface-root {
    --scrollbar-thumb: #3a4050;
}

:root[data-theme="light"].surface-root {
    --scrollbar-thumb: var(--gray-300);
}

/* Context DEFAULTS, not skins: wrapped in :where() so any component class beats
   them. A scoped bare-element rule otherwise outguns single-class skins. */
:where(.surface a) {
    color: inherit;
    text-decoration: none;
}

/* Unscoped: inline code reads the same wherever it sits, and the docs pages are
   not on .surface. The --ui-* tokens resolve on either ground. */
:where(code) {
    font-family: var(--font-mono);
    font-size: .92em;
    color: var(--ui-ink);
}

/* ---------------- nav ---------------- */

/* Fixed, not sticky: in the flow it pushes the first sticky stage down, which
   then snaps back up on the first pixel of scroll. */
.site-nav {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    min-height: 3.6rem;
    padding: .75rem clamp(1rem, 4vw, 2.5rem);
    background: rgb(10 11 13 / .76);
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
    border-bottom: 1px solid var(--ui-line);
    /* Dark by default — it sits over the landing's artwork; the light-themed
       screens restate sheen and lift against their own ground. */
    box-shadow: 0 6px 24px -14px rgb(0 0 0 / .5), inset 0 1px 0 rgb(255 255 255 / .07);

    /* The bar's ground is fixed dark, so its text is fixed light. `color` must
       be restated: `.surface` resolved it, and the finished colour descends. */
    --ui-ground: #0a0b0d;
    --ui-ink: #edf0f1;
    --ui-dim: #8c959c;
    --ui-soft: #b3bcc2;
    --ui-line: #1e2226;
    --ui-field-line: #30373d;
    color: var(--ui-ink);
}

.site-brand {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    font-family: var(--font-mono);
    font-size: var(--fs-body);
    font-weight: 600;
    letter-spacing: -.02em;
}

.site-brand i, .dash-brand i {
    color: var(--brand-accent);
    font-style: normal;
}

/* Ink, not the accent: the mark is the wordmark's first letter in weight, and a
   tinted one would read as a separate object sitting beside the name. */
.site-brand .site-mark {
    flex: none;
    width: 1.2rem;
    height: 1.2rem;
}

.site-mark svg {
    display: block;
    width: 100%;
    height: 100%;
}

.site-nav-links {
    display: flex;
    align-items: center;
    gap: clamp(.9rem, 2vw, 1.7rem);
    font-size: var(--fs-body);
    color: var(--ui-dim);
}

/* The bar carries its own look rather than borrowing the page's: it renders on
   the docs too, where an unstyled anchor arrives blue and underlined. */
/* Everything but the filled button: weightless (`:where`) defaults, so any
   skinned link keeps its own ink without per-skin exclusions. */
:where(.site-nav a) {
    text-decoration: none;
}

:where(.site-nav a, .site-nav a:visited) {
    color: inherit;
}

:where(.site-nav-links a:hover) {
    color: var(--ui-ink);
}

.ui-btn {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    font-size: var(--fs-body);
    font-weight: 600;
    white-space: nowrap;
    transition: opacity .15s, color .15s;
}

/* Ink on ground rather than a tinted fill: a grey button reads as disabled.
   Two classes so `.surface a { color: inherit }` cannot win. */
.ui-btn--solid,
.surface .ui-btn--solid {
    text-decoration: none;
    background: var(--ui-ink, var(--text-strong));
    color: var(--ui-ground, var(--bg-panel));
    padding: .55rem 1.1rem;
    border-radius: 4px;
}

.ui-btn--solid:hover {
    opacity: .88;
}

.ui-btn--plain {
    color: var(--ui-dim);
}

.ui-btn--plain:hover {
    color: var(--ui-ink);
}

/* ---------------- shared ---------------- */

.lp-stage {
    position: sticky;
    top: 0;
    height: 100vh;
    display: grid;
    place-items: center;
    overflow: hidden;
}

.lp-h1 {
    font-size: clamp(1.9rem, 4.4vw, 3rem);
    font-weight: 700;
    letter-spacing: -.035em;
    line-height: 1.14;
    margin: 0 0 .9rem;
}

.lp-h2 {
    font-size: clamp(1.5rem, 3.2vw, 2.2rem);
    font-weight: 700;
    letter-spacing: -.03em;
    line-height: 1.18;
    margin: 0 0 .7rem;
}

.lp-sub {
    color: var(--ui-dim);
    font-size: clamp(.86rem, 1.4vw, .96rem);
    line-height: 1.7;
    margin: 0;
    max-width: 34ch;
}

.lp-kicker {
    display: inline-block;
    font-family: var(--font-mono);
    font-size: var(--fs-caption);
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--ui-dim);
    margin-bottom: .8rem;
}

.lp-cta {
    display: flex;
    align-items: center;
    gap: 1.2rem;
    margin-top: 1.6rem;
}

/* The embed is shown at reduced scale so a whole store page fits at once. */
.lp-viewport {
    position: relative;
    width: 100%;
    height: min(30rem, 62vh);
    overflow: hidden;
    border: 1px solid var(--ui-line);
    border-radius: 6px;
    background: #fff;
}

.lp-viewport--tall {
    height: min(34rem, 70vh);
}

/* Shown at true size, for embeds whose content is already legible. */
.lp-viewport--flat .lp-frame {
    width: 100%;
    height: 100%;
    transform: none;
}

/* Height comes from the embed itself at runtime; the value here only covers the
   moment before the measurement lands. */
.lp-viewport--fit {
    height: min(20rem, 42vh);
    transition: height .45s cubic-bezier(.22, 1, .36, 1);
}

/* Dissolves the frame into the section behind it, so only the cards inside
   appear to arrive. Pairs with `appearance=dark` and the transparent document. */
.lp-viewport--blend,
.lp-viewport--blend .lp-frame {
    background: var(--ui-ground);
}

.lp-viewport--blend {
    border: 0;
    border-radius: 0;
}

/* Dissolves the frame's top and bottom edges. `--fade` is set by the host, the
   only side that knows the embed's resolved theme; the background follows it. */
.lp-viewport--fade {
    background: var(--fade);
}

.lp-viewport--fade::before,
.lp-viewport--fade::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    /* The iframe is positioned too, so only an explicit index lifts these above
       it — tree order alone would leave `::before` underneath. */
    z-index: 2;
    pointer-events: none;
}

/* Shallower at the top: a too-tall section is top-aligned and a deep fade would
   wash out its heading; content arrives from below. */
.lp-viewport--fade::before {
    top: 0;
    height: 2.25rem;
    background: linear-gradient(var(--fade), transparent);
}

.lp-viewport--fade::after {
    bottom: 0;
    height: 5rem;
    background: linear-gradient(transparent, var(--fade));
}

.lp-frame {
    position: absolute;
    top: 0;
    left: 0;
    width: 117.7%;
    height: 117.7%;
    border: 0;
    transform: scale(.85);
    transform-origin: 0 0;
    opacity: 0;
    transition: opacity .4s ease;
    pointer-events: none;
    /* Same ground the embed paints, so an overscroll bounce reveals nothing. */
    background: var(--bg-body);
}

.lp-frame.on {
    opacity: 1;
    pointer-events: auto;
}

/* ---------------- hero ---------------- */

.lp-hero {
    position: relative;
    height: 800vh;
}

/* All three stages take the same token, so during the handover from one to the
   next there is no colour difference to see. */
.lp-hero .lp-stage,
.lp-assembly .lp-stage,
.lp-depth .lp-stage {
    background: var(--ui-ground);
}

/* The panel throws the colour of the store it shows: cast upward so it reads as
   light off the panel, eased to cross-fade with the frames it belongs to. */
.lp-hero .lp-viewport {
    box-shadow: 0 -10px 60px -12px var(--glow);
    transition: box-shadow .5s ease;
}

.lp-hero-grid {
    width: min(72rem, 92vw);
    display: grid;
    grid-template-columns: 9.5rem minmax(0, 1fr) minmax(0, 1.15fr);
    gap: clamp(1.5rem, 3.5vw, 3rem);
    align-items: center;
}

.lp-index {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 1.1rem;
    align-self: start;
    padding-top: .5rem;
}

.lp-index li {
    display: grid;
    gap: .4rem;
    opacity: .4;
    transition: opacity .35s ease;
}

.lp-index li.on {
    opacity: 1;
}

.lp-index-bar {
    display: block;
    height: 2px;
    background: var(--ui-line);
    overflow: hidden;
}

.lp-index-bar i {
    display: block;
    height: 100%;
    background: var(--c);
    transform-origin: 0 50%;
    transition: transform .12s linear;
}

.lp-index-name {
    display: flex;
    align-items: center;
    gap: .38rem;
    font-family: var(--font-mono);
    font-size: var(--fs-caption);
    letter-spacing: var(--ls-caps);
    text-transform: uppercase;
    color: var(--ui-dim);
}

.lp-index li.on .lp-index-name {
    color: var(--ui-ink);
}

.lp-index-icon {
    flex: none;
    width: .85rem;
    height: .85rem;
}

.lp-index-icon svg {
    display: block;
}

/* Store logos ship brand colours that fight the theme colour under the bar:
   flattened to the label's ink; the knock-out keeps light-on-dark glyphs legible. */
.lp-index-icon svg * {
    fill: currentColor;
}

.lp-index-icon svg [fill="#fff"], .lp-index-icon svg [fill="#ffffff"] {
    fill: var(--ui-ground);
}

/* ---------------- assembly ---------------- */

.lp-assembly {
    position: relative;
    height: 1100vh;
}

/* No colours of its own: the ground is the page's own tokens, carried here by
   `useGroundBlend`; text, code and rules already read the tokens. */

.lp-assembly-grid {
    width: min(74rem, 92vw);
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
    gap: clamp(1.5rem, 4vw, 3.5rem);
    align-items: center;
}

.lp-snippet {
    position: relative;
    margin-top: 1.6rem;
    padding: .9rem 1rem;
    background: #0d1117;
    border-radius: 5px;
    font-family: var(--font-mono);
    font-size: clamp(.66rem, 1vw, .76rem);
    line-height: 1.9;
    color: #c9d3de;
    overflow-x: auto;
    white-space: nowrap;
}

.c-att {
    color: #c4b5fd;
}

.c-str {
    color: #86efac;
}

/* Zero width until revealed, so the closing quote is pushed along rather
   than jumping to a new position. */
.lp-token {
    display: inline-block;
    max-width: 0;
    overflow: hidden;
    vertical-align: bottom;
    color: #86efac;
    transition: max-width .5s cubic-bezier(.22, 1, .36, 1);
}

.lp-token.on {
    max-width: 12ch;
}

.lp-token-inner {
    display: inline-block;
    white-space: pre;
}

.lp-copy {
    position: absolute;
    top: .55rem;
    right: .55rem;
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    font-family: inherit;
    font-size: var(--fs-caption);
    font-weight: 600;
    color: #9fb3c8;
    background: rgb(255 255 255 / .07);
    border: 1px solid rgb(255 255 255 / .12);
    border-radius: 4px;
    padding: .25rem .55rem;
    cursor: pointer;
}

.lp-copy:hover {
    color: #fff;
}

.lp-copy svg {
    width: .75rem;
    height: .75rem;
}

.lp-legend {
    list-style: none;
    margin: 1.4rem 0 0;
    padding: 0;
    display: grid;
    gap: 0;
}

.lp-legend li {
    display: grid;
    grid-template-columns: 6.5rem minmax(0, 1fr);
    gap: 1rem;
    padding: .5rem 0;
    border-top: 1px solid #e3e7e4;
    font-size: var(--fs-body);
    color: #98a29c;
    opacity: .45;
    transition: opacity .35s ease, color .35s ease;
}

.lp-legend li.on {
    opacity: 1;
    color: #5b6560;
}

.lp-legend b {
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-weight: 500;
    color: #121512;
}

/* ---------------- depth ---------------- */

/* Five cards to reveal, so it needs the same runway as the assembly. */
.lp-depth {
    position: relative;
    height: 1100vh;
}

.lp-depth-grid {
    width: min(72rem, 92vw);
    display: grid;
    grid-template-columns: minmax(0, .95fr) minmax(0, 1.15fr);
    gap: clamp(1.5rem, 4vw, 3.5rem);
    align-items: center;
}

.lp-steps {
    list-style: none;
    margin: 1.6rem 0 0;
    padding: 0;
    display: grid;
    gap: 0;
    counter-reset: step;
}

.lp-steps li {
    display: grid;
    grid-template-columns: 7rem minmax(0, 1fr);
    gap: 1rem;
    padding: .6rem 0;
    border-top: 1px solid var(--ui-line);
    font-size: var(--fs-body);
    color: var(--ui-dim);
    opacity: .4;
    transition: opacity .35s ease;
}

.lp-steps li.on {
    opacity: 1;
}

.lp-steps b {
    font-weight: 600;
    color: var(--ui-ink);
}

/* ---------------- plans ---------------- */

.lp-plans {
    background: var(--ui-ground);
    padding: clamp(4rem, 11vh, 6.5rem) clamp(1rem, 4vw, 2.5rem);
    border-top: 1px solid var(--ui-line);
}

.lp-plans-inner {
    max-width: 60rem;
    margin: 0 auto;
}

/* The nav floats over the page, so the anchor has to clear it. */
.lp-plans {
    scroll-margin-top: 3.5rem;
}

.lp-plan-table {
    margin-top: 2.4rem;
}

/* The plan's name over its price, stacked and centred in the column header. */
.lp-plan-col {
    display: inline-grid;
    gap: .35rem;
    justify-items: center;
    padding: .4rem 0;
}

.lp-plan-name {
    font-family: var(--font-mono);
    font-size: var(--fs-caption);
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--ui-dim);
}

/* The header carries the price, so it must read strong, not as the faint
   caption a data-grid header defaults to. */
.lp-plan-price {
    font-size: var(--fs-title);
    font-weight: 700;
    letter-spacing: -.03em;
    text-transform: none;
    font-variant-numeric: tabular-nums;
    color: var(--text-strong);
}

.lp-plan-price em {
    font-size: var(--fs-sm);
    font-weight: 500;
    font-style: normal;
    color: var(--ui-dim);
    letter-spacing: 0;
}

/* Values sit centred against the metric that leads each row. */
.plan-matrix td {
    vertical-align: middle;
}

/* ---------------- footer ---------------- */

.lp-foot {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 1.8rem clamp(1rem, 4vw, 2.5rem) 3rem;
    border-top: 1px solid var(--ui-line);
    font-size: var(--fs-sm);
    color: var(--ui-quiet);
    background: var(--ui-ground);
}

:where(.lp-foot a:hover) {
    color: var(--ui-ink);
}

/* It wraps: three links and the language switch do not fit one line on a phone,
   and overflow took the page's horizontal scrollbar with it. */
.lp-foot-end { gap: .7rem 1.2rem; }/* Below the same width the other footer stacks at, so both read the same. */
@media (max-width: 36rem) {

    .lp-foot {
        flex-direction: column;
        align-items: center;
        text-align: center;
    }

    .lp-foot-end {
        justify-content: center;
    }
}

/* The switch is shared with the docs, whose header is light; this ground is not. */

/* ================= auth ================= */

.auth .lp-foot {
    justify-content: space-between;
}

/* ================= account ================= */

.acct-main {
    flex: 1;
    width: 100%;
    max-width: 42rem;
    margin: 0 auto;
    padding: 0 clamp(1rem, 4vw, 2.5rem) 3rem;
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.acct-sticky {
    margin: calc(3.6rem + 1px) auto 1.2rem;
    padding: calc(2.4rem - 1px) clamp(1rem, 4vw, 2.5rem) 0;
    max-width: 42rem;
    width: 100%;
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
    position: sticky;
    top: calc(3.6rem + 1px);
    /* Above the cards that scroll beneath it — anything positioned in a row
       (drag grips, focus tints) must pass under the header, never over. */
    z-index: 2;
}

/* Same header, kept in flow: pages whose title has no reason to follow the
   scroll (the deep-links list) drop the pin and the glass with it. */
.acct-sticky--flow {
    position: static;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

.acct-title {
    margin: 0 0 .6rem;
    font-size: clamp(1.5rem, 3.4vw, 1.85rem);
    font-weight: 600;
    letter-spacing: -.035em;
}

/* Account tabs: the same underline shape as the dashboard's view tabs — the
   site has one idea of a tab. Scrolls sideways in its own track on a phone. */
.acct-tabs {
    gap: .3rem;
    border-bottom: 1px solid var(--ui-line);
}

.acct-tab {
    flex: none;
    padding: .5rem .7rem .6rem;
    border: 0;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    background: none;
    color: var(--ui-dim);
    font-family: inherit;
    font-size: var(--fs-base);
    white-space: nowrap;
    cursor: pointer;
    transition: color .15s, border-color .15s;
}

.acct-tab:hover {
    color: var(--ui-soft);
}

.acct-tab.on {
    color: var(--ui-ink);
    font-weight: 600;
    border-bottom-color: var(--ui-ink);
    border-bottom-width: 3px;
}

.acct-card {
    padding: 1.15rem 1.25rem;
    border: 1px solid var(--ui-line);
    border-radius: 7px;
    background: var(--ui-panel);
    display: flex;
    flex-direction: column;
    gap: .7rem;
}




.acct-label {
    flex: none;
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--ui-dim);
}

.acct-value {
    flex: 1;
    display: flex;
    align-items: center;
    gap: .5rem;
    font-size: var(--fs-base);
    min-width: 0;
}

.quiet {
    white-space: pre-line;
    margin: 0;
    font-size: var(--fs-sm);
    line-height: 1.6;
    color: var(--ui-quiet);
}

.acct-bad {
    color: var(--ui-bad-ink);
}

.acct-spacer {
    flex: 1;
}

.acct-plan {
    flex: 1;
    font-size: var(--fs-heading);
    font-weight: 600;
    letter-spacing: -.02em;
}

.acct-pill {
    flex: none;
    padding: .12rem .45rem;
    border: 1px solid var(--ui-line);
    border-radius: 999px;
    font-size: var(--fs-caption);
    /* Dim, not quiet: at this size the fainter tone fell to 3:1 on the light
       ground, and the pill is a label rather than an aside. */
    color: var(--ui-dim);
}

.acct-pill.on {
    border-color: var(--ui-good-line);
    color: var(--ui-good-ink);
}

.note-box {
    white-space: pre-line;
    margin: 0;
    padding: .6rem .75rem;
    border: 1px solid var(--ui-note-line);
    border-left-width: 2px;
    border-radius: 4px;
    background: var(--ui-note-ground);
    font-size: var(--fs-sm);
    line-height: 1.55;
    color: var(--ui-note-ink);
}

.acct-form {
    display: grid;
    gap: .8rem;
    padding-top: .3rem;
}

.acct-field {
    display: grid;
    gap: .35rem;
}



/* A button rendered as an anchor keeps the browser underline unless the skin
   clears it: the look belongs to the control, not to the surface it sits on. */
.acct-btn,
.fetch-button,
.action-button,
.ui-btn,
.auth-provider,
.auth-submit,
.pg-btn,
.icon-btn,
.expand-button,
.try-file-btn,
.embed-back-btn,
.admin-link {
    text-decoration: none;
}

.acct-btn {
    flex: none;
    display: inline-flex;
    align-items: center;
    padding: .42rem .8rem;
    border: 1px solid var(--ui-line);
    border-radius: 5px;
    background: var(--ui-panel);
    color: var(--ui-ink);
    font-family: inherit;
    font-size: var(--fs-sm);
    font-weight: 500;
    cursor: pointer;
    transition: border-color .15s, background .15s, opacity .15s;
}

/* The dark wash belongs to the outlined buttons only. Letting it reach the
   filled one painted a dark ground under text that is dark by design. */
.acct-btn:hover:not(.acct-btn--solid) {
    border-color: var(--ui-line-strong);
    background: var(--ui-hover);
}

/* The card is a column, so a button on its own would stretch across it. */
.acct-btn--own {
    align-self: flex-start;
}

/* Same mark as the row it would add, so the two read as the same thing. */
/* A mark beside the label. A direct child only: the provider variant keeps its
   own inside `.list-row-icon`, which is sized on its own terms. */
.acct-btn:has(> svg) {
    gap: .35rem;
}

.acct-btn > svg {
    width: .8rem;
    height: .8rem;
    flex: none;
}

.acct-btn--mark {
    gap: .45rem;
}

.acct-btn--mark .list-row-icon {
    width: .95rem;
    height: .95rem;
}

.acct-btn:disabled {
    color: var(--ui-quiet);
    cursor: default;
}

/* Not scoped to `.acct`: the same dialog opens from the dashboard, where the
   fill would not apply. */
.acct-btn--solid {
    background: var(--ui-ink);
    border-color: var(--ui-ink);
    color: var(--ui-ground);
}

/* Brighter, not dimmer: on a dark page a filled button that fades reads as
   going away, and opacity would take the spinner down with it. */
.acct-btn--solid:hover {
    background: var(--ui-ink-strong);
    border-color: var(--ui-ink-strong);
}

/* Sunken and quiet; the ink must be restated or the equal-weight fill above
   keeps painting the live button's colour on a dead one. */
.acct-btn--solid:disabled {
    background: var(--ui-sunken);
    border-color: var(--ui-sunken);
    color: var(--ui-quiet);
}

.acct-btn--bad {
    border-color: var(--ui-bad-line);
    color: var(--ui-bad-ink);
}

.acct-btn--bad:hover {
    border-color: var(--ui-bad-line-strong);
    background: var(--ui-bad-ground);
}

/* Matches the nav links beside it, which are anchors. */
.acct-linkish {
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
}

.acct-linkish:hover {
    color: var(--ui-ink);
}

.list-rows {
    list-style: none;
    margin: -.7rem 0 0 0;
    padding: 0;
    display: grid;
    gap: .1rem;
}

/* The focused row is the one Alt + arrows will move. */
.list-row:focus {
    outline: none;
    border-radius: 7px;
    background: color-mix(in srgb, var(--ui-mark) 8%, transparent);
}

/* A highlighted row reads as one rounded block: its own hairline and the one
   the row above it wears would otherwise float 1px off the tint. */
.list-row:focus,
.list-row--carried,
.list-row:has(+ .list-row:focus),
.list-row:has(+ .list-row--carried),
.list-row:has(+ .list-row--over) {
    border-bottom-color: transparent;
}

.list-row--carried {
    opacity: .45;
    background: color-mix(in srgb, var(--ui-mark) 8%, transparent);
    border-radius: 7px;
}

.list-row--over {
    box-shadow: inset 0 2px 0 var(--ui-mark);
}

/* The slot after the last row. */
.list-row--over-end {
    box-shadow: inset 0 -2px 0 var(--ui-mark);
}

.list-row {
    display: flex;
    align-items: center;
    gap: .7rem;
    padding: .55rem;
    margin: 0 -.55rem;
    border-bottom: 1px solid var(--ui-rule);
    /* The list is a grid, whose items refuse to go below their content's own
       width: without this a row that cannot shrink widens the page instead of
       letting its own parts give way. */
    min-width: 0;
}

.list-row:last-child {
    border-bottom: 0;
}

/* A row's expansion: its own row beneath the one it belongs to, so it gets
   the list's whole width. The row's body is a flex child sharing the line
   with the buttons, and anything wide put there is squeezed. The two carry
   one hairline between them, as the highlighted rows above do.

   An open row also stays in view while that detail is scrolled, so what the
   reader is looking at is always named and the controls stay in reach, the
   way a sample's copy strip does. */
.list-row:has(+ .list-row-detail) {
    position: sticky;
    top: calc(3.6rem + 1px);
    z-index: 2;
    background: var(--ui-ground);
    border-bottom-color: transparent;
}

/* A row's name is a name, not the thing being read: a five-line exception
   turns the list into a wall, and pinned it would hold the top of the screen
   the whole way down. The full text is in the detail, and on the title. */
.list-row-main b {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* The facts under a row's name read as one block, off the name: at the list's
   own .15rem they lined up with it and the three lines became one paragraph. */
.issue-meta {
    margin-top: 0.3rem;
}

/* Where it broke: a line of its own, cut rather than wrapped, so the two lines
   under it always start at the same place.

   The `min-width: 0` is what makes that safe: these sit in a grid, whose items
   refuse to go below their content's own width by default, so a line that will
   not wrap pushes the whole row wider than the list instead. */
.issue-meta {
    min-width: 0;
}

.issue-where {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A share of a whole, beside the figure it belongs to: the column says 15,
   the bar says how much of the issue that is. */
.share-bar {
    display: inline-block;
    vertical-align: middle;
    width: 3.25rem;
    height: 5px;
    margin-right: 0.45rem;
    border-radius: 3px;
    background: var(--ui-rule);
    overflow: hidden;
}

.share-bar > span {
    display: block;
    height: 100%;
    background: var(--ui-mark);
}

.list-row-detail {
    padding: 0 0 .8rem;
    border-bottom: 1px solid var(--ui-rule);
    /* Same reason as the row above: the table inside must be free to hand its
       overflow to its own scrollport rather than push the page wide. */
    min-width: 0;
}

.list-row-detail:last-child {
    border-bottom: 0;
}

/* Something here has not been read. The dot carries it; the heavier title is
   what makes the row findable while scrolling past. */
.list-row--unread .list-row-main b {
    font-weight: 600;
}

.unread-dot {
    display: inline-block;
    width: .4rem;
    height: .4rem;
    margin-right: .4rem;
    border-radius: 50%;
    background: var(--fill-info);
    vertical-align: middle;
}

.list-row-icon {
    flex: none;
    width: 1.1rem;
    height: 1.1rem;
    color: var(--ui-dim);
}

.list-row-icon svg {
    display: block;
    width: 100%;
    height: 100%;
}

.list-row-main {
    flex: 1;
    display: grid;
    gap: .15rem;
    min-width: 0;
    font-size: var(--fs-body);
}

.list-row-main b {
    font-weight: 500;
    overflow-wrap: anywhere;
}

/* A row's drag handle; the only part of it that starts a drag, so the rest
   of the row still scrolls under a finger. */
.acct-grip {
    position: relative;
    flex: none;
    display: inline-flex;
    align-items: center;
    color: var(--ui-dim);
    touch-action: none;
    cursor: grab;
}

.acct-grip::before {
    content: "";
    position: absolute;
    inset: -10px;
}

.acct-grip:active,
.section-chip-grip:active {
    cursor: grabbing;
}

/* A row's trailing actions, kept as one group so they wrap as one. */
.list-row-end {
    flex: none;
    display: flex;
    align-items: center;
    gap: .7rem;
}

.list-row-acts {
    display: flex;
    align-items: center;
    gap: .7rem;
}

/* The two key cards carry different button counts, so key rows always use the
   narrow shape: the summary spans the row, buttons drop below, right-aligned. */
.dev-keys .list-row {
    flex-wrap: wrap;
    row-gap: .5rem;
}

.dev-keys .list-row-acts {
    width: 100%;
    justify-content: flex-end;
}

/* A row whose one action was conditional and absent. */
.list-row-acts:empty {
    display: none;
}



/* A version number beside a device name — set smaller, lighter and dimmer, so
   the browser and OS read first and the exact build sits quietly behind them. */
.acct-ver {
    margin-left: .25rem;
    font-size: .82em;
    font-weight: 400;
    color: var(--ui-dim);
}@media (max-width: 34rem) {

    .acct-row {
        align-items: flex-start;
    }

    .acct-value {
        width: 100%;
    }

    /* Narrow lists: the first line stays beside its checkbox and icon; the
       buttons drop below as one right-aligned group, never one by one. */
    .list-row {
        flex-wrap: wrap;
        row-gap: .5rem;
    }

    /* The pair takes the whole second row: label at the left, buttons at the
       right. Not `space-between`: a row that carries only buttons has one
       child, and one child at "space between" sits at the left, which is not
       where a row's actions live. The label is pushed left instead, so the
       buttons keep the right whether or not it is there. */
    .list-row-end {
        width: 100%;
        justify-content: flex-end;
    }

    .list-row-end > :not(.list-row-acts) {
        margin-right: auto;
    }

    /* The list header (select-all, count, delete) wraps the same way. */
    .select-bar {
        flex-wrap: wrap;
        row-gap: .4rem;
    }
}

/* The account page while its answer is in flight. Same cards in the same order,
   so nothing moves when the real content replaces them. */


/* Where a jump landed. A ring rather than a tint, so a card with its own
   background does not go see-through; it clears itself, nothing here is state. */
.landed {
    animation: landed 1.4s ease-out;
}

@keyframes landed {
    from { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 40%, transparent); }
    to { box-shadow: 0 0 0 3px transparent; }
}

/* The waiting state's skin, worn by whatever box is standing in for the value:
   a line of its own, or a control that keeps its shape while empty. */
.bone {
    background: var(--ui-bone);
    animation: bone-pulse 1.4s ease-in-out infinite;
}

/* The box for a bone standing in for text; a width comes from the caller. */
.bone-line {
    display: block;
    height: .78rem;
    border-radius: 4px;
}

.bone-line--head {
    width: 5.5rem;
    height: .62rem;
    margin-bottom: .35rem;
}

.bone-line--big {
    height: 1.3rem;
}

/* Standing in for something that sits at the far side of its column. */
.bone-line--end {
    align-self: flex-end;
}

/* Standing in for a word inside a line of text, so it takes the line's size. */
.bone-line--inline {
    display: inline-block;
    height: 1em;
    width: 7em;
    vertical-align: -0.15em;
}

/* Each row a beat behind the one above, so the card reads as one object rather
   than a row of independently blinking bars. */
.bone:nth-child(2) {
    animation-delay: .08s;
}

.bone:nth-child(3) {
    animation-delay: .16s;
}

.bone:nth-child(4) {
    animation-delay: .24s;
}

@keyframes bone-pulse {
    0%, 100% {
        opacity: 1;
    }
    50% {
        opacity: .45;
    }
}@media (max-width: 62rem) {

    .lp-hero-grid {
        grid-template-columns: minmax(0, 1fr);
    }

    .lp-index {
        grid-auto-flow: column;
        grid-auto-columns: 1fr;
        gap: .6rem;
    }

    .lp-assembly-grid, .lp-depth-grid {
        grid-template-columns: minmax(0, 1fr);
    }

}

/* A press is a real wait — the database is a region away, and a provider's own
   page can take seconds to appear — so the control that was pressed says so. */
/* A busy button hides its label in place and floats the spinner over it, so it
   never resizes; the button only has to be a positioning context. */
/* One rule for every disabled control: killing pointer events means `:hover`
   never matches. Covers `<a aria-disabled>`, which cannot take `disabled`. */
button:disabled,
[aria-disabled="true"] {
    pointer-events: none;
}

.btn--busy {
    position: relative;
}

/* `display: contents` drops the wrapper's box so the label and icon lay out as
   unpressed; `visibility` inherits down while the boxes hold the size. */
.btn-ghost {
    display: contents;
    visibility: hidden;
}

.btn-spin {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}

.spin {
    /* Sized at all: an <i> is inline, where width and height are ignored. It only
       worked in the account buttons because a flex item is blockified. */
    display: inline-block;
    flex: none;
    box-sizing: border-box;
    vertical-align: middle;
    width: .72rem;
    height: .72rem;
    border: 2px solid currentColor;
    /* One transparent side is what makes the ring read as turning. */
    border-top-color: transparent;
    border-radius: 50%;
    animation: spin-turn .6s linear infinite;
}

@keyframes spin-turn {
    to {
        transform: rotate(360deg);
    }
}@media (prefers-reduced-motion: reduce) {

    /* Slowed rather than stopped: a still ring says nothing is happening. */
    .spin {
        animation-duration: 2s !important;
    }
}

/* An anchor cannot be disabled, and the reader is on their way out anyway. */
.acct-btn--waiting {
    pointer-events: none;
}

.acct-btn--waiting:not(:has(.spin)) {
    opacity: .45;
}

/* The mark's box is kept, so swapping the logo for a ring moves nothing. */
.list-row-icon .spin {
    width: 100%;
    height: 100%;
    margin: 0;
}

/* ================= terms and privacy ================= */

/* Narrower than the account page. These are read in long runs, and a line much
   past 70 characters is where the eye starts losing its place returning. */
.legal-main {
    flex: 1;
    width: 100%;
    max-width: 44rem;
    margin: 0 auto;
    padding: 6rem clamp(1rem, 4vw, 2.5rem) 4rem;
}

.legal-title {
    margin: 0;
    font-size: clamp(1.5rem, 3.4vw, 1.85rem);
    font-weight: 600;
    letter-spacing: -.035em;
}

.legal-effective {
    margin: .5rem 0 0;
    font-size: var(--fs-sm);
    color: var(--ui-dim);
    font-variant-numeric: tabular-nums;
}

.legal-lead {
    margin: 1.4rem 0 0;
    padding-bottom: 1.6rem;
    border-bottom: 1px solid var(--ui-line);
    font-size: var(--fs-base);
    line-height: 1.75;
    color: var(--ui-soft);
}

.legal-block {
    margin-top: 2.2rem;
}

.legal-block h2 {
    margin: 0 0 .7rem;
    font-size: var(--fs-lead);
    font-weight: 600;
    letter-spacing: -.01em;
}

.legal-block p, .legal-block li {
    font-size: var(--fs-base);
    line-height: 1.8;
    color: var(--ui-soft);
}

.legal-block p {
    margin: 0 0 .7rem;
}

.legal-block p:last-child {
    margin-bottom: 0;
}

.legal-block ul {
    margin: 0;
    padding-left: 1.05rem;
    display: flex;
    flex-direction: column;
    gap: .5rem;
}

/* ================= not found ================= */

.missing-main {
    flex: 1;
    width: 100%;
    max-width: 34rem;
    margin: 0 auto;
    padding: 0 clamp(1rem, 4vw, 2.5rem);
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
}

/* Large enough to read as the subject of the page rather than a caption, and
   set back in the dim tone so the sentence below it stays the thing you read. */
.missing-code {
    margin: 0;
    font-size: clamp(3.5rem, 12vw, 5.5rem);
    font-weight: 600;
    line-height: 1;
    letter-spacing: -.06em;
    color: var(--ui-line);
    font-variant-numeric: tabular-nums;
}

.missing-title {
    margin: 1.2rem 0 0;
    font-size: clamp(1.35rem, 3.4vw, 1.7rem);
    font-weight: 600;
    letter-spacing: -.035em;
    text-wrap: balance;
}

.missing-body {
    margin: .7rem 0 1.6rem;
    font-size: var(--fs-base);
    line-height: 1.7;
    color: var(--ui-soft);
}

/* Qualified: `.surface a` clears decoration and an element selector in it
   outweighs a lone class. The underline sits away, in a dimmer tone. */
.surface .missing-home {
    font-size: var(--fs-base);
    text-decoration: underline;
    text-decoration-color: var(--ui-underline);
    text-underline-offset: 4px;
    transition: text-decoration-color .15s, color .15s;
}

/* The ink, not white: this page follows the reader's brightness now, and a
   white word on the light theme's ground is a link that vanishes on hover. */
.surface .missing-home:hover {
    color: var(--ui-ink);
    text-decoration-color: currentColor;
}

/* ================= ask dialog ================= */

.ask-veil {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.25rem;
    background: rgb(0 0 0 / .62);
    backdrop-filter: blur(6px) saturate(1.1);
    -webkit-backdrop-filter: blur(6px) saturate(1.1);
}

.ask {
    width: 100%;
    max-width: 24rem;
    padding: 1.25rem;
    border: 1px solid var(--ui-line);
    border-radius: 8px;
    background: var(--ui-panel);
    box-shadow: 0 18px 48px rgb(0 0 0 / .5);
}

.ask:focus {
    outline: none;
}


.ask-field {
    display: grid;
    gap: .4rem;
    margin-top: 1rem;
}

.ask-label {
    font-size: var(--fs-sm);
    color: var(--ui-dim);
}

.ask .quiet {
    margin: 1rem 0 0;
}

.ask-actions {
    display: flex;
    justify-content: flex-end;
    gap: .5rem;
    margin-top: 1.25rem;
}@media (prefers-reduced-motion: no-preference) {

    .ask {
        animation: ask-in .16s ease-out;
    }

    @keyframes ask-in {
        from {
            opacity: 0;
            transform: translateY(.4rem);
        }
        to {
            opacity: 1;
            transform: none;
        }
    }
}

/* Comes up a little on first sight, only in the tail of the page: the sections
   above pin a stage for their whole runway. `useEnterReveal` adds `shown`. */
.lp-rise {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity 420ms ease-out, transform 420ms ease-out;
}

/* Each a beat behind the one before, so a row of cards reads as one thing
   settling rather than three racing. */
.lp-rise:nth-child(2) {
    transition-delay: 60ms;
}

.lp-rise:nth-child(3) {
    transition-delay: 120ms;
}

.lp-rise.shown {
    opacity: 1;
    transform: none;
}@media (prefers-reduced-motion: reduce) {

    .lp-rise {
        opacity: 1;
        transform: none;
        transition: none;
    }
}

/* ================= dashboard chrome ================= */

/* Below the bar, only on this screen: what it holds decides the whole page, so
   it belongs to the frame rather than the results it sits above. */
.dash-bar {
    position: sticky;
    top: 3.6rem;
    z-index: 15;

    /* A table: the label column takes the wider label's width, so both rows of
       tabs begin at the same place — that alignment says they belong together. */
    display: grid;
    grid-template-columns: max-content 1fr;
    align-items: center;
    column-gap: 1.1rem;
    row-gap: .375rem;

    padding-block: .35rem .45rem;
    /* The same glass as the nav above, so the stacked bars are one sheet. No
       top sheen here: a highlight line between them would read as a seam. */
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
    border-bottom: 1px solid var(--glass-border);
    box-shadow: var(--glass-shadow);
    /* Its ground and rule belong to the screen; its tabs to the column below.
       Padding is whatever lines the first tab up with the form beneath. */
    padding-inline: calc(max(0px, (100% - 1150px) / 2) + clamp(1rem, 4vw, 2rem));
}

/* The bar is fixed and the toolbar sticks under it, so the first thing in the
   page needs room to clear both. */
.container:not(.embed) {
    padding-top: 3.6rem;
}/* Narrow: the label column goes first — it costs more width than it earns.
   The rows keep their order, which is most of what the labels said. */
@media (max-width: 46rem) {

    .dash-bar {
        top: 3.6rem;
        grid-template-columns: 1fr;
        row-gap: 0;
    }

    .dash-glabel {
        display: none;
    }
}@media (max-width: 34rem) {

    .dash-tabs--store .dash-tab span {
        display: none;
    }

    /* The word goes and the separator with it: a lone chevron and a slash is
       not a path, it is two marks with nothing between them. */
    .back-link span,
    .crumb-slash {
        display: none;
    }

}

.dash-nav {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    min-height: 3.6rem;
    padding: .75rem clamp(1rem, 4vw, 2.5rem);
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
    border-bottom: 1px solid var(--glass-border);
    /* The soft lift under the bar, and the bright line along its top edge — the
       two touches that read the ground as a sheet of glass rather than a fill. */
    box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-highlight);
}

/* On the dashboard the nav and toolbar are one glass slab: the nav drops its
   shadow (keeping the sheen) and the bar below carries the pair's single shadow. */
.dash-nav:has(+ .dash-bar) {
    box-shadow: inset 0 1px 0 var(--glass-highlight);
}

.dash-brand {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    font-family: var(--font-mono);
    font-size: var(--fs-body);
    font-weight: 600;
    letter-spacing: -.02em;
    color: var(--text-strong);
    text-decoration: none;
}

.dash-logo {
    flex: none;
    width: 1.2rem;
    height: 1.2rem;
}

.dash-logo svg {
    display: block;
    width: 100%;
    height: 100%;
}

.dash-links {
    display: flex;
    align-items: center;
    gap: clamp(.9rem, 2vw, 1.7rem);
    font-size: var(--fs-body);
    color: var(--text-muted);
}

/* Bar-link defaults, weightless (`:where`): any skinned link sets its own ink. */
:where(.dash-links a) {
    color: inherit;
    text-decoration: none;
}

:where(.dash-links a:hover) {
    color: var(--text-strong);
}

/* ── the two rows ────────────────────────────────────────────────────────
 * Both are tabs: the store is the condition, the question is a view inside it,
 * and the rows are in that order. */

.dash-glabel {
    flex: none;
    font-size: var(--fs-caption);
    font-weight: 600;
    letter-spacing: var(--ls-caps);
    line-height: 1;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* The small square a button carries: a store's logo here, a brightness mark in
   the footer. Without a size an inline SVG takes the whole page. */
.dash-mark {
    flex: none;
    width: .95rem;
    height: .95rem;
    display: block;
}

.dash-mark svg {
    display: block;
    width: 100%;
    height: 100%;
}

.dash-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: .1rem;
    min-width: 0;
}

.dash-tab {
    display: inline-flex;
    align-items: center;
    gap: .38rem;
    padding: .42rem .55rem .45rem;
    border: 0;
    border-bottom: 2px solid transparent;
    background: none;
    color: var(--text-muted);
    font-family: inherit;
    font-size: var(--fs-sm);
    white-space: nowrap;
    cursor: pointer;
    transition: color .15s, border-color .15s;
}

.dash-tab:hover {
    color: var(--text-body);
}

.dash-tab.on {
    color: var(--text-strong);
    font-weight: 600;
    border-bottom-color: var(--text-strong);
}

/* The upper row carries a little more weight, so the two never read as one tab
   bar that happened to wrap. */
.dash-tabs--store .dash-tab {
    padding: .48rem .55rem .5rem;
    font-size: var(--fs-body);
}

.dash-tabs--view .dash-tab {
    font-size: var(--fs-sm);
}

.dash-tabs--view .dash-tab.on {
    border-bottom-color: var(--text-muted);
}

.dash-tab .dash-mark {
    opacity: .75;
    transition: opacity .15s;
}

.dash-tab.on .dash-mark, .dash-tab:hover .dash-mark {
    opacity: 1;
}

/* ── what can be done with the result, attached to it ─────────────────── */

.crumb {
    display: flex;
    align-items: center;
    /* One line always: the title ellipsizes rather than dropping below the
       back button, which left "‹ back /" stranded on its own row. */
    flex-wrap: nowrap;
    gap: .5rem;
    margin-bottom: .7rem;
}

/* The way back, at the head of the row it belongs to. Quiet: it is the way out
   of this result, not something to do with it. */
/* The one way back: a chevron and the destination, muted until hovered.
   Shared, so a stats screen and a result screen leave by the same door. */
.back-link {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    padding: 0;
    border: 0;
    background: none;
    color: var(--ui-dim);
    font-family: inherit;
    font-size: var(--fs-body);
    cursor: pointer;
    transition: color .15s;
}

.back-link:hover {
    color: var(--ui-ink);
}

.back-link svg {
    width: .8rem;
    height: .8rem;
    display: block;
}

.crumb-title b {
    color: var(--ui-ink);
}

.crumb-title code {
    font-family: var(--font-mono);
}

.crumb-slash {
    flex: none;
    color: var(--text-faint);
    font-size: var(--fs-body);
}

.crumb-title {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--fs-body);
    color: var(--ui-dim);
}

.crumb-title b {
    color: var(--text-strong);
    font-weight: 600;
}

.crumb-spacer {
    flex: 1;
}

.res-act {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .4rem .7rem;
    border: 1px solid var(--border-subtle);
    border-radius: 6px;
    background: none;
    color: var(--text-body);
    font-family: inherit;
    font-size: var(--fs-sm);
    cursor: pointer;
    transition: border-color .15s, color .15s;
}

.res-act:hover {
    border-color: var(--text-faint);
    color: var(--text-strong);
}

.res-act svg {
    width: .85rem;
    height: .85rem;
    display: block;
}

.res-act[aria-pressed="true"] {
    color: var(--text-strong);
    border-color: var(--text-faint);
}

.res-cta {
    flex: none;
    padding: .45rem .95rem;
    border: 1px solid var(--text-strong);
    border-radius: 6px;
    background: var(--text-strong);
    color: var(--bg-panel);
    font-family: inherit;
    font-size: var(--fs-sm);
    font-weight: 700;
    white-space: nowrap;
    cursor: pointer;
    transition: opacity .15s;
}

.res-cta:hover {
    opacity: .88;
}@media (max-width: 46rem) {

    .crumb-title {
        width: 100%;
    }

    .crumb-spacer {
        display: none;
    }
}

/* ================= dashboard footer ================= */


/* The display-items tabs, one per screen — the site's one underline tab shape,
   in the modal's tokens. */
.option-tabs {
    gap: .3rem;
    margin-bottom: .6rem;
    border-bottom: 1px solid var(--border-subtle);
}

.option-tab {
    flex: none;
    padding: .4rem .6rem .5rem;
    border: 0;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    background: none;
    color: var(--text-muted);
    font: inherit;
    font-size: var(--fs-sm);
    cursor: pointer;
}

.option-tab:hover {
    color: var(--text-body);
}

.option-tab.on {
    color: var(--text-strong);
    font-weight: 600;
    border-bottom-color: var(--text-strong);
    border-bottom-width: 3px;
}

/* A quiet sub-label inside a tab, where a second option group used to be. */
.option-subhead {
    display: block;
    margin: .9rem 0 .35rem;
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--text-muted);
}

/* The chips are the running order of the embed, so they can be dragged into the
   order they should be drawn in. */
.section-chip {
    cursor: grab;
}

.section-chip-grip {
    position: relative;
    /* Above the label, so the widened hit area is not shadowed by it. */
    z-index: 1;
    display: inline-flex;
    align-items: center;
    margin-right: .3rem;
    color: var(--text-faint);
    /* The drag handle: without this the browser scrolls instead of dragging. */
    touch-action: none;
    cursor: grab;
}

/* The dots stay small; the finger target does not. Hits on the halo resolve
   to the grip itself, so dragging works unchanged. */
.section-chip-grip::before {
    content: "";
    position: absolute;
    inset: -8px -8px -8px -11px;
}

/* The one being carried stays visible but steps back, so the gap it will leave
   is legible while it is in the air. */
.section-chip--carried {
    opacity: .4;
}

/* Where it would land. */
.section-chip--over {
    position: relative;
}

.section-chip--over::before {
    content: "";
    position: absolute;
    left: calc(-0.1875rem - 2px);
    top: 3px;
    bottom: 3px;
    width: 2px;
    border-radius: 999px;
    background: var(--accent);
}

.section-chip--over-end {
    position: relative;
}

.section-chip--over-end::after {
    content: "";
    position: absolute;
    right: calc(-0.1875rem - 2px);
    top: 3px;
    bottom: 3px;
    width: 2px;
    border-radius: 999px;
    background: var(--accent);
}

/* Plain :focus: a chip clicked with the mouse must read as selected too. */
.section-chip:focus {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}


/* ================= light, for everything but the landing ================= */

/* The reader's choice reaches every screen that is chrome rather than
   composition. `.surface--themed` opts a screen in; the landing does not carry it. */
:root[data-theme="light"] .surface--themed,
:root[data-theme="light"] .docs-page {
    --ui-ground: #f8f9fb;
    --ui-ink: #111827;
    --ui-dim: #5b6472;
    --ui-soft: #4b5563;
    --ui-line: #e4e7ec;
    --ui-field-line: #d1d5db;

    --ui-panel: #ffffff;
    --ui-input: #f2f4f8;
    --ui-hover: #f1f3f6;
    --ui-sunken: #e9ecf1;
    --ui-rule: #eef0f4;
    --ui-bone: #e9ecf1;
    --ui-quiet: #666e7c;
    --ui-line-strong: #c8ced8;

}

:root[data-theme="light"] {
    --ui-bad-line: #fecdca;
    --ui-bad-line-strong: #f04438;
    --ui-bad-ground: #fef3f2;
    --ui-bad-ink: #b42318;

    --ui-warn-line: #fedf89;
    --ui-warn-line-strong: #f79009;
    --ui-warn-ground: #fffaeb;
    --ui-warn-ink: #b54708;

    --ui-good-line: #a9e5c3;
    --ui-good-ink: #067647;

    --ui-note-line: #d0d5dd;
    --ui-note-ground: #f2f4f7;
    --ui-note-ink: #475467;

    --ui-focus-line: #5e6673;
    --ui-field-line: #d1d5db;
    --ui-mark: #111827;
    --ui-underline: #c8ced8;
    --ui-ink-strong: #000000;
}

/* The bar keeps its own dark ground on the landing, where it sits over artwork.
   On a themed screen it belongs to the page. */
:root[data-theme="light"] .surface--themed .site-nav,
:root[data-theme="light"] .docs-page .site-nav {
    --ui-ground: #f8f9fb;
    --ui-ink: #111827;
    --ui-dim: #5b6472;
    --ui-soft: #4b5563;
    --ui-line: #e4e7ec;
    --ui-field-line: #d1d5db;
    background: rgb(248 249 251 / .76);
    /* Light ground now, so the sheen and lift come from the theme's glass. */
    box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-highlight);
}


/* ================= the footer every screen shares ================= */

/* One rule for both surfaces: `.surface` screens resolve these tokens from
   their own set; the dashboard gets them from the block below. */
/* Across the whole screen and at its bottom: the page column grows, so on a
   short page this is pushed down rather than floating mid-viewport. */
.dash-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: .8rem 1.4rem;
    width: 100%;
    margin-top: auto;
    padding: 1.8rem clamp(1rem, 4vw, 2.5rem) 3rem;
    border-top: 1px solid var(--ui-line, var(--border-subtle));
    font-size: var(--fs-sm);
    color: var(--ui-dim, var(--text-faint));
}

/* Brightness and language share one line at one height: a button beside a group
   of them, so the padding is stated rather than left to add up differently. */
.dash-foot-end {
    display: flex;
    align-items: stretch;
    flex-wrap: nowrap;
    gap: .5rem;
    height: 1.85rem;
}

.dash-foot-end > * {
    height: 100%;
    box-sizing: border-box;
}/* Below this the row does not fit and becomes a centred column on purpose; the
   breakpoint sits just above where it actually runs out of room. */
@media (max-width: 36rem) {

    .dash-foot {
        flex-direction: column;
        align-items: center;
        justify-content: center;
        text-align: center;
        gap: .85rem;
    }
}

/* One button that cycles, so what it says is what is on rather than a row of
   three where two are always wrong. */
.dash-theme {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: 0 .7rem;
    border: 1px solid var(--ui-line, var(--border-subtle));
    border-radius: 5px;
    background: none;
    color: var(--ui-dim, var(--text-muted));
    font-family: inherit;
    font-size: var(--fs-sm);
    cursor: pointer;
    transition: border-color .15s, color .15s;
}

.dash-theme:hover {
    border-color: var(--ui-line-strong, var(--text-faint));
    color: var(--ui-ink, var(--text-strong));
}

.dash-theme .dash-mark {
    width: .85rem;
    height: .85rem;
}

/* Holds the sign-in buttons' place until the server answers. One fixed size:
   the two answers differ in width and either one would shift the links beside it. */
/* Wide enough for the widest answer in any language, so the slot never resizes.
   Right-aligned inside: the button that matters sits at the end of the bar. */
.site-nav-auth { gap: clamp(.9rem, 2vw, 1.7rem); }

.site-nav-bone {
    display: inline-block;
    width: 5.5rem;
    height: 1.85rem;
    border-radius: 4px;
    pointer-events: none;
}/* The language switch, on both surfaces. A `.surface` screen has no `--text-strong`,
   so these read the page's own tokens first and fall back to the dashboard's. */
.dash-switch {
    display: flex;
    align-items: stretch;
    gap: .15rem;
    padding: .15rem;
    border: 1px solid var(--ui-line, var(--border-subtle));
    border-radius: 5px;
}

.dash-switch button {
    padding: 0 .6rem;
    border: 0;
    border-radius: 3px;
    background: none;
    color: var(--ui-dim, var(--text-muted));
    font-family: inherit;
    font-size: var(--fs-sm);
    cursor: pointer;
}

.dash-switch button:hover {
    color: var(--ui-ink, var(--text-strong));
}

.dash-switch button.on {
    background: var(--ui-ink, var(--text-strong));
    color: var(--ui-ground, var(--bg-panel));
    font-weight: 500;
}

/* ================= the dashboard before it has been asked anything ================= */

.starter:lang(ko), .starter:lang(zh) {
    word-break: keep-all;
}

.starter {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1.1rem;
    min-height: 22rem;
    padding: 3rem 1.5rem;
    text-align: center;
}

.starter.returning {
    min-height: unset;
}

.starter-lead {
    max-width: 34rem;
    margin: 0;
    text-wrap: balance;
    font-size: var(--fs-title);
    line-height: 1.55;
    letter-spacing: -.01em;
    color: var(--ui-ink);
}

/* An underlined link rather than a filled button, the same way out the 404 page
   offers: this is a suggestion to look at something, not the screen's action. */
.starter-try {
    display: inline-flex;
    align-items: center;
    gap: .38rem;
    padding: 0;
    border: 0;
    background: none;
    color: var(--ui-soft);
    font-family: inherit;
    font-size: var(--fs-base);
    font-weight: 600;
    /* A border rather than `text-decoration`: the name rolls in a box taken out
       of flow, and an underline skips it. */
    padding-bottom: 3px;
    border-bottom: 1px solid var(--gray-400);
    cursor: pointer;
    transition: border-color .15s, color .15s;
}

.starter-try:hover {
    color: var(--ui-ink);
    border-bottom-color: currentColor;
}

/* One line tall and clipped, so the name arriving is only ever visible inside
   the button rather than growing it. */
.starter-window {
    position: relative;
    display: inline-block;
    height: 1.25em;
    overflow: hidden;
    vertical-align: bottom;
}

/* `flex-start`, so each name is its own width and the column takes the
   widest — stretched children would all report the container's. */
.starter-sizer {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    /* A hair over the widest name: text lays out in fractions of a pixel, the
       slot does not, and clipping ate the last column. */
    padding-right: 1px;
    visibility: hidden;
    white-space: nowrap;
}

.starter-roll {
    position: absolute;
    inset: 0;
}

.starter-roll {
    display: flex;
    flex-direction: column;
    transform: translateY(0);
}

.starter-roll.rolling {
    transform: translateY(-1.25em);
    transition: transform .75s cubic-bezier(.25, 1, .5, 1);
}

.starter-name {
    height: 1.25em;
    display: flex;
    align-items: center;
    justify-content: center;
    white-space: nowrap;
}

/* The same space, once the reader has a history of their own in it. */
.starter--lists {
    align-items: stretch;
    justify-content: flex-start;
    gap: 1.6rem;
    padding: 2rem clamp(0rem, 3vw, 1.5rem) 3rem;
    text-align: left;
}



/* Heading on the left, a quiet clear-all on the right. */
.starter-group-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: .5rem;
    padding-bottom: 0.25rem;
    border-bottom: 1px solid var(--gray-200);
}


/* A text link, not a button: underlined, dim, and only reaching full colour on
   hover — an exit that does not compete with the names it sits above. */
.starter-clear {
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    font: inherit;
    font-size: var(--fs-sm);
    color: var(--ui-dim);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.starter-clear:hover {
    color: var(--ui-soft);
}

.starter-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.starter-row {
    display: flex;
    align-items: center;
    gap: .5rem;
    border-radius: 6px;
}

.starter-row:hover {
    background: var(--ui-hover);
}

.starter-link {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .55rem .65rem;
    color: inherit;
    text-decoration: none;
}

.starter-store {
    flex: none;
    width: 1rem;
    height: 1rem;
    display: block;
}

.starter-store svg {
    display: block;
    width: 100%;
    height: 100%;
}

.starter-app {
    flex: none;
    max-width: 60%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--ui-ink);
}

/* Which store and which question, set back: the app's name is what is being
   looked for and the rest is how it was found. */
.starter-where {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--fs-sm);
    color: var(--ui-dim);
}

.starter-forget {
    flex: none;
    display: grid;
    place-items: center;
    width: 1.5rem;
    height: 1.5rem;
    margin-right: .4rem;
    padding: 0;
    border: 0;
    border-radius: 4px;
    background: none;
    color: var(--ui-quiet);
    cursor: pointer;
    opacity: 0;
    transition: opacity .12s, color .12s;
}

.starter-forget svg {
    width: 0.625rem;
    height: 0.625rem;
}

.starter-row:hover .starter-forget,
.starter-forget:focus-visible {
    opacity: 1;
}

.starter-forget:hover {
    color: var(--ui-ink);
}/* A pointer that never rests on a row still has to be able to reach this. */
@media (hover: none) {

    .starter-forget {
        opacity: 1;
    }
}

/* ── connected stores ─────────────────────────────────────────────────── */

/* A credential row is taller than the others — it carries a list of app chips —
   so its controls align to the top rather than the middle. */
/* The switch between public and official reviews, for a reader who manages the
   app. */
/* A developer reply, posted from the review card itself, for a reader
   authorised for the app. */
.review-reply-edit {
    margin-top: .5rem;
    display: flex;
    flex-direction: column;
    gap: .4rem;
}

/* The mark a credit-spending permission wears where keys are issued: after the
   name, in the mark colour; the meaning rides in its tooltip. */
.credit-mark {
    flex: none;
    display: inline-flex;
    width: 1em;
    height: 1em;
    margin-inline-start: .35em;
    vertical-align: -.14em;
    color: var(--ui-mark);
}

/* The bookmark flag on a chart card whose numbers are the store's own. */
.official-flag {
    flex: none;
    display: inline-flex;
    width: .75em;
    height: .75em;
    color: var(--ui-dim);
}

/* The AI draft's options, folded inside the reply editor. The box is its own;
   every control inside is the house one (.seg, checkbox skin, .input). */
/* Capped: every row is a label opposite one control, and across the full
   editor width the pair drifts too far apart to read as one setting. */
.draft-options {
    max-width: 26rem;
    padding: .6rem .7rem;
    border: 1px solid var(--border-subtle);
    border-radius: .45rem;
}

.draft-options-input {
    max-width: 11rem;
}

.review-reply-open {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    padding: .25rem .6rem;
    border: 1px solid var(--border-subtle);
    border-radius: .35rem;
    background: none;
    color: var(--text-body);
    font: inherit;
    font-size: var(--fs-sm);
    cursor: pointer;
}

.review-reply-open:hover {
    border-color: var(--text-faint);
    color: var(--text-strong);
}




.review-reply-cancel, .review-reply-send {
    display: inline-flex;
    align-items: center;
    padding: .3rem .7rem;
    border-radius: .35rem;
    font: inherit;
    font-size: var(--fs-sm);
    font-weight: 500;
    cursor: pointer;
    border: 1px solid var(--border-subtle);
}

.review-reply-cancel {
    background: none;
    color: var(--text-muted);
}

.review-reply-send {
    background: var(--text-strong);
    border-color: var(--text-strong);
    color: var(--bg-panel);
}

.review-reply-send:disabled {
    opacity: .5;
    cursor: default;
}

.review-reply-error {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--error-text);
}



/* Translation of the review into the reader's language. */
.review-translate {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .15rem .6rem;
    margin: .2rem 0 .35rem;
    font-size: var(--fs-sm);
}

/* The split button's caret half, tight against the main label. */
.review-translate-caret {
    margin-left: -.45rem;
    padding: .2rem .15rem;
    text-decoration: none;
}

.review-translate-chevron {
    flex: none;
    transition: transform .15s ease;
}

.review-translate-chevron.open {
    transform: rotate(180deg);
}

.review-translate-menu {
    position: absolute;
    top: 100%;
    left: 0;
    z-index: 30;
    min-width: 10rem;
    max-height: 14rem;
    overflow-y: auto;
    margin-top: .25rem;
    padding: .25rem;
    border: 1px solid var(--border-subtle);
    border-radius: .45rem;
    background: var(--bg-panel);
    box-shadow: 0 8px 24px rgba(0, 0, 0, .18);
}

.review-translate-item {
    display: flex;
    align-items: baseline;
    gap: .35rem;
    width: 100%;
    padding: .3rem .5rem;
    border: none;
    border-radius: .3rem;
    background: none;
    color: var(--text-body);
    font: inherit;
    font-size: var(--fs-sm);
    text-align: left;
    cursor: pointer;
}

.review-translate-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.review-translate-item:hover {
    background: color-mix(in srgb, var(--ui-soft) 8%, transparent);
}

.review-translate-item--on {
    font-weight: 600;
}

/* Reserved even when empty, so names line up with the checked row. */
.review-translate-check {
    width: .9rem;
    flex: none;
}

.review-translate-default {
    font-size: var(--fs-caption);
    color: var(--text-faint);
}

.review-translate-btn {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: 0;
    border: none;
    background: none;
    color: var(--text-faint);
    font: inherit;
    font-size: var(--fs-sm);
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
}

.review-translate-btn:hover {
    color: var(--text-strong);
}

.review-translate-btn:disabled {
    cursor: default;
    text-decoration: none;
}

/* Visible but signed out: the feature shows itself, the click leads to sign-in. */
.review-translate-btn--dim {
    opacity: .55;
}

.review-translate-note {
    color: var(--text-faint);
}

.review-translate-error {
    color: var(--error-text);
}

/* Key management on the account page. */


.dev-key-code {
    font-size: var(--fs-sm);
    word-break: break-all;
}

/* The key's policy summary: one row per granted store plus the account flags.
   Rows subgrid one badge column hugging the widest title up to a cap. */
.key-grants {
    display: grid;
    grid-template-columns: fit-content(11rem) 1fr;
    column-gap: .8rem;
}

.key-grant-group {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: subgrid;
    align-items: start;
    padding: .4rem 0;
}

.key-grant-group + .key-grant-group {
    border-top: 1px solid var(--ui-rule);
}

.key-grant-group .list-row-title {
    min-width: 0;
}

.key-grant-group .list-row-title span,
.key-grant-group .list-row-title b {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}



.perm-chip {
    padding: .05rem .5rem;
    border: 1px solid var(--ui-rule);
    border-radius: 999px;
    font-size: var(--fs-sm);
    color: var(--ui-dim);
    white-space: nowrap;
}

/* The fold: shows "+N" collapsed, "접기" expanded; toggles its own group only. */
.perm-chip--more {
    background: none;
    border-color: var(--ui-line-strong);
    color: var(--ui-ink);
    font: inherit;
    font-size: var(--fs-sm);
    cursor: pointer;
}

.perm-chip--more:hover {
    background: var(--ui-hover);
}

/* The key stub: real edges around a blurred, fake middle. */
.dev-key-stub {
    font-family: var(--font-mono);
    white-space: nowrap;
}

/* Blurred with text-shadow, not filter: a filter promotes the span to its own
   composited layer, which escapes the sticky header's glass and draws over it. */
.dev-key-mask {
    color: transparent;
    text-shadow: 0 0 3px var(--ui-dim);
    user-select: none;
    margin: 0 .1rem;
}



/* The one moment the full key is visible: a quiet inset panel — the bold in
   the note carries the warning, not the box. */
.dev-key-fresh {
    display: flex;
    flex-direction: column;
    gap: .35rem;
    padding: .55rem .65rem;
    border: 1px solid var(--ui-rule);
    border-radius: .45rem;
    background: var(--ui-sunken);
}

.dev-key-fresh-note {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--ui-dim);
}



.dev-key-fresh-row .dev-key-code {
    flex: 1 1 14rem;
    min-width: 0;
    font-size: var(--fs-sm);
    color: var(--ui-ink);
}

/* The embed key policy editor. */
.ask--wide {
    max-width: 34rem;
    width: 100%;
}

.grant-list {
    display: flex;
    flex-direction: column;
    gap: .5rem;
    max-height: 18rem;
    overflow-y: auto;
    margin: .5rem 0;
}

.grant-row {
    border: 1px solid var(--border-subtle);
    border-radius: .45rem;
    padding: .5rem .65rem;
}

.grant-credential {
    display: flex;
    align-items: center;
    gap: .5rem;
    font-size: var(--fs-base);
    font-weight: 600;
    cursor: pointer;
}

.grant-actions {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
    gap: .3rem .75rem;
    margin-top: .45rem;
    padding-left: 1.4rem;
}

.grant-action {
    display: flex;
    align-items: center;
    gap: .4rem;
    font-size: var(--fs-body);
    color: var(--text-body);
    cursor: pointer;
}

.grant-action-hint {
    font-size: var(--fs-caption);
    color: var(--text-faint);
}

/* The account-wide permission group in the key editors, boxed like a store row. */
.grant-global-title {
    display: block;
    font-size: var(--fs-body);
    font-weight: 600;
    color: var(--text-body);
}

.grant-actions--global {
    padding-left: 0;
}

.review-reply-delete {
    color: var(--error-text);
}

.review-reply-delete:hover {
    border-color: var(--error-text);
    color: var(--error-text);
}

.review-reply-delete:disabled {
    opacity: .5;
    cursor: default;
}

/* Management embed origins ------------------------------------------------ */


/* Ligatures off: the mono font fuses `//` into one glyph while typing a URL. */
.embed-origins-input {
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-variant-ligatures: none;
}



.list-row--store {
    align-items: flex-start;
}

.list-row--store .list-row-icon {
    margin-top: .1rem;
}

/* The line under the name carries an issuer id or a service-account address,
   neither of which a browser will break on its own. */
.list-row--store .quiet {
    overflow-wrap: anywhere;
}

/* A credential the store no longer accepts. The icon dims and a badge says so,
   so a dead key is not mistaken for a working one at a glance. */
.list-row--stale .list-row-icon {
    opacity: .45;
}

/* A small state chip beside a name — "Reconnect needed". */
.acct-badge {
    display: inline-block;
    margin-left: .4rem;
    padding: .05rem .4rem;
    border-radius: 999px;
    font-size: var(--fs-caption);
    font-weight: 500;
    vertical-align: middle;
    white-space: nowrap;
}

.acct-badge--bad {
    color: var(--ui-bad-ink);
    background: color-mix(in srgb, var(--ui-bad-ink) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--ui-bad-ink) 35%, transparent);
}

.acct-chips {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem;
    margin-top: .35rem;
}

/* Status pill: a muted outline chip shared by every surface, so it carries no
   page's colours. The margin sits on the modifiers, which follow a name. */
.chip-status {
    display: inline-block;
    padding: 0.06rem 0.45rem;
    border: 1px solid var(--ui-line);
    border-radius: 999px;
    font-size: var(--fs-caption);
    color: var(--ui-dim);
}

.chip-status--accent {
    margin-left: 0.4rem;
    border-color: var(--accent);
    color: var(--accent);
}

.chip-status--bad {
    margin-left: 0.4rem;
    border-color: var(--ui-bad-ink);
    color: var(--ui-bad-ink);
}

/* The other verdict tones. Admin's guard chips wore these names before the
   rules existed, so they rendered as plain grey; ink tokens, never fills —
   an outlined chip is text on a ground. Caution rides the warn ink: no
   caution ink exists and a fourth hue would say nothing warn does not. */
.chip-status--good {
    border-color: var(--ui-good-ink);
    color: var(--ui-good-ink);
}

.chip-status--warn,
.chip-status--caution {
    border-color: var(--ui-warn-ink);
    color: var(--ui-warn-ink);
}

.acct-chip {
    padding: .1rem .45rem;
    border: 1px solid var(--border-subtle);
    border-radius: 999px;
    background: var(--bg-elev);
    font-size: var(--fs-sm);
    font-family: var(--font-mono);
    color: var(--text-body);
}

/* The connect dialog: a stack of labelled fields and a right-aligned pair of
   actions, the same shape the rest of the account uses. */


/* The connection guide: collapsed by default, so a reader who already has a key
   is not made to scroll past it. */
/* No filled box or border: a plain bold toggle line with a disclosure triangle
   reads as a section to open, not an input. */
.guide {
    /*margin-bottom: 1rem;*/
    /*border-top: 1px solid var(--border-subtle);*/
}

.guide-summary {
    padding: .6rem 0;
    font-size: var(--fs-body);
    font-weight: 600;
    color: var(--text-strong);
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: center;
    gap: .75rem;
}

.guide-summary:hover {
    color: var(--accent);
}

/* The disclosure triangle, drawn ourselves so it turns with the open state. */
.guide-summary::-webkit-details-marker {
    display: none;
}

.guide-summary::before {
    content: "";
    width: .375rem;
    height: .375rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(-45deg);
    transition: transform .15s;
    opacity: .6;
}

.guide[open] > .guide-summary::before {
    transform: rotate(45deg) translateY(-50%);
}



.guide-title {
    margin: 0;
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--text-strong);
}

.guide-official {
    flex: none;
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--accent);
    text-decoration: none;
    white-space: nowrap;
}

.guide-official:hover {
    text-decoration: underline;
}

/* The carousel: one slide wide, the track holding all steps side by side and
   sliding by whole slides. */
.guide-viewport {
    margin-top: .6rem;
    overflow: hidden;
}

.guide-track {
    display: flex;
    transition: transform .28s ease;
}

.guide-slide {
    flex: 0 0 100%;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: .5rem;
}

/* The console link, fixed below the carousel so it does not jump between steps.
   The reserved height keeps the row from collapsing on a step that has no link. */
.guide-open {
    min-height: 1.5rem;
    margin-top: .6rem;
    display: flex;
    align-items: center;
}

.guide-text {
    white-space: pre-line;
    margin: 0;
    font-size: var(--fs-sm);
    line-height: 1.55;
    color: var(--text-body);
    min-height: 2.5rem;
}

/* The link to the console page this step happens on. */
.guide-link {
    align-self: flex-start;
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--accent);
    text-decoration: none;
}

.guide-link:hover {
    text-decoration: underline;
}

/* Arrows either side of the dots. */
.guide-nav {
    margin-top: .2rem;
    margin-bottom: .7rem;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .6rem;
}

.guide-arrow {
    flex: none;
    width: 1.6rem;
    height: 1.6rem;
    border: 1px solid var(--border-subtle);
    border-radius: 50%;
    background: var(--bg-panel);
    color: var(--text-body);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
}

.guide-arrow svg {
    width: 0.8rem;
    height: 0.8rem;
}

.guide-arrow--prev svg {
    transform: rotate(180deg);
}

.guide-arrow:hover {
    color: var(--text-strong);
    border-color: var(--text-faint);
}

.guide-arrow:disabled {
    opacity: .35;
    cursor: default;
}

/* The active bullet marks which step is showing; the rest are small and faint. */


.guide-dot {
    width: .45rem;
    height: .45rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--border-subtle);
    cursor: pointer;
    transition: background .15s, width .2s, border-radius .2s;
}

.guide-dot:hover {
    background: var(--text-faint);
}

.guide-dot.on {
    width: 1.1rem;
    border-radius: .25rem;
    background: var(--accent);
}

/* HTML stand-ins for the console screens, fixed to a light palette: the real
   consoles are light whatever theme the reader keeps. */
.mock {
    --m-bg: #ffffff;
    --m-ink: #1d1d1f;
    --m-dim: #6e6e73;
    --m-line: #e3e3e6;
    --m-elev: #f5f5f7;
    --m-blue: #0071e3;

    border: 1px solid var(--m-line);
    border-radius: .5rem;
    overflow: hidden;
    background: var(--m-bg);
    color: var(--m-ink);
    font-size: var(--fs-caption);
    line-height: 1.4;
    position: relative;
}

.mock-page {
    display: flex;
    min-height: 8.5rem;
}

.mock-content {
    flex: 1;
    min-width: 0;
    padding: .55rem .65rem;
    display: flex;
    flex-direction: column;
    gap: .5rem;
}

.mock-page-h {
    font-size: var(--fs-lead);
    font-weight: 700;
}

.mock-h2 {
    font-size: var(--fs-sm);
    font-weight: 600;
    margin-top: .1rem;
}

.mock-desc {
    margin: 0;
    color: var(--m-dim);
    font-size: var(--fs-caption);
    line-height: 1.5;
}

.mock-crumb {
    color: var(--m-dim);
}

/* Apple ------------------------------------------------------------------- */
.mock--apple .mock-topnav {
    display: flex;
    align-items: center;
    gap: .4rem;
    height: 1.5rem;
    padding: 0 .55rem;
    background: var(--m-elev);
    overflow: hidden;
}

.mock-logo {
    width: .8rem;
    height: .8rem;
    flex: none;
    color: var(--m-ink);
    display: block;
}

.mock-logo svg {
    width: 100%;
    height: 100%;
    display: block;
}

.mock-brand {
    font-weight: 600;
    flex: none;
    white-space: nowrap;
}

/* Nowrap and clip rather than wrap: a real top bar runs off the edge, it does
   not stack onto a second line. */
/* The whole menu group — the collapsed "···" and the items — sits at the right,
   with the gap falling between the brand and the group. */
.mock-topnav-items {
    display: flex;
    min-width: 0;
    margin: auto .25rem;
    overflow: hidden;
    color: var(--m-dim);
    white-space: nowrap;
    justify-content: space-around;
    flex: 1;
}

.mock-topnav-items span {
    flex: none;
}

.mock-topnav-items .on {
    color: var(--m-ink);
    font-weight: 600;
}

.mock-userchip {
    flex: none;
    width: .8rem;
    height: .8rem;
    border-radius: 50%;
    background: #cfcfd4;
}

.mock-side {
    flex: none;
    width: 6.2rem;
    padding: .55rem .35rem;
    display: flex;
    flex-direction: column;
    gap: .25rem;
}

.mock-side-h {
    color: var(--m-dim);
    font-weight: 700;
    font-size: var(--fs-caption);
    text-transform: uppercase;
    letter-spacing: var(--ls-caps);
    margin-bottom: .1rem;
}

.mock-side-item {
    padding: .2rem .35rem;
    border-radius: .25rem;
    color: var(--m-dim);
}

.mock-side-item.on {
    background: var(--m-elev);
    color: var(--m-ink);
    font-weight: 600;
}

.mock-ptabs {
    display: flex;
    gap: .75rem;
    border-bottom: 1px solid var(--m-line);
    color: var(--m-dim);
    white-space: nowrap;
    overflow: hidden;
}

.mock-ptabs span {
    flex: none;
}

.mock-ptabs .on {
    color: var(--m-blue);
    font-weight: 600;
    box-shadow: inset 0 -2px 0 var(--m-blue);
    padding-bottom: .18rem;
}

.mock-ptabs--sub {
    font-size: var(--fs-caption);
}

.mock-issuer {
    display: flex;
    align-items: center;
    gap: .4rem;
    border-radius: .3rem;
}

.mock-k {
    color: var(--m-dim);
}

.mock-v {
    font-family: var(--font-mono);
}

.mock-link {
    color: var(--m-blue);
    font-weight: 500;
}

.mock-issuer .mock-link {
    margin-left: auto;
}





.mock-ghost {
    padding: .12rem .4rem;
    border: 1px solid var(--m-line);
    border-radius: .25rem;
    color: var(--m-dim);
}

.mock-table {
    border-top: 1px solid var(--m-line);
    border-bottom: 1px solid var(--m-line);
    overflow: hidden;
}

.mock-tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, .75fr);
    gap: .4rem;
    padding: .3rem .45rem;
    align-items: center;
}

.mock-tr > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mock-th {
    background: var(--m-elev);
    color: var(--m-dim);
    font-size: var(--fs-caption);
}

.mock-tr:not(.mock-th) {
    border-top: 1px solid var(--m-line);
}

.mock-tr .mock-link {
    justify-self: end;
    overflow: visible;
}

.mock-scrim {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, .28);
}

.mock-modal {
    position: absolute;
    z-index: 1;
    left: 50%;
    top: 54%;
    transform: translate(-50%, -50%);
    width: 74%;
    background: var(--m-bg);
    border-radius: .4rem;
    box-shadow: 0 8px 24px rgba(0, 0, 0, .25);
    padding: .6rem;
    display: flex;
    flex-direction: column;
    gap: .4rem;
}

.mock-modal-h {
    font-weight: 700;
    font-size: var(--fs-sm);
}



.mock-input { gap: .12rem; }

.mock-input span {
    color: var(--m-dim);
}

.mock-input i {
    font-style: normal;
    padding: .25rem .4rem;
    border: 1px solid var(--m-line);
    border-radius: .25rem;
    background: var(--m-bg);
}

.mock-btn {
    align-self: flex-start;
    padding: .25rem .55rem !important;
    border-radius: .25rem;
    background: var(--m-elev);
    color: var(--m-ink);
    font-weight: 600;
    border: 1px solid var(--m-line);
}

.mock-btn-add {
    position: relative;
    width: .75rem;
    height: .75rem;
    padding: 0 !important;
    border-radius: 50% !important;
    background: var(--m-elev);
    color: var(--m-ink);
}

.mock-btn-add::before,
.mock-btn-add::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: .4rem;
    height: 1px;
    background: #fff;
    transform: translate(-50%, -50%);
}

.mock-btn-add::after {
    width: 1px;
    height: .4rem;
}

.mock-btn--blue {
    background: var(--m-blue);
    color: #fff;
    border-color: var(--m-blue);
}

.mock-btn--wide {
    align-self: flex-start;
}

/* Google ------------------------------------------------------------------ */
.mock--google .mock-gbar {
    display: flex;
    align-items: center;
    gap: .5rem;
    height: 1.6rem;
    padding: 0 .55rem;
    border-bottom: 1px solid var(--m-line);
}

.mock-ham { gap: .12rem; }

.mock-ham i {
    width: .7rem;
    height: 1.5px;
    background: var(--m-dim);
    display: block;
}

.mock-gtitle {
    font-weight: 500;
    font-size: var(--fs-caption);
    color: #5f6368;
}

.mock-gsearch {
    margin-left: auto;
    width: 5rem;
    height: .8rem;
    border-radius: .5rem;
    background: var(--m-elev);
}

.mock-gavatar {
    width: .85rem;
    height: .85rem;
    border-radius: 50%;
    background: #cfcfd4;
    flex: none;
}

.mock-grail {
    flex: none;
    width: 5.4rem;
    padding: .55rem .3rem;
    border-right: 1px solid var(--m-line);
}

.mock-grail-item {
    display: block;
    padding: .28rem .4rem;
    border-radius: 0 1rem 1rem 0;
    color: var(--m-dim);
}

.mock-grail-item.on {
    background: #e8f0fe;
    color: #1967d2;
    font-weight: 600;
}



.mock-menu-item {
    padding: .22rem .4rem;
}

.mock-menu-item:first-child {
    border: 1px solid var(--m-line);
    border-radius: .25rem;
    align-self: flex-start;
}

.mock-menu-sub {
    color: var(--m-dim);
    padding-left: .8rem;
}

.mock-radio, .mock-check {
    display: flex;
    align-items: center;
    gap: .4rem;
}

.mock-rb, .mock-cb {
    width: .7rem;
    height: .7rem;
    flex: none;
    border: 1.5px solid var(--m-blue);
}

.mock-rb {
    border-radius: 50%;
    position: relative;
}

.mock-rb.on::after {
    content: "";
    position: absolute;
    inset: .18rem;
    border-radius: 50%;
    background: var(--m-blue);
}

.mock-cb {
    border-radius: .15rem;
}

.mock-cb.on {
    background: var(--m-blue);
}

.mock-chip {
    align-self: flex-start;
    padding: .12rem .4rem;
    border-radius: 1rem;
    background: var(--m-elev);
    color: var(--m-dim);
}

/* The element the step points at — an accent ring that says "press this". */
.is-target {
    padding: .2rem .5rem;
    position: relative;
    border-radius: .3rem;
    box-shadow: 0 0 0 2px rgba(0, 113, 227, .6),
    0 0 0 5px rgba(0, 113, 227, .18);
    animation: target-pulse 1.5s ease-in-out infinite;
}

@keyframes target-pulse {
    0%,
    100% {
        box-shadow: 0 0 0 2px rgba(0, 113, 227, .6),
        0 0 0 5px rgba(0, 113, 227, .18);
    }

    50% {
        box-shadow: 0 0 0 3px rgba(0, 113, 227, 1),
        0 0 0 9px rgba(0, 113, 227, .35),
        0 0 18px rgba(0, 113, 227, .35);
    }
}

.mock--google .is-target {
    box-shadow: 0 0 0 2px rgba(25, 103, 210, .6),
    0 0 0 5px rgba(25, 103, 210, .18);
    animation-name: google-target-pulse;
}

@keyframes google-target-pulse {
    0%,
    100% {
        box-shadow: 0 0 0 2px rgba(25, 103, 210, .6),
        0 0 0 5px rgba(25, 103, 210, .18);
    }

    50% {
        box-shadow: 0 0 0 3px rgba(25, 103, 210, 1),
        0 0 0 9px rgba(25, 103, 210, .35),
        0 0 18px rgba(25, 103, 210, .35);
    }
}

/* The empty slot, shown until a screenshot is dropped in. */
.modal-textarea {
    resize: vertical;
    min-height: 4.5rem;
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    line-height: 1.5;
}






/* ── selecting rows to remove several at once ─────────────────────────── */

.select-bar {
    display: flex;
    align-items: center;
    gap: .8rem;
    /* Hold the button row's height so revealing "삭제" doesn't nudge the list.
       Box is border-box: cover the button plus this row's padding/border. */
    min-height: calc(2.5rem + 2px);
    margin-bottom: .3rem;
    padding-bottom: .5rem;
    border-bottom: 1px solid var(--ui-rule);
    font-size: var(--fs-body);
}

/* Pushes everything after the count — one button or several — to the right as a
   group; the buttons themselves sit together, spaced by the row's own gap. */
.select-bar-count {
    color: var(--ui-soft);
    margin-right: auto;
}

/* The per-row checkbox sits ahead of the store mark, aligned with the row's
   first line of text. */
.list-row-pick {
    flex: none;
    display: inline-flex;
    align-items: center;
    padding-top: .1rem;
    cursor: pointer;
}




/* ================================================================
 * Category grid — App-Store-like tinted tiles
 * ================================================================ */

.cat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
    gap: 0.75rem;
}

.cat-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.875rem 0.5rem;
    background: var(--bg-elev);
    border: 1px solid transparent;
    border-radius: 0.75rem;
    cursor: pointer;
    text-decoration: none;
    transition: background 0.15s, border-color 0.15s;
}

.cat-tile:hover {
    background: var(--bg-elev-hover);
    border-color: var(--border-subtle);
}

.cat-tile--static,
.cat-tile--static:hover {
    cursor: default;
    background: var(--bg-elev);
    border-color: transparent;
}

.cat-tile-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    flex-shrink: 0;
    border-radius: 0.75rem;
    background: var(--accent-light);
    color: var(--accent);
}

/* On dark ground the faint accent tint and a dark accent glyph both wash out,
   so deepen the tile and lift the glyph toward white for any chosen accent. */
:root[data-theme="dark"] .cat-tile-icon {
    background: color-mix(in srgb, var(--accent) 26%, transparent);
    color: color-mix(in srgb, var(--accent) 55%, white);
}

.cat-tile-id {
    font-size: var(--fs-caption);
    font-weight: 500;
    line-height: 1;
    color: var(--text-body);
    text-align: center;
    word-break: break-all;
}

.cat-tile-name {
    font-size: var(--fs-sm);
    font-weight: 500;
    line-height: 1.3;
    color: var(--text-strong);
    text-align: center;
    word-break: keep-all;
}

/* The model an endpoint speaks, as a pill beside its example headings; both
   link to the schema section. Inside the uppercased subhead, so casing is restated. */
.docs-model-name {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    margin-left: 0.6rem;
    padding: 0.14rem 0.55rem;
    border: 1px solid var(--border-subtle);
    border-radius: 999px;
    background: var(--bg-panel);
    font-size: var(--fs-sm);
    font-weight: 500;
    letter-spacing: normal;
    text-transform: none;
    color: var(--text-muted);
    vertical-align: middle;
    text-decoration: none;
}

.docs-model-name code {
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    color: var(--accent);
}

a.docs-model-name:hover {
    border-color: var(--accent);
}

a.docs-model-name:hover code {
    text-decoration: underline;
}

/* Anchor jumps land clear of the fixed page bar (3.6rem) instead of under it. */
.docs-main [id],
.docs-endpoint,
.docs-schema {
    scroll-margin-top: 4.6rem;
}

/* The page navigator shared by every paged account list: prev/next arrows and
   jumpable page numbers, left-aligned under the list. */
.pager {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 0.3rem;
    margin-top: 0.6rem;
}

.pager-arrow,
.pager-num {
    min-width: 1.9rem;
    height: 1.9rem;
    padding: 0 0.35rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 999px;
    background: none;
    color: var(--ui-dim);
    font-family: inherit;
    font-size: var(--fs-body);
    font-variant-numeric: tabular-nums;
    cursor: pointer;
    transition: color .15s, background .15s;
}

.pager-arrow svg {
    width: 1rem;
    height: 1rem;
    display: block;
}

.pager-arrow:hover,
.pager-num:hover:not(.on) {
    color: var(--ui-ink);
    background: var(--ui-hover);
}

.pager-num.on {
    background: var(--ui-ink);
    color: var(--ui-ground);
    font-weight: 600;
    cursor: default;
}

.pager-arrow:disabled {
    opacity: .4;
}

.pager-gap {
    min-width: 1.2rem;
    text-align: center;
    color: var(--ui-quiet);
    user-select: none;
}

/* The embed's attribution mark: a pill fixed to the frame viewport, visible
   through internal scrolling. A paid owner's key session stops it rendering. */
.embed-brand {
    position: fixed;
    right: 0.6rem;
    /* Same corner rule the deep-link editor's button follows: nothing claims
       this corner on the embed surface today, so the fallback keeps it put. */
    bottom: calc(0.6rem + var(--corner-claim, 0px));
    z-index: 50;
    display: flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.25rem 0.55rem;
    border: 1px solid var(--border-subtle);
    border-radius: 999px;
    background: var(--bg-panel);
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
    font-size: var(--fs-caption);
    font-weight: 600;
    letter-spacing: var(--ls-caps);
    color: var(--text-faint);
    text-decoration: none;
}

.embed-brand:hover {
    color: var(--accent);
}

.embed-brand svg {
    width: 0.85rem;
    height: 0.85rem;
    flex-shrink: 0;
}

.embed-brand i {
    font-style: normal;
}

/* Allowance meters: a count and a small track whose fill warns as the
   allowance runs out. The compact form pins to the card's top-right. */
.acct-card {
    position: relative;
}

.usage {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.25rem;
}

.acct-meter {
    position: absolute;
    top: 1.15rem;
    right: 1.25rem;
}

.usage-count {
    font-size: var(--fs-sm);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--ui-dim);
}

.usage-track {
    width: 5.5rem;
    height: 0.3rem;
    border-radius: 999px;
    background: var(--ui-sunken);
    overflow: hidden;
}

.usage--wide .usage-track {
    width: 100%;
}

.usage--wide {
    width: 100%;
    max-width: 24rem;
    align-items: stretch;
}

.usage--wide .usage-count {
    font-size: var(--fs-base);
    align-self: flex-end;
}

.usage-fill {
    display: block;
    height: 100%;
    border-radius: inherit;
    background: var(--accent);
    transition: width 0.25s ease;
}

.usage--warn .usage-fill {
    background: var(--fill-warn);
}

.usage--bad .usage-fill {
    background: var(--fill-bad);
}

.usage--bad .usage-count {
    color: var(--error-text);
}

.acct-credit-label { gap: .15rem; }

.acct-credit-title {
    /* Same size as quiet; only the weight rises. */
    font-size: var(--fs-sm);
    font-weight: 600;
}



/* Glyph refresh beside the text: a bare icon, no button chrome. While the
   fetch runs it swaps to a same-size spinning ring. */
.acct-refresh {
    border: none;
    background: none;
    padding: 0;
    line-height: 0;
    color: var(--ui-dim);
    cursor: pointer;
}

.acct-refresh svg {
    width: 0.8rem;
    height: 0.8rem;
    display: block;
}

.acct-refresh:hover {
    color: var(--ui-ink);
}

.acct-refresh--busy svg {
    animation: acct-refresh-spin 0.8s linear infinite;
}

@keyframes acct-refresh-spin {
    to {
        transform: rotate(360deg);
    }
}@media (prefers-reduced-motion: reduce) {

    .acct-refresh--busy svg {
        animation-duration: 2s !important;
    }
}

/* ---------------------------------------------------------------------------
   Deep-link studio (/deeplinks). Sits on the account surface's --ui-* tokens;
   the route swatches are the analytics identity colors. */
:root {
    --dlr-scheme: #2a78d6;
    --dlr-universal: #eb6834;
    --dlr-intent: #1baf7a;
    --dlr-fallback: #eda100;
    --dlr-store: #e87ba4;
    --dlr-escape: #0e7490;
    --dlr-qr: #6b7280;
    --dlr-failed: #e34948;
}

:root[data-theme="dark"] {
    --dlr-scheme: #3987e5;
    --dlr-universal: #d95926;
    --dlr-intent: #199e70;
    --dlr-fallback: #c98500;
    --dlr-store: #d55181;
    --dlr-escape: #4fb3cd;
    --dlr-qr: #8b93a1;
    --dlr-failed: #e66767;
}

/* ---------------------------------------------------------------------------
   Dashboard side menu: the hamburger in the site nav and the drawer it opens.
   Drawn from the global app tokens so it reads the same on every surface. */
.dash-menu-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border: none;
    border-radius: 8px;
    background: none;
    color: var(--text-body);
    cursor: pointer;
}
.dash-menu-btn:hover { color: var(--text-strong); background: var(--bg-elev); }

.dash-drawer-backdrop {
    position: fixed;
    inset: 0;
    z-index: 60;
    background: rgba(0, 0, 0, .38);
}

.dash-drawer {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: min(320px, 88vw);
    background: var(--bg-panel);
    border-left: 1px solid var(--border-subtle);
    box-shadow: var(--shadow-panel);
    display: flex;
    flex-direction: column;
    animation: dash-drawer-in .18s ease;
}

@keyframes dash-drawer-in {
    from { transform: translateX(24px); opacity: 0; }
    to { transform: none; opacity: 1; }
}.dash-drawer-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: .9rem 1rem .7rem;
    border-bottom: 1px solid var(--border-muted);
    font-size: var(--fs-sm);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: var(--ls-caps);
    color: var(--text-faint);
}

.dash-drawer-x {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border: 0;
    border-radius: 7px;
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
}
.dash-drawer-x:hover { color: var(--text-strong); background: var(--bg-elev); }

.dash-drawer-list {
    display: flex;
    flex-direction: column;
    gap: .25rem;
    padding: .7rem;
}

.dash-drawer-item {
    /* The positioning context for the badge, which is stuck on like a tag
       rather than laid out. */
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: .7rem;
    padding: .65rem .7rem;
    border-radius: 10px;
    text-decoration: none;
    color: var(--text-body);
}
.dash-drawer-item:hover { background: var(--bg-elev); }
.dash-drawer-item.on { background: var(--bg-elev); }
.dash-drawer-item.on b { color: var(--text-strong); }

.dash-drawer-icon {
    display: inline-flex;
    color: var(--text-muted);
    margin-top: .1rem;
}
.dash-drawer-item.on .dash-drawer-icon { color: var(--text-strong); }

.dash-drawer-text { display: flex; flex-direction: column; gap: .05rem; min-width: 0; }
.dash-drawer-text b { color: var(--text-strong); font-size: var(--fs-base); }

/* A paid-only surface's row while the reader's plan cannot enter: inert, its
   own content stepped back, and a badge saying why at full strength. */
.dash-drawer-item--locked {
    cursor: default;
}

.dash-drawer-item--locked .dash-drawer-icon,
.dash-drawer-item--locked .dash-drawer-text {
    opacity: 0.45;
}

/* No row space taken, allowed to sit over the description, on its own panel
   fill so it stays readable. */
.dash-drawer-badge {
    position: absolute;
    top: 50%;
    right: 0.6rem;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    gap: 0.28rem;
    padding: 0.14rem 0.55rem;
    border: 1px solid var(--ui-line);
    border-radius: 999px;
    background: var(--ui-panel);
    font-size: var(--fs-caption);
    line-height: var(--lh-caption);
    color: var(--ui-quiet);
    white-space: nowrap;
}
/* A count, not a caption: a solid ground with white text, the way every other
   status fill in the app is drawn. */
.dash-drawer-badge--count {
    min-width: 1.35rem;
    justify-content: center;
    border-color: transparent;
    background: var(--fill-info);
    color: #fff;
    font-variant-numeric: tabular-nums;
}

.dash-drawer-text span { font-size: var(--fs-sm); color: var(--text-muted); }


/* ================================================================
 * Environment adaptations
 * ================================================================ */

/* Touch: only the boxes a finger must hit dead-on grow. Fields keep their size;
   the viewport tag is what stops iOS zooming into them. */
@media (pointer: coarse) {
    .option-toggle input,
    .select-bar .select-check input {
        width: 1.25rem;
        height: 1.25rem;
    }
}

/* Increased contrast: hairline borders that sit under 3:1 step up, per HIG's
   increased-contrast variant guidance. */
@media (prefers-contrast: more) {
    :root {
        --border-subtle: var(--gray-400);
        --ui-line: var(--gray-400);
        --ui-field-line: var(--gray-500);
    }
}


/* Below this the bar's own words no longer fit between the edges. Nothing is
   dropped, everything tightens. */
@media (max-width: 420px) {
    .site-nav, .dash-nav {
        gap: .5rem;
        padding-inline: .75rem;
    }

    .site-nav-links, .dash-links {
        gap: .6rem;
        font-size: var(--fs-sm);
    }

    .site-nav-auth {
        gap: .6rem;
    }

    .site-brand, .dash-brand {
        font-size: var(--fs-sm);
    }

    .site-brand .site-mark, .dash-brand .dash-logo {
        width: 1rem;
        height: 1rem;
    }
}

@media (max-width: 380px) {
    /* Signed out the bar carries two more words and they no longer fit: the
       mark stands in for the wordmark, which stays for anyone reading aloud. */
    .site-brand > span, .dash-brand > span {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    /* Carrying the name on its own, it is the site's whole signature here. */
    .site-brand .site-mark, .dash-brand .dash-logo {
        width: 1.7rem;
        height: 1.7rem;
    }
}


/* The actions on the named thing, as one group. */
.crumb-acts {
    flex: none;
    display: flex;
    align-items: center;
    gap: .5rem;
}

/* Narrow, path and actions no longer share a line: the actions take their own,
   the name keeps a floor. Stated after `.crumb`/`.crumb-title` so it wins. */
@media (max-width: 34rem) {
    .crumb {
        flex-wrap: wrap;
        row-gap: .5rem;
    }

    /* A floor to shrink to. The group below takes the whole next row, so what
       is left of this one is the name's. */
    .crumb-title {
        flex: 1 1 8rem;
    }

    /* All of them together, never one by one: the group takes the whole second
       row and sits at its end. */
    .crumb-acts {
        width: 100%;
        justify-content: flex-end;
    }

    /* Nothing left to push apart once the row is two rows. */
    .crumb-spacer {
        display: none;
    }
}


/* On a phone the logo and name stack: the logo keeps its size, the name gets
   the whole width. Stated at the end of the file, after every rule it overrides. */
@media (max-width: 34rem) {
    .app-header {
        flex-direction: column;
        align-items: flex-start;
        gap: .9rem;
    }

    .app-header--has-banner {
        padding: 1.25rem 1.1rem;
    }

    .app-logo {
        width: 4.5rem;
        height: 4.5rem;
    }
}


/* A share card as a service draws it. The palette is the card's own, driven by
   the preview's switch: a card is read where it was sent, not where it was made. */
.card-set {
    --c-panel: #ffffff;
    --c-line: #e3e6ea;
    --c-ink: #111827;
    --c-body: #4b5563;
    --c-quiet: #6b7280;
    --c-link: #2f6fdb;
    --c-sunken: #eef0f3;
    --c-rail: #c8cfd8;
    max-width: 22rem;
    margin-inline: auto;
}

.card-set[data-mode="dark"] {
    --c-panel: #1a1d21;
    --c-line: #2b3038;
    --c-ink: #e9ecf1;
    --c-body: #a9b1bd;
    --c-quiet: #8b93a1;
    --c-link: #6ea8ff;
    --c-sunken: #23262c;
    --c-rail: #3c434e;
}

/* Whose card this is: the service's own mark, then its name. Heavier than a
   caption, so the label reads apart from the card's own words below it. */
.card-name {
    font-size: var(--fs-sm);
    line-height: var(--lh-sm);
    font-weight: 800;
    color: var(--ui-dim);
}

/* Between two services on one label, at the weight of a separator rather than
   a name. */
.card-dot {
    font-weight: 400;
    color: var(--ui-quiet);
}

.card-mark {
    flex: none;
    width: .95rem;
    height: .95rem;
    display: inline-flex;
}

.card-shape {
    border: 1px solid var(--c-line);
    border-radius: 10px;
    overflow: hidden;
    background: var(--c-panel);
    color: var(--c-ink);
}

.card-shot {
    display: block;
    width: 100%;
    aspect-ratio: 1.91 / 1;
    object-fit: cover;
    background: var(--c-sunken);
}

.card-text {
    display: grid;
    gap: .1rem;
    padding: .5rem .6rem;
    font-size: var(--fs-sm);
}

/* Weightless, so the link colour below beats them where a client uses one:
   these are only what the three lines look like by default. */
:where(.card-text) b {
    font-weight: 600;
    overflow-wrap: anywhere;
}

:where(.card-text) span {
    font-size: var(--fs-caption);
    color: var(--c-body);
    overflow-wrap: anywhere;
}

:where(.card-text) i {
    font-size: var(--fs-caption);
    font-style: normal;
    color: var(--c-quiet);
}

/* Whatever the client draws in its link colour: the address in KakaoTalk, the
   title in Slack, Discord and Telegram. */
.card-link {
    color: var(--c-link);
}

/* Facebook says the host in small capitals above the name. */
.card-host {
    font-size: var(--fs-caption);
    letter-spacing: .04em;
    text-transform: uppercase;
}

/* A name and a host, with nothing between them. */
.card-text--tight {
    gap: 0;
}

/* Slack and its kin stand the words beside a rule, with the picture under them
   rather than above. */
.card-shape--rail {
    display: grid;
    gap: .5rem;
    padding: .55rem .6rem;
    border-left: 3px solid var(--c-rail);
}

.card-shape--rail .card-text {
    min-width: 0;
    padding: 0;
}

/* Held inside the rule rather than run to the card's edges: these three inset
   the picture within the quoted block. */
.card-shape--rail .card-shot {
    border-radius: 6px;
}

/* X with a picture: the picture is the card. The headline and the words are
   dropped and only the domain rides in the corner. */
.card-shape--plate {
    position: relative;
}

.card-stamp {
    position: absolute;
    left: .5rem;
    bottom: .5rem;
    padding: .05rem .35rem;
    border-radius: 4px;
    background: rgb(0 0 0 / .65);
    color: #fff;
    font-size: var(--fs-caption);
}

/* iMessage with no picture: the name and host beside the site's own icon. No
   icon leaves the plate empty, as the client draws it. */
.card-shape--strip {
    display: flex;
    align-items: center;
    gap: .55rem;
    padding: .5rem .6rem;
}

.card-shape--strip .card-text {
    min-width: 0;
    padding: 0;
}

.card-plate {
    flex: none;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 6px;
    background: var(--c-sunken);
    object-fit: cover;
}

/* ================================================================
 * Share surface (components/ui/ShareSheet.tsx)
 * ================================================================ */

/* One surface, two shapes. The scrim is the sheet's ground and the popover's
   outside-press catcher, so it covers the window in both; painted only for the sheet. */
.share-scrim {
    position: fixed;
    inset: 0;
    z-index: 1100;
}

.share-scrim--leaving {
    opacity: 0;
    transition: opacity .2s ease;
    pointer-events: none;
}

.share-scrim--sheet {
    background: rgb(10 12 16 / .42);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
}

.share {
    position: fixed;
    display: flex;
    flex-direction: column;
    /* Portalled to `body`, so the page's own size does not reach it. */
    font-size: var(--fs-body);
    line-height: var(--lh-body);
    background: var(--ui-panel);
    border: 1px solid var(--ui-line);
    box-shadow: 0 8px 24px rgb(0 0 0 / .18);
}

.share:focus-visible {
    outline: none;
}

/* Anchored beside the button that opened it. The list is what scrolls, so the
   name and the code stay put. */
.share--popover {
    width: 16.5rem;
    max-height: min(26rem, calc(100vh - 5rem));
    padding: .25rem;
    border-radius: .45rem;
}

/* Up from the bottom edge, clear of the home indicator. */
.share--sheet {
    left: 0;
    right: 0;
    bottom: 0;
    /* Centred once the window is wider than a sheet wants to be. */
    max-width: 30rem;
    margin-inline: auto;
    padding: .25rem .35rem calc(.7rem + env(safe-area-inset-bottom, 0px));
    border-bottom: 0;
    border-radius: 16px 16px 0 0;
    /* Coming, going and springing back are one transform on one transition: a
       keyframed entrance restarts whenever a class changes. */
    transition: transform .24s cubic-bezier(.32, .72, .35, 1);
}

/* Under the finger it tracks exactly, with nothing easing behind it. */
.share--pulled {
    transition: none;
}

/* The handle is for dragging a sheet, only that: wide and tall enough for a
   thumb, only the bar shows. Shared by both sheets. */
.sheet-grab {
    flex: none;
    padding: .5rem 0 .45rem;
    cursor: grab;
    touch-action: none;
}

.sheet-grab:active {
    cursor: grabbing;
}

.sheet-grab i {
    display: block;
    width: 2rem;
    height: 3px;
    margin: 0 auto;
    border-radius: 999px;
    background: var(--ui-line-strong);
}

/* What is being shared, said once: a sheet that covers the list has to name
   which row it came from. */
.share-head {
    flex: none;
    padding: .3rem .45rem .4rem;
}

.share--sheet .share-head {
    padding-inline: .55rem;
}

.share-title {
    display: block;
    font-size: var(--fs-body);
    font-weight: 600;
    line-height: var(--lh-body);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.share-link {
    margin-top: .2rem;
}

.share-link code {
    flex: 1;
    min-width: 0;
    font-family: var(--font-mono);
    font-size: var(--fs-caption);
    color: var(--ui-dim);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.share-rule {
    flex: none;
    height: 1px;
    margin: 0 .45rem .25rem;
    background: var(--ui-line);
}

.share--sheet .share-rule {
    margin-inline: .55rem;
}

.share-targets {
    min-height: 0;
    overflow-y: auto;
}

/* A phone shows them as a grid of marks; a pointer reads a list of names. */
/* The count follows the width: a cell never goes under 4.5rem (labels break)
   and the grid stops at 26rem, so a wide phone gets more columns, not wider ones. */
.share--sheet .share-targets {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(4.5rem, 1fr));
    /* Both: the width is the sheet's; without it an auto margin sizes the grid
       to its content, which overflowed a narrow sheet. */
    width: 100%;
    max-width: 26rem;
    margin-inline: auto;
    gap: .35rem .1rem;
    padding: .3rem 0 .2rem;
}

.share-target {
    display: flex;
    align-items: center;
    gap: .5rem;
    width: 100%;
    padding: .35rem .45rem;
    border: 0;
    border-radius: .3rem;
    background: transparent;
    color: var(--ui-ink);
    font-family: inherit;
    font-size: var(--fs-sm);
    text-align: left;
    cursor: pointer;
}

.share-target:hover {
    background: var(--ui-hover);
}

.share--sheet .share-target {
    flex-direction: column;
    gap: .3rem;
    padding: .45rem .15rem;
    font-size: var(--fs-caption);
    text-align: center;
}

.share-glyph {
    flex: none;
    width: 1.5rem;
    height: 1.5rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 7px;
    background: var(--ui-sunken);
    font-size: var(--fs-caption);
    font-weight: 700;
}

.share--sheet .share-glyph {
    width: 2.15rem;
    height: 2.15rem;
    font-size: var(--fs-sm);
}

.share-glyph svg {
    width: .9rem;
    height: .9rem;
}

/* The code is how a link reaches another device. On the device itself there is
   nothing to scan it with, so the sheet does not carry one. */
.share-qr {
    flex: none;
    margin-top: .25rem;
    padding: .55rem .45rem .15rem;
    border-top: 1px solid var(--ui-line);
    text-align: center;
}

.share-qr img {
    width: 4.75rem;
    height: 4.75rem;
    padding: 5px;
    border-radius: 8px;
    background: #fff;
}

.share-qr p {
    margin: .35rem 0 0;
}

/* ================= the reference assistant ================= */
/* Docked, never a route: the reader keeps the passage beside the answer. The
   launcher and panel share one corner, so the panel grows out of the button. */
/* The buttons that stand over the page: this panel's launcher, its
   jump-to-newest, and the deep-link outputs button share one --ui-* skin. */
.float-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--ui-line);
    background: var(--ui-panel);
    color: var(--ui-ink);
    font-family: inherit;
    cursor: pointer;
    transition: border-color .15s, background .15s;
}

.float-btn:hover {
    border-color: var(--ui-line-strong);
    background: var(--ui-hover);
}

/* The panel's two are round; the editor's is a pill. */
.chat-float {
    padding: 0;
    border-radius: 50%;
}

/* Blocked: an inline svg sits on the text baseline and keeps a descender's
   worth of room under it, which lifts the glyph off the button's centre. */
.chat-float svg {
    display: block;
}

.chat-launcher {
    position: fixed;
    right: 1.25rem;
    bottom: 1.25rem;
    /* One below the panel: as a sheet the panel reaches the bottom edge, and
       at equal z-index only DOM order kept the launcher out of the composer. */
    z-index: 59;
    width: 3rem;
    height: 3rem;
    box-shadow: 0 6px 20px rgb(0 0 0 / .18);
}

.chat-launcher svg {
    width: 1.35rem;
    height: 1.35rem;
}

.chat-panel {
    position: fixed;
    right: 1.25rem;
    bottom: 1.25rem;
    z-index: 60;
    display: flex;
    flex-direction: column;
    width: min(30rem, calc(100vw - 2.5rem));
    /* A window has a size; pinning top and bottom made it as tall as the
       screen. It grows with the viewport only until it has room to read in. */
    height: min(44rem, calc(100vh - 2.5rem));
    border: 1px solid var(--border-subtle);
    border-radius: 0.75rem;
    background: var(--bg-panel);
    box-shadow: 0 12px 32px rgb(0 0 0 / .22);
    overflow: hidden;
    /* Closed it is not merely invisible: it must not take a tab stop. */
    visibility: hidden;
    opacity: 0;
    transform: translateY(.5rem);
    transition: opacity .16s, transform .16s, visibility .16s,
        left .18s ease, top .18s ease, width .18s ease, height .18s ease;
}

/* Under the pointer the frame is the hand's: neither size nor position may
   ease, or the sheet trails the thumb. */
.chat-panel--held {
    transition: opacity .16s, visibility .16s;
}

.chat-panel.open {
    visibility: visible;
    opacity: 1;
    transform: none;
}

.chat-head {
    flex: none;
    /* Whole pixels at the default root: a 12px circle straddling the pixel
       grid antialiases unevenly and the traffic dots read as off-centre. */
    padding: .75rem .875rem;
    border-bottom: 1px solid var(--border-muted);
    /* The header is the handle. */
    cursor: grab;
    user-select: none;
}

.chat-head:active {
    cursor: grabbing;
}

/* On a phone the panel is a bottom sheet. The handle sizes it and, pulled far
   enough, closes it, so the window controls are not drawn. */
.chat-panel--sheet {
    right: 0;
    left: 0;
    top: auto;
    bottom: 0;
    width: auto;
    border-width: 1px 0 0;
    border-radius: 16px 16px 0 0;
    padding-bottom: env(safe-area-inset-bottom, 0px);
    transition: opacity .16s, visibility .16s,
        transform .24s cubic-bezier(.32, .72, .35, 1),
        height .24s cubic-bezier(.32, .72, .35, 1);
}

.chat-panel--sheet.chat-panel--held {
    transition: opacity .16s, visibility .16s;
}

/* Minimized is the window's word; a sheet is sized by its handle. */
.chat-panel--sheet .chat-head {
    cursor: default;
}

/* Minimized, the panel is its header: the body keeps its state, just not shown.
   Doubled class: it must out-rank the phone sheet's later equal-specificity `bottom`. */
.chat-panel.chat-panel--min {
    height: auto;
    top: auto;
    bottom: 1.25rem;
}

@media (max-width: 30rem) {
    .chat-panel.chat-panel--min {
        bottom: .75rem;
    }
}

.chat-panel--min .chat-stage,
.chat-panel--min .chat-compose {
    display: none;
}

.chat-panel--min .chat-head {
    border-bottom: 0;
}

/* The window controls in the shape every desktop knows. The colours are the
   metaphor itself, so they stay literal; glyphs appear on group hover, macOS-style. */
.traffic {
    display: inline-flex;
    gap: 0.5rem;
}

.traffic-dot {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 12px;
    height: 12px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    cursor: pointer;
}

.traffic-dot--close { background: #ff5f57; }
.traffic-dot--min   { background: #febc2e; }
.traffic-dot--max   { background: #28c840; }

.traffic-dot svg {
    /* Block, or the inline baseline lifts the glyph off the dot's centre —
       the same trap .chat-float svg documents. */
    display: block;
    width: 8px;
    height: 8px;
    fill: none;
    stroke: rgb(0 0 0 / .55);
    stroke-width: 1.4;
    stroke-linecap: round;
    opacity: 0;
    transition: opacity .12s;
}

.traffic:hover .traffic-dot svg,
.traffic-dot:focus-visible svg {
    opacity: 1;
}

.traffic-dot svg.tri {
    fill: rgb(0 0 0 / .55);
    stroke: none;
}

/* The other grammar, for desktops that are not Macs: square corner buttons,
   glyphs always drawn, close last and alone in taking a colour. */
.chat-head-end {
    /* So the caption can reach the header's full height. */
    align-self: stretch;
    flex-wrap: nowrap;
}

/* A caption button is only a window control if it reaches the corner: the
   header gives up the padding on that side and the title carries its own. */
.chat-head--win {
    padding-top: 0;
    padding-bottom: 0;
    padding-right: 0;
}

.chat-head--win .chat-title {
    padding: .75rem 0;
}

.caption {
    display: inline-flex;
    align-self: stretch;
}

.cap {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    align-self: stretch;
    /* Windows draws these 46 wide. Three of those would take a third of this
       panel's header, so they are stepped down and keep only the proportion. */
    width: 40px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--ui-ink);
    cursor: pointer;
    transition: background .12s, color .12s;
}

.cap:hover {
    background: var(--ui-hover);
}

/* The only fixed colour here: white on Fluent's close red, which reads the
   same on either theme. */
.cap--close:hover {
    background: #c42b1c;
    color: #fff;
}

/* Inward, or the panel's own overflow clips the ring off the corner button. */
.cap:focus-visible {
    outline-offset: -3px;
}

.cap svg {
    /* Block, for the reason .traffic-dot svg gives. */
    display: block;
    width: 10px;
    height: 10px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.1;
}


/* The edges and corners of a resizable panel: thin grab areas wearing the
   cursor for their direction. */
.panel-edge {
    position: absolute;
    z-index: 2;
}

.panel-edge--n, .panel-edge--s { left: 8px; right: 8px; height: 6px; cursor: ns-resize; }
.panel-edge--e, .panel-edge--w { top: 8px; bottom: 8px; width: 6px; cursor: ew-resize; }
.panel-edge--n { top: -3px; }
.panel-edge--s { bottom: -3px; }
.panel-edge--e { right: -3px; }
.panel-edge--w { left: -3px; }

.panel-edge--ne, .panel-edge--nw, .panel-edge--se, .panel-edge--sw {
    width: 10px;
    height: 10px;
}

.panel-edge--ne { top: -3px; right: -3px; cursor: nesw-resize; }
.panel-edge--sw { bottom: -3px; left: -3px; cursor: nesw-resize; }
.panel-edge--nw { top: -3px; left: -3px; cursor: nwse-resize; }
.panel-edge--se { bottom: -3px; right: -3px; cursor: nwse-resize; }

.chat-title {
    font-size: var(--fs-body);
    font-weight: 600;
    color: var(--text-strong);
}

/* Holds the scrolling log and the control that floats over it, so the button
   stays put while the conversation moves under it. */
.chat-stage {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    /* The stage is the log's viewport. Padding does not shrink with a flex
       item, so a squeezed log still stood its own padding tall. */
    overflow: hidden;
}

/* Only there while the reader is away from the end. It fades rather than
   appearing, and stays out of the tab order while it is gone. */
.chat-to-end {
    position: absolute;
    right: .75rem;
    bottom: .6rem;
    width: 1.85rem;
    height: 1.85rem;
    box-shadow: 0 4px 12px rgb(0 0 0 / .16);
    visibility: hidden;
    opacity: 0;
    transition: opacity .16s, visibility .16s, background .15s, border-color .15s;
}

.chat-to-end.on {
    visibility: visible;
    opacity: 1;
}

.chat-to-end svg {
    width: .8rem;
    height: .8rem;
}

.chat-log {
    flex: 1 1 auto;
    /* A flex item will not shrink past its content without this; the log kept
       a padding-tall strip of conversation as the sheet was squeezed shut. */
    min-height: 0;
    overflow-y: auto;
    padding: .9rem;
    display: flex;
    flex-direction: column;
    gap: .5rem;
}

/* Centred in the log the way a fresh chat opens: the hint over a few sample
   questions that fade from one set to the next rather than blink. */
.chat-empty {
    min-height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .9rem;
    margin: 0;
    padding: 1rem;
    text-align: center;
    white-space: pre-line;
    font-size: var(--fs-sm);
    line-height: 1.6;
    color: var(--text-muted);
}

/* The dashboard's StarterPanel reused verbatim: only bounded to the narrow
   panel, and a step down in type — the dashboard size is too loud here. */
.chat-empty .starter {
    align-items: center;
    text-align: center;
}

.chat-empty .starter-lead {
    font-size: var(--fs-sm);
}

.chat-empty .starter-try {
    font-size: var(--fs-sm);
}

/* The slot follows each question's width. Clip vertically only (the roll): a
   wider name overflows into view while the width eases, never clipped. */
.chat-empty .starter-window {
    max-width: 100%;
    overflow: visible;
    clip-path: inset(0 -100vw);
}

.chat-turn {
    max-width: 90%;
    margin: 0;
    padding: .45rem .65rem;
    border-radius: .55rem;
    font-size: var(--fs-sm);
    line-height: 1.55;
    user-select: text;
    -webkit-user-select: text;
}

.chat-turn--user {
    align-self: flex-end;
    white-space: pre-wrap;
    background: var(--fill-info);
    color: #fff;
}

.chat-turn--assistant {
    align-self: flex-start;
    border: 1px solid var(--border-subtle);
    background: var(--bg-elev);
    color: var(--text-body);
}

/* The work behind an answer: one line that ticks and settles, unfolding to the
   lookups. Kept with the turn, so a reopened conversation reads the same. */
.chat-work {
    margin-bottom: .1rem;
}

.chat-think {
    display: flex;
    align-items: center;
    gap: .2rem;
    margin: 0 0 .35rem;
    padding: 0;
    border: 0;
    background: none;
    font-family: inherit;
    font-size: var(--fs-caption);
    line-height: var(--lh-caption);
    text-align: left;
    color: var(--text-muted);
}

button.chat-think {
    cursor: pointer;
}

button.chat-think:hover {
    color: var(--text-body);
}

.chat-steps {
    margin: 0 0 .4rem;
    padding: 0 0 0 1.2rem;
    list-style: none;
    font-size: var(--fs-caption);
    line-height: 1.5;
    color: var(--text-muted);
}

/* Where the answer came from. Every link lands on a passage of the reference,
   so a claim can be checked rather than trusted. */
/* Everything after an answer — sources, verdict, follow-ups — behind one
   divider. `--ui-line-strong`: the muted hairline read as a rendering artefact. */
/* One suggestion per line: a question is a sentence, and side-by-side they
   wrapped into a jumble nobody could scan. */
.chat-after .chip-row {
    flex-direction: column;
    align-items: flex-start;
}

.chat-after {
    margin-top: 0.6rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--gray-300);
}

/* The verdict sits under the answer as a quiet afterthought, not a toolbar. */
.chat-verdict {
    margin: 0.25rem 0 0;
}

.chat-verdict .icon-btn svg {
    width: 13px;
    height: 13px;
}

.chat-sources {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .3rem;
    /* The strip above carries the divider now. */
    margin: 0;
    font-size: var(--fs-caption);
}

.chat-sources-label {
    color: var(--text-muted);
}

.chat-sources a {
    font-family: var(--font-mono);
    color: var(--accent);
}

/* Shortcuts to where a cited feature works, on their own line beneath the
   sources: named, not mono, and underlined as a go-to rather than a citation. */
.chat-opens {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .5rem;
    margin: .35rem 0 0;
    font-size: var(--fs-caption);
}

.chat-opens a {
    color: var(--accent);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.chat-failure {
    margin: 0;
    align-self: stretch;
    padding: .45rem .65rem;
    border: 1px solid var(--ui-bad-line);
    border-radius: .45rem;
    font-size: var(--fs-sm);
    line-height: 1.5;
    white-space: pre-line;
    color: var(--ui-bad-ink);
}

/* An answer's own prose and lists. The code blocks inside are the reference's
   own `.docs-code`, so a sample reads the same here as on the page. */
.answer-line {
    margin: 0 0 .5rem;
    white-space: pre-wrap;
}

.answer-head {
    margin: .8rem 0 .35rem;
    font-size: var(--fs-body);
    line-height: var(--lh-body);
    font-weight: 700;
    color: var(--text-strong);
}

.answer-head:first-child {
    margin-top: 0;
}

/* What an answer writes as `---`. A rule, not a border on the block above: the
   break belongs between two passages rather than to either of them.

   Mixed into the ground rather than named as a line token, because none of them
   lands in the middle here: against the turn's raised ground the hairlines sit
   4 to 6 units off it and read as a rendering artefact, and `--ui-line-strong`
   sits 85 to 91 off and shouts over the prose it separates. A share of the ink
   is the same distance in both themes by construction. */
.answer-rule {
    margin: .9rem 0;
    border: 0;
    border-top: 1px solid color-mix(in srgb, var(--ui-ink) 14%, transparent);
}

.chat-turn .ui-table-wrap {
    margin: 0 0 .5rem;
}

.answer-list {
    margin: 0 0 .5rem;
    padding-left: 1.1rem;
}

.chat-turn :is(.answer-line, .answer-head, .answer-list, .ui-table-wrap):last-child {
    margin-bottom: 0;
}

.chat-turn .docs-code {
    margin: .5rem 0;
}

/* Dictation: while open the microphone wears the turn's remaining time as a
   ring. `--gauge` is written by the panel, a readout reduce-motion must not flatten. */
.mic-btn {
    position: relative;
    flex: none;
    /* Round at rest too: the listening ring is a circle, and a hover ground
       with different corners reads as two controls. Shape never changes on state. */
    border-radius: 50%;
}

.mic-btn.on {
    color: var(--fill-bad);
}

/* Recording keeps its colour under the pointer: hover says "you can press
   this", not "this is no longer running". */
.mic-btn.on:hover {
    color: var(--fill-bad);
}

.mic-btn.on::before {
    content: "";
    position: absolute;
    inset: -1px;
    border-radius: 50%;
    background: conic-gradient(var(--fill-bad) calc(var(--gauge, 0) * 360deg), var(--border-subtle) 0);
    /* Hollow, so the icon is never sitting on a disc. */
    mask: radial-gradient(closest-side, transparent 76%, #000 77%);
    -webkit-mask: radial-gradient(closest-side, transparent 76%, #000 77%);
}

/* Heard but not settled on. Kept apart from the box so the reader can watch it
   being reconsidered without it touching what they typed. */
.chat-heard {
    margin: 0;
    font-size: var(--fs-sm);
    line-height: var(--lh-sm);
    color: var(--text-faint);
    font-style: italic;
}

/* Its own layer, above the log: the jump-to-end button and sticky code bar
   float in the stage and at a sheet's height reached over the composer. */
.chat-compose {
    position: relative;
    z-index: 1;
    flex: none;
    padding: .7rem .9rem .9rem;
    border-top: 1px solid var(--border-muted);
    background: var(--bg-panel);
}

.chat-compose textarea {
    resize: none;
}

.chat-note {
    flex: 1 1 auto;
    min-width: 0;
    font-size: var(--fs-caption);
    line-height: 1.45;
    white-space: pre-line;
    color: var(--text-muted);
}

@media (max-width: 30rem) {
    .chat-launcher {
        right: .75rem;
        bottom: .75rem;
    }
}

/* ================================================================
 * The Guardian challenge (components/auth/GuardCaptcha.tsx)
 *
 * Lives here rather than with the sign-in screen: the same component is
 * shown by the deep-link visit page and by the admin's Guardian tab, and a
 * stylesheet only one of them imports leaves the others drawing nothing.
 * The image plays despite prefers-reduced-motion: it must stay solvable.
 * ================================================================ */
/* The picture sets the width and the field takes it: the two are one column,
   not a centred picture with a wider field under it. The picture itself is
   never scaled to fit — see below. */
/* The picture takes the width it is given and the field takes the same: the two
   are one column, and the dialog decides how wide that is. */
.guard-captcha {
    width: 100%;
    padding-top: 0.8rem;
}

/* A running total: each line contributes, the last one sums. Three columns so
   the amounts line up down the right edge however long the names are. */
.ledger {
    display: grid;
    grid-template-columns: max-content 1fr max-content;
    align-items: baseline;
    gap: 0.3rem 0.75rem;
    font-size: var(--fs-sm);
    line-height: var(--lh-sm);
}

.ledger-row {
    display: contents;
}

.ledger-row > b {
    font-variant-numeric: tabular-nums;
    text-align: right;
}

/* One rule across all three columns; a border on each cell would break at the
   column gaps. `--ui-line-strong`, because the subtle hairline reads as a
   rendering artefact rather than a divider. */
.ledger-rule {
    grid-column: 1 / -1;
    margin-top: 0.2rem;
    border-top: 1px solid var(--ui-line-strong);
}


/* Native size, never scaled: the letters read from 1px/frame noise, and any
   browser resampling aliases that motion into unreadable shimmer. */
/* Neither picture is a thing to take away: dragging one out, long-pressing it
   for a save-image menu or selecting around it all hand the challenge to
   something other than the person answering it. */
.guard-captcha-image,
.guard-pick-image {
    -webkit-user-drag: none;
    -webkit-touch-callout: none;
    user-select: none;
}

.guard-captcha-image {
    display: block;
    width: 100%;
    height: auto;
    margin: 0 0 0.6rem;
    border-radius: 0.5rem;
    background: #0d1117;
    image-rendering: pixelated;
}

/* The bone standing in while a new image is minted; width and height are set
   inline from the image's measured box, so nothing shifts. */
.guard-captcha-bone {
    margin: 0 0 0.6rem;
    border-radius: 0.5rem;
}

.guard-captcha-input {
    text-align: center;
    letter-spacing: 0.2em;
    text-transform: uppercase;
}

/* ---- the click task ------------------------------------------------ */

.guard-question {
    margin: 0 0 0.1rem;
    color: var(--text-strong);
}

/* The mark drawn beside its name, centred on the line rather than sat on the
   baseline: it is a legend, not a letter. `middle` aligns to half the font's
   x-height, which leaves it 0.08em low against Hangul, so the rest is measured
   off the ink itself. */
.guard-question svg {
    position: relative;
    top: -0.08em;
    margin-left: 0.2em;
    vertical-align: middle;
}

/* Holds the question's skeleton off the picture; its width is the component's.
   The sentence under the picture needs no rule: it stands in a stack. */
.guard-question-bone {
    margin-bottom: 0.35rem;
}

/* The colour asked for, shown as itself: a colour word is the one thing a
   colour-blind reader cannot match back to the picture. Ringed in white and
   then in black rather than bordered — the palette runs dark, and a dark swatch
   on a dark panel with a dark border is a hole in the sentence. The pair of
   rings reads on either ground, and neither eats into the colour. */
.guard-swatch {
    display: inline-block;
    width: 0.85em;
    height: 0.85em;
    border-radius: 3px;
    vertical-align: -0.08em;
    box-shadow: 0 0 0 1px rgb(255 255 255 / .92), 0 0 0 2px rgb(17 24 39 / .55);
}

/* The picture is the control: a press is a place on it, sent as it was made.
   Nothing here knows how the server groups those, so nothing here can leak it. */
.guard-pick {
    position: relative;
    display: block;
    width: 100%;
    margin: 0 0 0.6rem;
    line-height: 0;
    /* A mark near an edge is half outside the picture; the picture is the
       whole of the thing being answered, so it is where the marks end too. */
    overflow: hidden;
    border-radius: 0.5rem;
}

.guard-pick-image {
    display: block;
    width: 100%;
    height: auto;
    border-radius: 0.5rem;
    border: 1px solid var(--border-subtle);
    cursor: crosshair;
    touch-action: manipulation;
}

/* The border rounds the corners, so the clipping box has to sit inside it or a
   mark in a corner shows through the curve. */
.guard-pick::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 0.5rem;
    border: 1px solid var(--border-subtle);
    pointer-events: none;
}

/* Exactly where the finger landed: a ring, so it says "here" without covering
   the mark the choice was about. Dark, and edged in white on both sides, so it
   reads over the pale ground and over a dark shape alike — the picture's own
   colours are chosen to be dark, so a single colour could not do both.
   `--ui-mark` is not it: outside `.surface` that token is near-white. */
.guard-mark {
    position: absolute;
    width: 1.75rem;
    height: 1.75rem;
    margin: -0.875rem 0 0 -0.875rem;
    border-radius: 50%;
    border: 3px solid #111827;
    box-shadow: 0 0 0 2px rgb(255 255 255 / .95), inset 0 0 0 2px rgb(255 255 255 / .95);
    pointer-events: none;
}
