/* ==========================================================================
   Quatro90 Web Interface — design system
   Palette and type are taken from the live Quatro90 portal: indigo #3B3081,
   the violet logo gradient, and lime #84D11F as the signal accent.
   ========================================================================== */

:root {
    color-scheme: light;

    /* Brand constants — identical in every theme. */
    --q-indigo: #3B3081;
    --q-violet: #745EFF;
    --q-violet-light: #8775FF;
    --q-violet-deep: #5142B2;
    --q-lime: #84D11F;
    --q-amber: #FFB020;
    --q-red: #BE1C1C;

    /* Light theme surfaces. */
    --bg: #ECECF3;
    --bg-accent: #E3E2EF;
    --surface: #FFFFFF;
    --surface-sunken: #F6F6FA;
    --text: #14132B;
    --text-soft: #4A4B66;
    --text-muted: #6E6F8C;
    --border: #DFDFEA;
    --border-strong: #C9C9DA;
    --field: #FFFFFF;
    --shadow-card: 0 1px 2px rgb(20 19 43 / 6%), 0 24px 48px -24px rgb(20 19 43 / 22%);
    --focus-ring: 0 0 0 3px rgb(116 94 255 / 32%);

    /* The monitor rail is dark in every theme — a confidence monitor always is. */
    --rail-bg: #0B0A14;
    --rail-panel: #131228;
    --rail-line: #24234A;
    --rail-text: #E9E8F6;
    --rail-muted: #8688AD;

    --radius-card: 20px;
    --radius-field: 10px;
    --radius-pill: 999px;

    /* Named because two things have to agree about it: the bar itself, and the side navigation,
       whose sticky column would otherwise run underneath it. */
    --statusbar-h: 26px;

    /* Zero unless an impersonation is in progress, when .shell--impersonating gives it a height.
       Everything that measures itself against the top of the window adds it, so the strip pushes
       the application down rather than covering the first 34 pixels of it. */
    --impersonating-h: 0px;

    /* The two shapes a content item is drawn as. Amber for a folder is the one convention every
       file explorer already taught everybody, and it is the colour nothing else here uses; the
       shapes are tinted rather than solid so the content type's own icon on top stays the darkest
       thing in the glyph, and so the thing being read first. */
    --glyph-folder: color-mix(in srgb, var(--q-amber) 62%, transparent);
    --glyph-item: color-mix(in srgb, var(--q-violet) 50%, transparent);

    --font-display: 'Manrope', 'Segoe UI', system-ui, sans-serif;
    --font-body: 'Lato', 'Segoe UI', system-ui, sans-serif;
    --font-mono: 'JetBrains Mono', 'Cascadia Mono', ui-monospace, monospace;

    --step-cubic: cubic-bezier(.2, .8, .2, 1);
}

:root[data-theme="dark"] {
    color-scheme: dark;
    --bg: #08070F;
    --bg-accent: #0E0D1C;
    --surface: #14132A;
    --surface-sunken: #100F22;
    --text: #F0EFFA;
    --text-soft: #C3C2DC;
    --text-muted: #8A8BAB;
    --border: #262547;
    --border-strong: #34335C;
    --field: #0E0D1F;
    --shadow-card: 0 1px 2px rgb(0 0 0 / 40%), 0 32px 64px -28px rgb(0 0 0 / 80%);
    --focus-ring: 0 0 0 3px rgb(135 117 255 / 40%);

    /* Both tints carry further here: mixed towards transparent over a near-black surface they
       lose the weight they have over a white one. */
    --glyph-folder: color-mix(in srgb, var(--q-amber) 82%, transparent);
    --glyph-item: color-mix(in srgb, var(--q-violet) 82%, transparent);
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        color-scheme: dark;
        --bg: #08070F;
        --bg-accent: #0E0D1C;
        --surface: #14132A;
        --surface-sunken: #100F22;
        --text: #F0EFFA;
        --text-soft: #C3C2DC;
        --text-muted: #8A8BAB;
        --border: #262547;
        --border-strong: #34335C;
        --field: #0E0D1F;
        --shadow-card: 0 1px 2px rgb(0 0 0 / 40%), 0 32px 64px -28px rgb(0 0 0 / 80%);
        --focus-ring: 0 0 0 3px rgb(135 117 255 / 40%);
        --glyph-folder: color-mix(in srgb, var(--q-amber) 82%, transparent);
        --glyph-item: color-mix(in srgb, var(--q-violet) 82%, transparent);
    }
}

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

html,
body {
    height: 100%;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-body);
    font-size: 14px;
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

:focus-visible {
    outline: 2px solid var(--q-violet);
    outline-offset: 2px;
}

/* ==========================================================================
   Stage — monitor rail beside the auth panel
   ========================================================================== */

.stage {
    display: grid;
    grid-template-columns: minmax(0, 1.02fr) minmax(0, 1fr);
    min-height: 100dvh;
}

/* One column, for the pages an emailed link lands on: there is no monitor to show beside a
   card that exists to spend a key. The panel keeps its own dressing, narrowed and centred. */
.stage--single {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
}

.stage--single .panel {
    width: min(100%, 640px);
}

/* --------------------------------------------------------------------------
   Left: the confidence monitor
   -------------------------------------------------------------------------- */

.rail {
    position: relative;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 40px;
    padding: 32px 36px;
    overflow: hidden;
    background:
        radial-gradient(120% 90% at 8% 0%, rgb(116 94 255 / 26%) 0%, transparent 58%),
        radial-gradient(90% 70% at 100% 100%, rgb(59 48 129 / 46%) 0%, transparent 60%),
        var(--rail-bg);
    color: var(--rail-text);
}

/* Scanlines: a CRT tell, kept almost subliminal. */
.rail::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: repeating-linear-gradient(to bottom, rgb(255 255 255 / 3%) 0 1px, transparent 1px 3px);
    opacity: .5;
    mix-blend-mode: overlay;
}

.rail > * {
    position: relative;
    z-index: 1;
}

.rail__brand {
    display: flex;
    align-items: center;
    gap: 14px;
}

.rail__wordmark {
    display: flex;
    flex-direction: column;
    line-height: 1.1;
}

.rail__name {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 18px;
    letter-spacing: -.02em;
}

.rail__tagline {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--rail-muted);
}

/* The monitor itself. */
.monitor {
    border: 1px solid var(--rail-line);
    border-radius: 16px;
    background: linear-gradient(180deg, rgb(255 255 255 / 4%), transparent 40%), var(--rail-panel);
    box-shadow: inset 0 1px 0 rgb(255 255 255 / 6%), 0 30px 60px -30px rgb(0 0 0 / 90%);
    overflow: hidden;
}

.monitor__bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 14px;
    border-bottom: 1px solid var(--rail-line);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--rail-muted);
}

.monitor__source {
    display: flex;
    align-items: center;
    gap: 8px;
}

.monitor__body {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 18px;
    padding: 18px 16px 14px;
}

.timecode {
    display: block;
    font-family: var(--font-mono);
    font-size: clamp(24px, 3.4vw, 34px);
    font-weight: 500;
    letter-spacing: .01em;
    font-variant-numeric: tabular-nums;
    color: var(--rail-text);
    text-shadow: 0 0 26px rgb(116 94 255 / 45%);
}

.timecode__frames {
    color: var(--q-violet-light);
}

.monitor__label {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--rail-muted);
    margin: 0 0 6px;
}

.monitor__readouts {
    display: flex;
    gap: 26px;
    margin-top: 18px;
}

.readout__value {
    font-family: var(--font-mono);
    font-size: 13px;
    color: var(--rail-text);
}

/* Peak programme meters. Levels come from script via --lvl / --peak. */
.ppm {
    display: flex;
    align-items: flex-end;
    gap: 7px;
}

.ppm__channel {
    position: relative;
    width: 12px;
    height: 112px;
    border-radius: 3px;
    background: #07060E;
    box-shadow: inset 0 0 0 1px var(--rail-line);
    overflow: hidden;
}

.ppm__fill {
    position: absolute;
    inset: auto 0 0 0;
    height: calc(var(--lvl, 0) * 1%);
    background: linear-gradient(to top, var(--q-lime) 0%, var(--q-lime) 62%, var(--q-amber) 82%, var(--q-red) 100%);
    transition: height 90ms linear;
}

.ppm__peak {
    position: absolute;
    left: 0;
    right: 0;
    bottom: calc(var(--peak, 0) * 1%);
    height: 2px;
    background: var(--rail-text);
    opacity: .9;
    transition: bottom 260ms var(--step-cubic);
}

.ppm__scale {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    height: 112px;
    padding-left: 6px;
    font-family: var(--font-mono);
    font-size: 9px;
    color: var(--rail-muted);
}

/* Tally — the auth state, expressed the way a gallery expresses it. */
.tally {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    border-top: 1px solid var(--rail-line);
    background: rgb(0 0 0 / 25%);
}

.tally__lamp {
    width: 11px;
    height: 11px;
    border-radius: 50%;
    background: var(--tally-color, var(--rail-muted));
    box-shadow: 0 0 14px var(--tally-color, transparent);
    flex: none;
}

.tally__state {
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: .2em;
    text-transform: uppercase;
    color: var(--tally-color, var(--rail-muted));
}

.tally__detail {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--rail-muted);
    text-align: right;
    max-width: 58%;
}

.tally[data-state="standby"] {
    --tally-color: #6E7091;
}

.tally[data-state="preroll"] {
    --tally-color: var(--q-amber);
}

.tally[data-state="onair"] {
    --tally-color: var(--q-lime);
}

.tally[data-state="fault"] {
    --tally-color: var(--q-red);
}

.tally[data-state="preroll"] .tally__lamp,
.tally[data-state="fault"] .tally__lamp {
    animation: tally-pulse 1s steps(1, end) infinite;
}

@keyframes tally-pulse {
    50% {
        opacity: .25;
    }
}

.rail__foot {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 20px;
}

.rail__statement {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: clamp(17px, 1.9vw, 21px);
    line-height: 1.2;
    letter-spacing: -.02em;
    margin: 0;
    max-width: 22ch;
}

.rail__statement em {
    font-style: normal;
    color: var(--q-lime);
}

.rail__meta {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--rail-muted);
    text-align: right;
    line-height: 1.8;
}

/* --------------------------------------------------------------------------
   Right: the auth panel
   -------------------------------------------------------------------------- */

.panel {
    position: relative;
    display: flex;
    flex-direction: column;
    padding: 28px 40px 40px;
    background:
        radial-gradient(80% 50% at 100% 0%, var(--bg-accent) 0%, transparent 70%),
        var(--bg);
}

.panel__top {
    display: flex;
    justify-content: flex-end;
}

.panel__center {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px 0;
}

.card {
    width: min(396px, 100%);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-card);
    padding: 26px 26px 24px;
}

.card__eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 12px;
}

.card__eyebrow::before {
    content: "";
    width: 18px;
    height: 1px;
    background: var(--q-violet);
}

.card__title {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 22px;
    letter-spacing: -.025em;
    line-height: 1.15;
    margin: 0 0 6px;
    color: var(--text);
}

.card__lede {
    margin: 0 0 20px;
    color: var(--text-muted);
    font-size: 13.5px;
}

.field {
    margin-bottom: 14px;
}

.field__label {
    display: block;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 7px;
}

.field__row {
    display: flex;
    gap: 12px;
}

.field__row > .field {
    flex: 1;
    margin-bottom: 16px;
}

.input {
    width: 100%;
    height: 40px;
    padding: 0 13px;
    font-family: var(--font-body);
    font-size: 14px;
    color: var(--text);
    background: var(--field);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-field);
    transition: border-color 140ms var(--step-cubic), box-shadow 140ms var(--step-cubic);
}

.input::placeholder {
    color: var(--text-muted);
    opacity: .7;
}

.input:hover {
    border-color: var(--q-violet-light);
}

.input:focus {
    outline: none;
    border-color: var(--q-violet);
    box-shadow: var(--focus-ring);
}

.input[aria-invalid="true"] {
    border-color: var(--q-red);
}

.input--code {
    font-family: var(--font-mono);
    font-size: 19px;
    letter-spacing: .45em;
    text-align: center;
    padding-right: 0;
    text-indent: .5em;
}

.input--key {
    font-family: var(--font-mono);
    font-size: 13px;
    letter-spacing: .02em;
}

.password {
    position: relative;
}

.password .input {
    padding-right: 42px;
}

.password__toggle {
    position: absolute;
    top: 0;
    right: 0;
    width: 40px;
    height: 40px;
    display: grid;
    place-items: center;
    border: 0;
    background: none;
    color: var(--text-muted);
    cursor: pointer;
    border-radius: var(--radius-field);
}

.password__toggle:hover {
    color: var(--q-violet);
}

.hint {
    display: block;
    margin-top: 7px;
    font-size: 12.5px;
    color: var(--text-muted);
}

.hint--warn {
    color: var(--q-amber);
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: .08em;
    text-transform: uppercase;
}

.hint--error {
    color: var(--q-red);
}

/* Caps lock is detected in script and flagged with a body class, so noticing it costs
   the circuit nothing. */
.caps-hint {
    display: none;
}

body.q90-caps .caps-hint {
    display: block;
}

/* Password strength — four segments, because a bar with no scale says nothing. */
.strength {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 9px;
}

.strength__track {
    display: flex;
    gap: 4px;
    flex: 1;
}

.strength__segment {
    height: 3px;
    flex: 1;
    border-radius: 2px;
    background: var(--border-strong);
    transition: background 200ms var(--step-cubic);
}

.strength__segment[data-on="true"] {
    background: var(--strength-color, var(--q-lime));
}

.strength__label {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--strength-color, var(--text-muted));
    min-width: 68px;
    text-align: right;
}

.button {
    width: 100%;
    height: 42px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: -.01em;
    color: #fff;
    background: linear-gradient(135deg, var(--q-violet) 0%, var(--q-violet-deep) 100%);
    border: 0;
    border-radius: var(--radius-field);
    cursor: pointer;
    transition: transform 140ms var(--step-cubic), box-shadow 140ms var(--step-cubic), filter 140ms;
    box-shadow: 0 10px 24px -12px rgb(81 66 178 / 90%);
}

.button:hover:not(:disabled) {
    transform: translateY(-1px);
    box-shadow: 0 16px 30px -14px rgb(81 66 178 / 95%);
    filter: brightness(1.06);
}

.button:active:not(:disabled) {
    transform: translateY(0);
}

.button:disabled {
    cursor: progress;
    filter: saturate(.5) brightness(.9);
}

.button__spinner {
    width: 15px;
    height: 15px;
    border: 2px solid rgb(255 255 255 / 35%);
    border-top-color: #fff;
    border-radius: 50%;
    animation: spin .7s linear infinite;
}

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

.link {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    font-size: 13.5px;
    color: var(--q-violet);
    cursor: pointer;
    text-decoration: none;
    border-radius: 4px;
}

.link:hover {
    text-decoration: underline;
}

.card__actions {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    margin-top: 18px;
    padding-top: 16px;
    border-top: 1px solid var(--border);
    font-size: 13.5px;
    color: var(--text-muted);
    flex-wrap: wrap;
}

.card__actions--split {
    justify-content: space-between;
}

/* Feedback banner inside the card. */
.notice {
    display: flex;
    gap: 11px;
    align-items: flex-start;
    padding: 12px 14px;
    border-radius: var(--radius-field);
    font-size: 13px;
    margin-bottom: 16px;
    border: 1px solid transparent;
}

.notice__icon {
    flex: none;
    width: 18px;
    height: 18px;
    margin-top: 1px;
}

.notice[data-tone="error"] {
    background: color-mix(in srgb, var(--q-red) 10%, var(--surface));
    border-color: color-mix(in srgb, var(--q-red) 34%, transparent);
    color: color-mix(in srgb, var(--q-red) 78%, var(--text));
}

.notice[data-tone="success"] {
    background: color-mix(in srgb, var(--q-lime) 14%, var(--surface));
    border-color: color-mix(in srgb, var(--q-lime) 40%, transparent);
    color: color-mix(in srgb, var(--q-lime) 52%, var(--text));
}

.notice[data-tone="info"] {
    background: color-mix(in srgb, var(--q-violet) 10%, var(--surface));
    border-color: color-mix(in srgb, var(--q-violet) 30%, transparent);
    color: color-mix(in srgb, var(--q-violet) 62%, var(--text));
}

/* Shown while we work out whether a stored session is still usable. */
.resuming {
    display: flex;
    align-items: center;
    gap: 11px;
    margin: 0;
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.resuming .button__spinner {
    border-color: var(--border-strong);
    border-top-color: var(--q-violet);
}

/* View transition when the card swaps between sign in / register / reset. */
.view {
    animation: view-in 320ms var(--step-cubic) both;
}

@keyframes view-in {
    from {
        opacity: 0;
        transform: translateY(6px);
    }
}

/* Theme switch — three states, because "auto" is a real choice. */
.themeswitch {
    display: inline-flex;
    padding: 3px;
    gap: 2px;
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
}

.themeswitch__option {
    width: 34px;
    height: 28px;
    display: grid;
    place-items: center;
    border: 0;
    background: none;
    border-radius: var(--radius-pill);
    color: var(--text-muted);
    cursor: pointer;
    transition: background 160ms var(--step-cubic), color 160ms var(--step-cubic);
}

.themeswitch__option:hover {
    color: var(--text);
}

.themeswitch__option[aria-checked="true"] {
    background: var(--surface);
    color: var(--q-violet);
    box-shadow: 0 1px 3px rgb(20 19 43 / 14%);
}

/* ==========================================================================
   Signed-in shell — top bar, tenant picker, side navigation
   ========================================================================== */

.shell {
    display: grid;
    grid-template-rows: auto 1fr auto;
    min-height: 100dvh;
    background: var(--bg);
}

/* One more row than the ordinary shell, for the strip above the bar. Declared here rather than by
   giving .shell four rows always, because an empty fourth row would collapse but its gap would not. */
.shell--impersonating {
    grid-template-rows: auto auto 1fr auto;
    --impersonating-h: 34px;
}

/*
    The impersonation strip. Amber in both themes and not softened for either: it is the only thing
    on screen saying that the account acting is not the account signed in, and every destructive act
    available while it is up will be recorded against the name on it.
*/
.impersonating {
    --impersonating-ink: #2A1B00;

    position: sticky;
    top: 0;
    /* Above the top bar, which stops just below it. */
    z-index: 31;
    display: flex;
    align-items: center;
    gap: 10px;
    height: var(--impersonating-h);
    padding: 0 14px;
    background: linear-gradient(90deg,
        var(--q-amber),
        color-mix(in srgb, var(--q-amber) 82%, var(--q-red)));
    color: var(--impersonating-ink);
    font-size: 12.5px;
}

.impersonating__text {
    display: flex;
    align-items: baseline;
    gap: 8px;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.impersonating__text strong {
    font-weight: 800;
    letter-spacing: -.01em;
}

.impersonating__meta {
    /* Mixed towards the background rather than given an opacity, so it stays legible against
       amber instead of going grey. */
    color: color-mix(in srgb, var(--impersonating-ink) 72%, var(--q-amber));
}

.impersonating__lapsed {
    margin-left: auto;
    font-weight: 700;
}

/* Pushed to the end, and the only thing here that is not text. */
.impersonating__return {
    margin-left: auto;
    flex: none;
    background: var(--impersonating-ink);
    border-color: var(--impersonating-ink);
    color: #FFF;
}

.impersonating__lapsed + .impersonating__return {
    margin-left: 0;
}

.impersonating__return:hover:not(:disabled) {
    background: color-mix(in srgb, var(--impersonating-ink) 82%, #FFF);
    border-color: color-mix(in srgb, var(--impersonating-ink) 82%, #FFF);
}

/* A signal bus across the top of the application. */
.shell::before {
    content: "";
    position: fixed;
    inset: 0 0 auto 0;
    height: 2px;
    z-index: 40;
    background: linear-gradient(90deg, var(--q-violet), var(--q-violet-light) 40%, var(--q-lime));
}

.topbar {
    position: sticky;
    /* Below the impersonation strip when there is one, which is why that is a token and not a
       literal: both of these have to agree or one sticks over the other. */
    top: var(--impersonating-h);
    z-index: 30;
    display: flex;
    align-items: center;
    gap: 18px;
    height: 52px;
    padding: 0 16px 0 14px;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}

.topbar__brand {
    display: flex;
    align-items: center;
    gap: 10px;
    text-decoration: none;
    color: inherit;
    flex: none;
}

.topbar__brandname {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 15px;
    letter-spacing: -.02em;
    color: var(--text);
}

.topbar__divider {
    width: 1px;
    height: 26px;
    background: var(--border);
    flex: none;
}

.topbar__spacer {
    flex: 1;
}

.topbar__right {
    display: flex;
    align-items: center;
    gap: 8px;
}

/* Icon buttons in the bar. */
.iconbutton {
    position: relative;
    width: 34px;
    height: 34px;
    display: grid;
    place-items: center;
    border: 1px solid transparent;
    background: none;
    border-radius: 10px;
    color: var(--text-soft);
    cursor: pointer;
    transition: background 140ms var(--step-cubic), color 140ms var(--step-cubic);
}

.iconbutton:hover,
.iconbutton[aria-expanded="true"] {
    background: var(--surface-sunken);
    color: var(--text);
    border-color: var(--border);
}

.badge {
    position: absolute;
    top: 4px;
    right: 4px;
    min-width: 17px;
    height: 17px;
    padding: 0 4px;
    display: grid;
    place-items: center;
    border-radius: var(--radius-pill);
    background: var(--q-red);
    color: #fff;
    font-family: var(--font-mono);
    font-size: 10px;
    line-height: 1;
    border: 2px solid var(--surface);
}

/* ----------------------------------------------------- tenant in the bar */

/* The tenant in use, stated rather than offered — no box and no hover, because it is a label
   and not a control. Switching lives in the account menu. */
.tenant__current {
    display: flex;
    align-items: center;
    gap: 11px;
    flex: none;
    height: 38px;
    max-width: 320px;
    color: var(--text);
}

.tenant__meta {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    line-height: 1.15;
    min-width: 0;
}

.tenant__label {
    font-family: var(--font-mono);
    font-size: 9px;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.tenant__name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13.5px;
    letter-spacing: -.01em;
    max-width: 190px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---------------------------------------------------- channel in the bar */

/* The channel in use is a control, unlike the tenant label beside it: picking up another
   channel is everyday work, so the chip itself opens the picker. The menu anchors here. */
.channelbar {
    position: relative;
    display: flex;
    align-items: center;
    flex: none;
}

.channelbar__trigger {
    display: flex;
    align-items: center;
    gap: 11px;
    height: 38px;
    max-width: 320px;
    padding: 0 10px;
    background: none;
    border: 1px solid transparent;
    border-radius: 10px;
    color: var(--text);
    cursor: pointer;
    transition: background 140ms var(--step-cubic), border-color 140ms var(--step-cubic);
}

.channelbar__trigger:hover,
.channelbar__trigger[aria-expanded="true"] {
    background: var(--surface-sunken);
    border-color: var(--border);
}

/* ------------------------------------------------------------- dropdowns */

.backdrop {
    position: fixed;
    inset: 0;
    z-index: 35;
    background: transparent;
    border: 0;
    cursor: default;
}

.menu {
    position: absolute;
    top: calc(100% + 8px);
    z-index: 36;
    min-width: 260px;
    max-width: min(380px, calc(100vw - 32px));
    padding: 6px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 14px;
    box-shadow: var(--shadow-card);
    animation: menu-in 140ms var(--step-cubic) both;
}

.menu--left {
    left: 0;
}

.menu--right {
    right: 0;
}

.menu--wide {
    min-width: 340px;
}

/* A menu that has to leave the card it is in — see q90.openMenu for why nothing else works.

   In the top layer it is positioned against the viewport, so the coordinates come from script and
   everything here is what has to give way for them: a popover's own UA style is inset: 0 with
   margin: auto, which centres it. The zeroes are a starting point script overwrites in the same
   turn it shows the element, so nothing is ever painted at them. */
.menu--floating {
    position: fixed;
    inset: auto;
    top: 0;
    left: 0;
    margin: 0;
    overflow: visible;
}


/* A menu hung off a control inside a card rather than off the top bar: the field is the anchor, so
   the panel opens under the trigger and is as wide as it. */
.pickerfield {
    position: relative;
    max-width: 340px;
}

/* ------------------------------------------------------------------ field groups as tabs

   The strip that replaces the stack of folds when somebody prefers one group at a time. Scrolls
   sideways rather than wrapping: a content type with nine groups would otherwise push the fields
   themselves off the bottom of the pane, which is the thing this layout exists to avoid. */
.grouppills {
    display: flex;
    gap: 4px;
    overflow-x: auto;
    padding-bottom: 2px;
    margin-bottom: 12px;
    border-bottom: 1px solid var(--border);
    scrollbar-width: thin;
}

.grouppills__pill {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    border: 1px solid transparent;
    border-radius: var(--radius-pill);
    background: none;
    color: var(--text-soft);
    font: inherit;
    font-size: 12px;
    white-space: nowrap;
    cursor: pointer;
    transition: background 140ms var(--step-cubic), color 140ms var(--step-cubic);
}

.grouppills__pill:hover {
    background: var(--surface-sunken);
    color: var(--text);
}

.grouppills__pill[aria-selected="true"] {
    background: color-mix(in srgb, var(--q-violet) 16%, transparent);
    border-color: color-mix(in srgb, var(--q-violet) 40%, transparent);
    color: var(--text);
    font-weight: 600;
}

/* How many of the group's fields are filled. Tabular so the numbers do not shuffle the pill's width
   as somebody types into the form behind it. */
.grouppills__count {
    color: var(--text-muted);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
}

/* A dot saying this group carries a note, whose text is the pill's tooltip. */
.grouppills__note {
    color: var(--q-violet);
}

/* The layout switch, small enough to sit in a row of icon buttons. */
.segmented--sm {
    padding: 2px;
}

.segmented--sm .segmented__option {
    padding: 3px 9px;
    font-size: 11.5px;
}

/* One radio and its explanation, on the account page. The switch in the panes has no room for the
   sentence; this card is the place it fits. */
.prefchoice {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 9px 2px;
    cursor: pointer;
}

.prefchoice__text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.prefchoice__name {
    font-size: 13.5px;
    font-weight: 600;
}

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


/* ------------------------------------------------------------------ the asset type filter

   Shared by the dashboard's latest-assets strip and the file explorer's bar, which want different
   widths and nothing else different — so the anchor carries no size of its own and each host says.

   Deliberately not built on .field: that carries margin-bottom, and in a bar centring its items the
   margin lifted the button a few pixels above every control beside it. */
.typefilter {
    position: relative;
}

.typefilter__button {
    width: 100%;
    text-align: left;
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Twelve types is taller than some of what this sits in, so the panel scrolls rather than the page
   growing for it. */
.typefilter__menu {
    max-height: min(52vh, 320px);
    overflow-y: auto;
}

/* A row that is a label wrapping its checkbox, so the whole row is the hit target. menu__item is a
   button everywhere else, which is why the layout is restated here. */
.typefilter__row {
    display: flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
}

/* The dashboard strip: shares the row with two selects, so it flexes like them. */
.widgetsetup__types {
    flex: 1 1 148px;
    min-width: 0;
    max-width: 220px;
}

/* The explorer bar: sized to its longest type name rather than flexing, because the bar it is in
   holds buttons whose widths are their own. */
.mediabar__types {
    flex: none;
    width: 168px;
}

/* The trigger reads as a field, because a field is what it stands in for. Sized and bordered by
   .input, which it carries as well. */
.pickertrigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    text-align: left;
    cursor: pointer;
}

.pickertrigger > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A submenu opens beside the panel that owns it rather than inside it, so a long list — the
   tenants — does not crowd out the handful of items around it. Right-aligned menus grow
   leftwards, which is the only direction with room at the far end of the bar. */
.submenu__host {
    position: relative;
}

.submenu {
    top: 0;
    right: calc(100% + 10px);
    left: auto;
}

.submenu__chevron {
    margin-left: auto;
    flex: none;
    color: var(--text-muted);
}

@keyframes menu-in {
    from {
        opacity: 0;
        transform: translateY(-4px);
    }
}

.menu__heading {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 9px 10px 7px;
    font-family: var(--font-mono);
    font-size: 9px;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.menu__item {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 9px 10px;
    border: 0;
    background: none;
    border-radius: 9px;
    color: var(--text);
    font: inherit;
    font-size: 13.5px;
    text-align: left;
    text-decoration: none;
    cursor: pointer;
}

.menu__item:hover {
    background: var(--surface-sunken);
}

.menu__item[aria-current="true"] {
    background: color-mix(in srgb, var(--q-violet) 12%, transparent);
    color: var(--q-violet);
}

.menu__item--danger {
    color: var(--q-red);
}

.menu__item--danger:hover {
    background: color-mix(in srgb, var(--q-red) 12%, transparent);
}

.menu__itemtext {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.3;
}

.menu__sub {
    font-size: 12px;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The way out of a list that is only part of the truth: the loaded options are one tenant's
   members, and the person being looked for may be on another tenant entirely. Kept above the
   rows, where it is reachable without scrolling past forty names to find out it exists. */
.menu__lookup {
    color: var(--q-violet);
    border-bottom: 1px solid var(--border);
    border-radius: 9px 9px 0 0;
}

.menu__lookup .menu__sub {
    color: inherit;
    opacity: .75;
}

.menu__lookup:disabled {
    cursor: progress;
    opacity: .6;
}

.menu__separator {
    height: 1px;
    margin: 6px 4px;
    background: var(--border);
}

/* Type-to-filter inside a menu. */
.menu__search {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 8px 10px;
    margin-bottom: 2px;
    border-bottom: 1px solid var(--border);
}

.menu__searchinput {
    flex: 1;
    min-width: 0;
    border: 0;
    padding: 0;
    background: none;
    color: var(--text);
    font-family: var(--font-body);
    font-size: 14px;
}

.menu__searchinput:focus {
    outline: none;
}

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

.menu__clear {
    display: grid;
    place-items: center;
    width: 20px;
    height: 20px;
    padding: 0;
    border: 0;
    background: none;
    border-radius: 50%;
    color: var(--text-muted);
    cursor: pointer;
}

.menu__clear:hover {
    color: var(--text);
    background: var(--surface-sunken);
}

/* Keyboard cursor — distinct from hover so both can be visible at once. */
.menu__item[data-active="true"] {
    background: var(--surface-sunken);
    box-shadow: inset 0 0 0 1px var(--q-violet);
}

.menu__match {
    background: color-mix(in srgb, var(--q-lime) 34%, transparent);
    color: inherit;
    border-radius: 3px;
    padding: 0 1px;
}

.menu__empty {
    padding: 22px 12px;
    text-align: center;
    color: var(--text-muted);
    font-size: 13px;
}

.menu__scroll {
    max-height: 340px;
    overflow-y: auto;
}

/* Notification rows carry an unread marker rather than relying on weight alone. */
/* A notification is a row of three things: what happened, and the two answers to it — read or
   gone. The actions stay visible rather than appearing on hover, because a menu that is opened
   by touch has no hover to reveal them with. */
.notification__row {
    display: flex;
    align-items: flex-start;
    gap: 2px;
}

.notification {
    align-items: flex-start;
    flex: 1;
    min-width: 0;
}

.notification__actions {
    display: flex;
    align-items: center;
    gap: 2px;
    flex: none;
    padding-top: 4px;
}

.notification__bulk {
    display: flex;
    align-items: center;
    gap: 12px;
    flex: none;
}

/* "Yes, clear" is one thing to click, not two — let the sentence beside it wrap instead. */
.notification__bulk > .link {
    white-space: nowrap;
}

.notification__dot {
    width: 7px;
    height: 7px;
    margin-top: 6px;
    border-radius: 50%;
    background: var(--q-violet);
    flex: none;
}

.notification__dot[data-read="true"] {
    background: transparent;
    box-shadow: inset 0 0 0 1px var(--border-strong);
}

.notification__message {
    font-size: 13.5px;
    line-height: 1.4;
}

.notification__time {
    font-family: var(--font-mono);
    font-size: 10.5px;
    color: var(--text-muted);
    letter-spacing: .04em;
}

/* ------------------------------------------------------------------ user */

.avatar {
    position: relative;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    overflow: hidden;
    display: grid;
    place-items: center;
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 12px;
    letter-spacing: .02em;
    color: #fff;
    background: linear-gradient(135deg, var(--q-violet), var(--q-violet-deep));
    flex: none;
}

/* Lies over the initials. A load failure hides it and the letters come back. */
.avatar__image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border-radius: inherit;
    background: inherit;
    object-fit: cover;
}

.avatar--lg {
    width: 40px;
    height: 40px;
    font-size: 14px;
}

.profile {
    position: relative;
    flex: none;
}

.profile__trigger {
    display: flex;
    align-items: center;
    gap: 8px;
    height: 40px;
    padding: 0 6px 0 6px;
    background: none;
    border: 1px solid transparent;
    border-radius: 10px;
    cursor: pointer;
    color: var(--text);
}

.profile__trigger:hover,
.profile__trigger[aria-expanded="true"] {
    background: var(--surface-sunken);
    border-color: var(--border);
}

.profile__identity {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px;
}

.profile__name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13.5px;
}

/* -------------------------------------------------------- side navigation */

.shell__body {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: start;
}

.sidenav {
    position: sticky;
    top: calc(52px + var(--impersonating-h));
    height: calc(100dvh - 52px - var(--impersonating-h) - var(--statusbar-h));
    width: 222px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 16px 12px;
    background: var(--surface);
    border-right: 1px solid var(--border);
    overflow-y: auto;
    transition: width 180ms var(--step-cubic);
}

.sidenav[data-collapsed="true"] {
    width: 68px;
}

.sidenav__group {
    padding: 14px 10px 6px;
    font-family: var(--font-mono);
    font-size: 9px;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
}

.sidenav[data-collapsed="true"] .sidenav__group {
    padding-left: 0;
    text-align: center;
    letter-spacing: 0;
}

.sidenav__link {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 9px 10px;
    border-radius: 10px;
    color: var(--text-soft);
    text-decoration: none;
    font-size: 13.5px;
    white-space: nowrap;
    overflow: hidden;
    transition: background 130ms var(--step-cubic), color 130ms var(--step-cubic);
}

.sidenav__link:hover {
    background: var(--surface-sunken);
    color: var(--text);
}

.sidenav__link.active {
    background: color-mix(in srgb, var(--q-violet) 13%, transparent);
    color: var(--q-violet);
    font-weight: 700;
}

.sidenav__link--disabled,
.sidenav__link--disabled:hover {
    color: var(--text-muted);
    background: none;
    opacity: .45;
    cursor: not-allowed;
}

.sidenav__icon {
    flex: none;
    display: grid;
    place-items: center;
    width: 20px;
}

.sidenav[data-collapsed="true"] .sidenav__text {
    display: none;
}

.sidenav[data-collapsed="true"] .sidenav__link {
    justify-content: center;
    padding: 9px 0;
}

.sidenav__foot {
    margin-top: auto;
    padding-top: 12px;
    border-top: 1px solid var(--border);
    display: flex;
    justify-content: center;
}

.content {
    padding: 20px 24px 40px;
    min-width: 0;
}

.page__header {
    margin-bottom: 22px;
}

.page__eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 8px;
}

.page__title {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 22px;
    letter-spacing: -.03em;
    margin: 0;
    color: var(--text);
}

.page__lede {
    margin: 6px 0 0;
    color: var(--text-muted);
    max-width: 62ch;
}

/* A card is a query container, so what is inside it can lay itself out against the width the card
   actually got rather than the width of the window — the pane columns mean those are now two
   different questions. Inline-size only: the height has to keep coming from the content, which is
   what lets the columns pack. */
.panelcard {
    container-type: inline-size;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 14px;
    padding: 18px;
}

.datalist {
    display: grid;
    grid-template-columns: minmax(140px, max-content) 1fr;
    gap: 12px 26px;
    margin: 0;
}

.datalist dt {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--text-muted);
    padding-top: 3px;
}

.datalist dd {
    margin: 0;
    color: var(--text);
}

@media (max-width: 900px) {
    .sidenav {
        width: 68px;
    }

    .sidenav .sidenav__text {
        display: none;
    }

    .sidenav .sidenav__link {
        justify-content: center;
        padding: 9px 0;
    }

    .sidenav__group {
        text-align: center;
        padding-left: 0;
        letter-spacing: 0;
    }

    .content {
        padding: 20px 16px 40px;
    }

    .tenant__name {
        max-width: 120px;
    }
}

@media (max-width: 620px) {
    .topbar__brandname {
        display: none;
    }

    .datalist {
        grid-template-columns: 1fr;
        gap: 4px 0;
    }

    .datalist dd {
        margin-bottom: 12px;
    }
}

/* --------------------------------------------------------------------------
   Account settings
   -------------------------------------------------------------------------- */

/* Panes pack into as many columns as the space will take, each column filled to the depth of the
   others rather than to a shared row height — so two short cards stack beside one tall one instead
   of leaving a hole under the short one. Grid lays out in rows and cannot pack that way, which is
   why this is multi-column.

   Two numbers govern it. The 420px floor is what one of these cards needs before its forms and
   lists start to cramp, and it decides how many columns the space can hold — measured off the
   container, so collapsing the side nav widens the page without a breakpoint of its own. The
   number of panes caps that count, because columns divide the width equally: uncapped, a
   two-pane page on a wide screen would sit in two narrow columns with the rest of the row empty.
   Between the two, a column settles anywhere from 420px to one comfortable pane wide. */
.settings {
    --pane: 740px;
    --panes: 4;

    column-width: 420px;
    column-count: var(--panes);
    column-gap: 14px;
    max-width: calc(var(--panes) * var(--pane) + (var(--panes) - 1) * 14px);
}

/* How many panes there are to place. Five or more keeps the default, by which point the 420px
   floor is doing the deciding anyway. */
.settings:has(> :only-child) {
    --panes: 1;
}

.settings:has(> :nth-child(2):last-child) {
    --panes: 2;
}

.settings:has(> :nth-child(3):last-child) {
    --panes: 3;
}

/* A pane is one thing to deal with, so it goes wholly in one column or wholly in the next. */
.settings > * {
    break-inside: avoid;
    margin-bottom: 14px;
}

/* Hung off a tab strip, which needs clearing before the first card. */
.settings--tabbed {
    margin-top: 18px;
}

/* --------------------------------------------------------------------------
   Card grid — arrangeable cards
   -------------------------------------------------------------------------- */

/* Everything a card grid is made of shares one width: the toolbar, the viewport the canvas is seen
   through, and the canvas itself. Declared here so the three cannot drift apart — the script
   measures the viewport to decide where a card fits, and a viewport wider than the canvas would let
   it place cards past the canvas's own edge. */
.cardgridhost {
    --pane: 740px;
    max-width: calc(4 * var(--pane) + 3 * 14px);
}

/* The canvas's viewport. Ordinarily an ordinary block that clips nothing; CardGrid.razor.js turns
   it into a scroll box for exactly as long as some card sits past its right edge, which is what a
   window narrower than the arrangement gets instead of cards being squeezed into each other. */
.cardgrid__scroll {
    position: relative;
}

/* A .settings container whose cards the user has arranged, or can. This is a free canvas rather
   than a grid: every card carries its own left/top/width, written by CardGrid.razor.js, snapped
   to a fine raster rather than to its neighbours — moving a card never shoves the others. Until
   the script has run, or when the container is too narrow for free placement to mean anything,
   the cardgrid--free class is absent and the cards simply stack. */
.settings.cardgrid {
    column-width: auto;
    column-count: auto;
    position: relative;

    /* Stated outright rather than through --panes: the pane-counting :has() rules above key off
       the child count, and pixels have replaced panes as the way a card gets width here anyway. */
    max-width: calc(4 * var(--pane) + 3 * 14px);
}

/* The stacked fallback: plain flow, one card under the next. */
.settings.cardgrid > .cardgrid__item {
    margin-bottom: 14px;
}

.settings.cardgrid.cardgrid--free > .cardgrid__item {
    position: absolute;
    margin-bottom: 0;
}

.cardgrid__item {
    position: relative;
    min-width: 0;
    border-radius: 14px;
}

/* While arranging, the raster everything snaps to is shown as it truly is. */
.cardgrid--free[data-editing="true"] {
    background-image: radial-gradient(circle, var(--border) 1px, transparent 1px);
    background-size: 14px 14px;
    border-radius: 8px;
}

/* Whatever a card holds stays inside it: content that cannot shrink to a narrow span scrolls
   sideways within the card rather than bleeding over its border. */
.cardgrid__item .panelcard {
    overflow-x: auto;
}

/* A card given an explicit height keeps its content inside it too: the pane fills the card and
   scrolls, rather than spilling over the bottom border. The selector keys off the inline height
   the script writes — the only inline height these items ever carry. */
.cardgrid--free > .cardgrid__item[style*="height"] > .panelcard {
    height: 100%;
    overflow-y: auto;
}

/* A narrow card stacks its fact list rather than squeezing two columns into no room — the same
   move the 620px viewport rule below makes for the auth pages, keyed off the card instead. */
@container (max-width: 380px) {
    .datalist {
        grid-template-columns: 1fr;
        gap: 4px 0;
    }

    .datalist dd {
        margin-bottom: 12px;
    }
}

/* The toolbar sits above the canvas, sharing its width cap so the controls align with its
   right edge. */
.cardgrid__bar {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 10px;
    min-height: 30px;
    margin-bottom: 10px;
    max-width: calc(4 * var(--pane) + 3 * 14px);
}

.cardgrid__hint {
    margin-right: auto;
    font-size: 12.5px;
    color: var(--text-muted);
}

.cardgrid__scope {
    display: inline-flex;
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    overflow: hidden;
}

.cardgrid__scopebtn {
    border: 0;
    background: none;
    padding: 7px 14px;
    font: inherit;
    font-size: 12.5px;
    color: var(--text-muted);
    cursor: pointer;
    max-width: 220px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.cardgrid__scopebtn[data-on="true"] {
    background: color-mix(in srgb, var(--q-violet) 14%, transparent);
    color: var(--q-violet);
    font-weight: 600;
}

/* While arranging, every card shows its edges and grows its two handles: an overlay that makes
   the whole card the drag target (and keeps a stray click from landing in a form), and a strip
   on the right edge for resizing. */
.cardgrid[data-editing="true"] .cardgrid__item {
    outline: 1.5px dashed color-mix(in srgb, var(--q-violet) 45%, transparent);
    outline-offset: 2px;
}

/* Held over another card, the edge warns: this is not a place. Dropped here, the card slides
   to the nearest free spot instead — an overlapping arrangement is never saved. */
.cardgrid[data-editing="true"] .cardgrid__item--colliding {
    outline-color: color-mix(in srgb, var(--q-red) 70%, transparent);
}

.cardgrid__mover {
    position: absolute;
    inset: 0;
    z-index: 5;
    border-radius: 14px;
    cursor: grab;
    touch-action: none;
}

.cardgrid__mover:hover {
    background: color-mix(in srgb, var(--q-violet) 6%, transparent);
}

.cardgrid__sizer {
    position: absolute;
    top: 0;
    bottom: 0;
    right: -8px;
    width: 16px;
    z-index: 6;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: ew-resize;
    touch-action: none;
}

.cardgrid__sizer::after {
    content: "";
    width: 4px;
    height: 36px;
    border-radius: 999px;
    background: var(--border-strong);
}

.cardgrid__sizer:hover::after {
    background: var(--q-violet);
}

/* The bottom edge is the height handle — same grip, turned on its side. */
.cardgrid__sizer-y {
    position: absolute;
    left: 0;
    right: 0;
    bottom: -8px;
    height: 16px;
    z-index: 6;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: ns-resize;
    touch-action: none;
}

.cardgrid__sizer-y::after {
    content: "";
    width: 36px;
    height: 4px;
    border-radius: 999px;
    background: var(--border-strong);
}

.cardgrid__sizer-y:hover::after {
    background: var(--q-violet);
}

/* The corner does both at once, and sits above the two edge strips it overlaps. */
.cardgrid__sizer-xy {
    position: absolute;
    right: -8px;
    bottom: -8px;
    width: 22px;
    height: 22px;
    z-index: 7;
    cursor: nwse-resize;
    touch-action: none;
}

.cardgrid__item--active {
    z-index: 10;
    opacity: .92;
    box-shadow: 0 18px 44px -18px rgb(0 0 0 / 50%);
}

.cardgrid--dragging,
.cardgrid--dragging .cardgrid__mover {
    cursor: grabbing;
}

.setting__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 16px;
    margin-bottom: 16px;
}

.setting__title {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 15px;
    letter-spacing: -.02em;
    margin: 0 0 3px;
    color: var(--text);
}

.setting__hint {
    margin: 0;
    font-size: 13.5px;
    color: var(--text-muted);
    max-width: 54ch;
}

.setting__status {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 4px 10px;
    border-radius: var(--radius-pill);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .14em;
    text-transform: uppercase;
    white-space: nowrap;
    flex: none;
}

.setting__status[data-on="true"] {
    background: color-mix(in srgb, var(--q-lime) 16%, transparent);
    color: color-mix(in srgb, var(--q-lime) 60%, var(--text));
}

.setting__status[data-on="false"] {
    background: color-mix(in srgb, var(--q-amber) 18%, transparent);
    color: color-mix(in srgb, var(--q-amber) 62%, var(--text));
}

.setting__actions {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 4px;
}

/* Buttons that sit inside a settings card rather than ending a form. */
.button--inline {
    width: auto;
    height: 36px;
    padding: 0 16px;
    font-size: 13.5px;
}

.button--ghost {
    background: none;
    border: 1px solid var(--border-strong);
    color: var(--text);
    box-shadow: none;
}

.button--ghost:hover:not(:disabled) {
    border-color: var(--q-violet);
    color: var(--q-violet);
    background: none;
    box-shadow: none;
    transform: none;
    filter: none;
}

.button--danger {
    background: none;
    border: 1px solid color-mix(in srgb, var(--q-red) 45%, transparent);
    color: var(--q-red);
    box-shadow: none;
}

.button--danger:hover:not(:disabled) {
    background: color-mix(in srgb, var(--q-red) 10%, transparent);
    box-shadow: none;
    transform: none;
    filter: none;
}

/* Avatar row. */
.avatarrow {
    display: flex;
    align-items: center;
    gap: 18px;
    flex-wrap: wrap;
}

.avatar--xl {
    width: 62px;
    height: 62px;
    font-size: 20px;
    border-radius: 20px;
}

.avatarrow__file {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

.avatarrow__name {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--text-muted);
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Two-factor enrolment. */
.enrol {
    display: grid;
    gap: 18px;
    align-items: start;
    padding: 16px;
    margin-bottom: 16px;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: var(--surface-sunken);
}

/* The code goes beside the steps once the card is wide enough to hold both at a usable size, and
   above them when it isn't — this card can end up in a single narrow column, where a code big
   enough to scan and three steps beside it will not both fit. */
@container (min-width: 460px) {
    .enrol {
        grid-template-columns: auto 1fr;
    }
}

/* Big enough to scan across a desk without the phone hunting for it. Left at the browser's own
   smoothing: the API decides the image's real resolution, and a soft QR still reads, where the
   hard-edged alternative drops modules whenever the source comes back larger than this. */
.enrol__qr {
    width: 200px;
    height: 200px;
    max-width: 100%;
    border-radius: 12px;
    background: #fff;
    padding: 8px;
    border: 1px solid var(--border);
    object-fit: contain;
}

.enrol__qr--missing {
    display: grid;
    place-items: center;
    text-align: center;
    font-size: 12px;
    color: var(--text-muted);
    background: var(--surface);
}

.enrol__step {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin: 0 0 6px;
}

.codeblock {
    font-family: var(--font-mono);
    font-size: 13px;
    letter-spacing: .06em;
    word-break: break-all;
    white-space: pre-wrap;
    padding: 11px 13px;
    border-radius: 10px;
    border: 1px dashed var(--border-strong);
    background: var(--surface);
    color: var(--text);
    margin: 0 0 14px;
}

/* Recovery codes: one box each, numbered, and selectable in a single click —
   they are read and typed one at a time, so they are listed one at a time. */
.codes {
    display: grid;
    /* Five-character codes, so the boxes are sized to the content rather than to a row. */
    grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
    gap: 7px;
    margin: 0 0 14px;
    padding: 0;
    list-style: none;
    counter-reset: code;
}

.codes__item {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 9px 12px;
    border: 1px solid var(--border-strong);
    border-radius: 10px;
    background: var(--surface);
}

.codes__index {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .1em;
    color: var(--text-muted);
    min-width: 17px;
    flex: none;
}

.codes__value {
    font-family: var(--font-mono);
    font-size: 13.5px;
    letter-spacing: .14em;
    color: var(--text);
    /* One click selects a whole code, so nobody half-copies one. */
    user-select: all;
    overflow-wrap: anywhere;
}

.codes__used {
    opacity: .55;
}

/* --------------------------------------------------------------- API keys */

.keys {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 10px;
}

.keys__item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface-sunken);
}

.keys__main {
    min-width: 0;
    flex: 1;
}

.keys__title {
    display: flex;
    align-items: center;
    gap: 10px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13.5px;
    color: var(--text);
    margin-bottom: 5px;
}

.keys__secret {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 5px;
}

.keys__secret code {
    font-family: var(--font-mono);
    font-size: 12.5px;
    letter-spacing: .06em;
    color: var(--text-soft);
    user-select: all;
    overflow-wrap: anywhere;
}

.keys__meta {
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: .04em;
    color: var(--text-muted);
    display: flex;
    gap: 5px;
    flex-wrap: wrap;
}

.keys__actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

/* One key opened for editing, on its own line under the row it belongs to rather than beside it —
   three fields and a button will not share a line with the name and the switch. */
.keys__edit {
    flex: 1 0 100%;
    padding-top: 10px;
    margin-top: 2px;
    border-top: 1px solid var(--border);
}

.keys__edit .formrow:last-child {
    margin-bottom: 0;
}

.keys__confirm {
    font-size: 13px;
    color: var(--q-red);
    font-weight: 700;
}

.keys__empty {
    margin: 0;
    padding: 24px;
    text-align: center;
    color: var(--text-muted);
    font-size: 13.5px;
    border: 1px dashed var(--border-strong);
    border-radius: 12px;
}

.keyform {
    padding: 16px;
    margin-bottom: 16px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface-sunken);
}

/* The freshly created secret, shown once. */
.newkey {
    padding: 16px;
    margin-bottom: 16px;
    border: 1px solid color-mix(in srgb, var(--q-lime) 45%, transparent);
    border-radius: 12px;
    background: color-mix(in srgb, var(--q-lime) 10%, var(--surface));
}

.newkey__row {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}

.newkey__value {
    flex: 1;
    min-width: 220px;
    font-family: var(--font-mono);
    font-size: 13px;
    letter-spacing: .05em;
    padding: 10px 12px;
    border-radius: 9px;
    border: 1px dashed var(--border-strong);
    background: var(--surface);
    color: var(--text);
    user-select: all;
    overflow-wrap: anywhere;
}

.iconbutton--sm {
    width: 28px;
    height: 28px;
    border-radius: 8px;
}

.pill {
    display: inline-flex;
    align-items: center;
    padding: 2px 9px;
    border-radius: var(--radius-pill);
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .14em;
    text-transform: uppercase;
    white-space: nowrap;
}

.pill[data-tone="good"] {
    background: color-mix(in srgb, var(--q-lime) 18%, transparent);
    color: color-mix(in srgb, var(--q-lime) 58%, var(--text));
}

.pill[data-tone="muted"] {
    background: var(--bg-accent);
    color: var(--text-muted);
}

.pill[data-tone="danger"] {
    background: color-mix(in srgb, var(--q-red) 14%, transparent);
    color: color-mix(in srgb, var(--q-red) 72%, var(--text));
}

.checkline {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 4px 0 16px;
    font-size: 13.5px;
    color: var(--text-soft);
    cursor: pointer;
}

.checkline input {
    width: 16px;
    height: 16px;
    accent-color: var(--q-violet);
}

/* ----------------------------------------------------------------- tenant */

/* An identifier that is copied rather than read — kept monospaced so it can be checked
   character by character. */
.mono {
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: .04em;
    color: var(--text-soft);
    user-select: all;
}

.field__optional {
    font-family: var(--font-body);
    font-size: 11px;
    letter-spacing: 0;
    text-transform: none;
    color: var(--text-muted);
}

/* ------------------------------------------------------------------ teams */

.teams {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 10px;
}

.teams__item {
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface-sunken);
    overflow: hidden;
}

.teams__row {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    padding: 10px 14px;
}

/* The whole title area is the disclosure control, so opening a team is a comfortable target
   rather than a chevron-sized one. */
.teams__toggle {
    display: flex;
    align-items: center;
    gap: 9px;
    flex: 1;
    min-width: 0;
    padding: 4px 0;
    background: none;
    border: 0;
    color: var(--text);
    cursor: pointer;
    text-align: left;
    font: inherit;
}

/* A team that cannot be opened. The blank the chevron would have filled is held open, so names
   down the list still start on the same edge whether or not their row is a control. */
.teams__toggle--static {
    cursor: default;
}

.teams__toggle--static::before {
    content: "";
    flex: none;
    width: 16px;
}

.teams__name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13.5px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.teams__tags {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}

.teams__roles {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    margin: 0 0 16px;
    font-size: 12.5px;
    color: var(--text-muted);
}

/* The teams this account is in, grouped under the tenant each belongs to. */
.myteams__tenant {
    display: flex;
    align-items: center;
    gap: 6px;
}

.myteams__roles {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 6px;
}

/* The rights one team grants, revealed under it. Full width of the row it hangs off — the row is
   a wrapping flex line, so claiming the whole basis is what puts this on its own line. */
.rights {
    flex-basis: 100%;
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid var(--border);
}

.rights__role {
    margin: 0 0 7px;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.rights__list {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 0 0 12px;
    padding: 0;
    list-style: none;
}

.rights__list:last-child {
    margin-bottom: 0;
}

/* ---------------------------------------------------------------- effective access

   What one account can do, read back as the path that grants it: a tenant, the teams they stand in,
   the roles those teams hold, and the rights on those roles. Nested three deep, so the indent is
   what carries the structure and the counts are what make it scannable. */
/* A section of the record in its own right, so it needs no rule of its own to separate it. */
.access {
    margin: 0;
}

/* Find a tenant, choose how many of them, and see how many that left. */
.access__tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin: 0 0 10px;
}

.access__find {
    flex: 1 1 150px;
    min-width: 110px;
}

.access__tenant + .access__tenant {
    margin-top: 4px;
}

/* A tenant is a fold, so its name is a button that still reads as a name. */
.access__name {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 6px var(--admin-pad-x, 10px);
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface-sunken);
    font: inherit;
    color: var(--text);
    text-align: left;
    cursor: pointer;
}

.access__name:hover {
    border-color: var(--border-strong);
}

.access__tenantname {
    flex: 1 1 auto;
    min-width: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.access__body {
    padding: 10px 0 4px 22px;
}

/* One team, or one channel, inside a tenant. */
.access__team {
    margin-top: 10px;
    padding: 8px 12px;
    border: 1px solid var(--border);
    border-radius: 10px;
}

.access__teamhead {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
}

/* A role that reaches one channel rather than the whole tenant, indented under the team. */
.access__on {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 10px 0 6px;
    font-size: 12.5px;
    color: var(--text-soft);
}

/* The tally beside a role name or a list heading — how many, before reading which. */
.access__count {
    margin-left: 8px;
    padding: 1px 6px;
    border-radius: var(--radius-pill);
    background: var(--bg-accent);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .06em;
    color: var(--text-muted);
}

.access .rights__role {
    display: flex;
    align-items: center;
    margin-bottom: 6px;
}

.teampanel {
    padding: 4px 14px 14px;
    border-top: 1px solid var(--border);
}

.teampanel__invite {
    display: flex;
    align-items: flex-end;
    gap: 10px;
    flex-wrap: wrap;
    margin: 14px 0;
}

.teampanel__count {
    margin: 12px 0 8px;
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* ---------------------------------------------------------------- profile */

.input--area {
    height: auto;
    min-height: 108px;
    padding: 11px 14px;
    line-height: 1.55;
    resize: vertical;
    font-family: var(--font-body);
}

/* An area holding machine text rather than prose — a storage account's settings JSON. Same weight
   as the rule above and after it, which is what makes the font stick. */
.input--json {
    font-family: var(--font-mono);
    font-size: 12.5px;
    line-height: 1.5;
    white-space: pre;
}

.colourrow {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
}

.colourrow input[type="color"] {
    width: 40px;
    height: 40px;
    padding: 3px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-field);
    background: var(--field);
    cursor: pointer;
}

.colourrow input[type="color"]::-webkit-color-swatch {
    border: none;
    border-radius: 7px;
}

.colourrow input[type="color"]::-moz-color-swatch {
    border: none;
    border-radius: 7px;
}

.swatches {
    display: flex;
    gap: 6px;
}

.swatch {
    width: 26px;
    height: 26px;
    padding: 0;
    border-radius: 8px;
    border: 1px solid rgb(0 0 0 / 14%);
    cursor: pointer;
    transition: transform 120ms var(--step-cubic);
}

.swatch:hover:not(:disabled) {
    transform: scale(1.12);
}

.swatch:disabled {
    cursor: not-allowed;
    opacity: .45;
}

.notice__list {
    margin: 6px 0 0;
    padding-left: 18px;
    font-size: 13px;
}

.notice__list li {
    margin-bottom: 3px;
}

/* --------------------------------------------------------------------------
   Signed-in landing
   -------------------------------------------------------------------------- */

.home {
    min-height: 100dvh;
    display: grid;
    place-items: center;
    padding: 40px;
    background: var(--bg);
}

.home__card {
    width: min(520px, 100%);
    text-align: center;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-card);
    padding: 32px 28px;
}

.home__title {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 24px;
    letter-spacing: -.03em;
    margin: 14px 0 8px;
}

.home__meta {
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--text-muted);
    margin-bottom: 26px;
}

/* ==========================================================================
   Administration — section tabs and the tenant the tabs are about
   ========================================================================== */

/* Built from the theme switch's shape rather than a component library's tabs, so the section
   reads as part of this application and not as a panel bolted onto it. */
.tabs {
    display: inline-flex;
    flex-wrap: wrap;
    padding: 3px;
    gap: 2px;
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
}

.tabs__tab {
    padding: 7px 16px;
    border: 0;
    background: none;
    border-radius: var(--radius-pill);
    color: var(--text-muted);
    font: inherit;
    font-size: 13px;
    cursor: pointer;
    transition: background 160ms var(--step-cubic), color 160ms var(--step-cubic);
}

.tabs__tab:hover {
    color: var(--text);
}

.tabs__tab[aria-selected="true"] {
    background: var(--surface);
    color: var(--q-violet);
    box-shadow: 0 1px 3px rgb(20 19 43 / 14%);
}

.adminscope {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin: 14px 0 18px;
    color: var(--text-muted);
    font-size: 13px;
}

.adminscope__level {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: .04em;
}

/* --------------------------------------------------------------------------
   Content channels
   -------------------------------------------------------------------------- */

/* A channel row carries a mark as well as a name, so it starts at the top rather than centring
   a two-line block against a 30px picture. */
.channels__item {
    align-items: flex-start;
}

.channels__item .avatar {
    margin-top: 2px;
}

/* A second heading inside a card, for the two lists that share one. Quieter than the card's own
   title, because it is a division of it and not a rival to it. */
.setting__subtitle {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: .02em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin: 22px 0 10px;
}

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

/* Tags and genres are one word each and there are many of them, so they wrap as chips rather than
   stacking as rows — a list of forty single-word rows is a scroll, not a list. */
.chips {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.chip {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 5px 6px 5px 12px;
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    font-size: 13px;
    color: var(--text);
}

/* Nothing to remove, so nothing to pad for. */
.chip:not(:has(.chip__remove)) {
    padding-right: 12px;
}

.chip__code {
    font-size: 11px;
    color: var(--text-muted);
}

.chip__remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--text-muted);
    cursor: pointer;
    transition: background 140ms var(--step-cubic), color 140ms var(--step-cubic);
}

.chip__remove:hover:not(:disabled) {
    background: var(--q-red);
    color: #fff;
}

.chip__remove:disabled {
    cursor: not-allowed;
    opacity: .45;
}

.chip--action {
    padding: 0;
    border-style: dashed;
    background: none;
}

.chip__add {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px 12px;
    border: 0;
    border-radius: var(--radius-pill);
    background: none;
    font: inherit;
    font-size: 13px;
    color: var(--text);
    cursor: pointer;
    transition: background 140ms var(--step-cubic), color 140ms var(--step-cubic);
}

.chip__add:hover:not(:disabled) {
    background: var(--q-violet);
    color: #fff;
}

.chip__add:disabled {
    cursor: not-allowed;
    opacity: .45;
}

/* A colour's value is free text upstream, so this is only painted when the server recognised it.
   Not a button, unlike .swatch on the profile screen — there is nothing to click here. */
.colourmark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 30px;
    height: 30px;
    border-radius: 9px;
    border: 1px solid rgb(0 0 0 / 14%);
}

.colourmark--unknown {
    background: var(--field);
    border-style: dashed;
    border-color: var(--border-strong);
    color: var(--text-muted);
    font-size: 13px;
    font-weight: 700;
}

/* The picker and its hex box are one control, so they never wrap apart — and the box takes what
   is left rather than insisting on a width the column may not have. */
.field .colourrow {
    flex-wrap: nowrap;
}

.field .colourrow .input {
    flex: 1;
    min-width: 0;
}

/* What is actually stored in a picture slot, at a size worth looking at. Wide rather than square:
   a logo is a wide thing, and letterboxing one into a circle tells you less than it hides. */
.mediathumb {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 76px;
    height: 48px;
    padding: 4px;
    border-radius: 10px;
    border: 1px solid var(--border);
    background: var(--field);
    color: var(--text-muted);
    overflow: hidden;
}

/* Nothing there yet — dashed, so an empty slot reads as waiting rather than broken. */
.mediathumb[data-set="false"] {
    border-style: dashed;
    opacity: .7;
}

/* Over the kind, filling the box, so what shows is the picture when it loads and the letters
   when it does not. */
.mediathumb__image {
    position: absolute;
    inset: 4px;
    width: calc(100% - 8px);
    height: calc(100% - 8px);
    /* contain, not cover: a logo cropped to fill a box is a logo misrepresented. */
    object-fit: contain;
    background: var(--field);
}

.mediathumb__kind {
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: .06em;
    color: var(--text-soft);
}

/* An address opens where it sits rather than in a dialog, so it needs room the row does not give
   it — full width, and set back from the row's own padding. */
.addressform {
    width: 100%;
    padding: 4px 0 2px;
}

.addressform .setting__subtitle {
    margin-top: 0;
}

/* --------------------------------------------------------------------------
   Responsive
   -------------------------------------------------------------------------- */

@media (max-width: 1040px) {
    .stage {
        grid-template-columns: 1fr;
    }

    .rail {
        padding: 24px 24px 20px;
        gap: 22px;
    }

    .monitor__body {
        padding: 16px;
    }

    .ppm__channel,
    .ppm__scale {
        height: 84px;
    }

    .rail__foot {
        display: none;
    }

    .panel {
        padding: 20px 20px 32px;
    }
}

@media (max-width: 560px) {
    .card {
        padding: 26px 22px 24px;
        border-radius: 16px;
    }

    .field__row {
        flex-direction: column;
        gap: 0;
    }

    .monitor__readouts {
        display: none;
    }
}

@media (prefers-reduced-motion: reduce) {

    *,
    *::before,
    *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
    }
}

/* ==========================================================================
   Administration — the workbench: scope bar, mode tabs, tables and switches
   ========================================================================== */

/* The tenant every tab answers for, chosen once and kept in sight. */
.adminbar {
    display: flex;
    align-items: flex-end;
    gap: 16px;
    flex-wrap: wrap;
    margin: 16px 0 18px;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface);
}

.adminbar .pickerfield {
    flex: 0 1 300px;
    min-width: 240px;
    margin-bottom: 0;
}

.adminbar__note {
    flex: 1 1 220px;
    padding-bottom: 11px;
    font-size: 12.5px;
    color: var(--text-muted);
}

.adminbar__level {
    margin-left: auto;
    padding-bottom: 13px;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: .04em;
    color: var(--text-muted);
    white-space: nowrap;
}

/* The sections of one open record — sign-in, teams, keys. Underlined rather than pilled, so the
   page keeps a single pill strip: the section tabs above. */
.subtabs {
    display: flex;
    gap: 2px;
    margin: 0 0 16px;
    border-bottom: 1px solid var(--border);
}

.subtabs__tab {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 9px 14px 10px;
    margin-bottom: -1px;
    border: 0;
    border-bottom: 2px solid transparent;
    background: none;
    font: inherit;
    font-size: 13px;
    color: var(--text-muted);
    cursor: pointer;
    transition: color 140ms var(--step-cubic), border-color 140ms var(--step-cubic);
}

.subtabs__tab:hover {
    color: var(--text);
}

.subtabs__tab[aria-selected="true"] {
    color: var(--q-violet);
    border-bottom-color: var(--q-violet);
    font-weight: 600;
}

.subtabs__count {
    font-family: var(--font-mono);
    font-size: 10px;
    padding: 1px 7px;
    border-radius: var(--radius-pill);
    background: var(--bg-accent);
    color: var(--text-muted);
}

/* One aligned grid for every admin listing. The panel sets --cols once; the head and every row
   share it, so columns line up by construction rather than by eye. */
.admintable {
    min-width: 540px;
    border: 1px solid var(--border);
    border-radius: 12px;
    overflow: hidden;
    background: var(--surface);
}

.admintable__head,
.admintable__row {
    display: grid;
    grid-template-columns: var(--cols, 1fr);
    gap: 14px;
    align-items: center;
    padding: 10px 14px;
}

.admintable__head {
    background: var(--surface-sunken);
    border-bottom: 1px solid var(--border);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.admintable__row + .admintable__row {
    border-top: 1px solid var(--border);
}

.admintable__row:hover {
    background: color-mix(in srgb, var(--q-violet) 4%, var(--surface));
}

.admintable__name {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
    flex-wrap: wrap;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13.5px;
    color: var(--text);
}

.admintable__cell {
    min-width: 0;
    font-size: 13px;
    color: var(--text-soft);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Two facts in one cell — a username with the address underneath it — without a column each.
   The row grows to fit rather than the pair being squeezed into one line. */
.admintable__stack {
    display: flex;
    flex-direction: column;
    justify-content: center;
    white-space: normal;
    line-height: 1.35;
}

.admintable__stack > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.admintable__sub {
    font-size: .86em;
    color: var(--text-muted);
}

/* For the one under a value being typed that would break what reads it. */
.admintable__sub[data-tone="danger"] {
    color: var(--q-red);
    white-space: normal;
}

.admintable__id {
    min-width: 0;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: .03em;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    user-select: all;
}

.admintable__actions {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.admintable__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 12px;
}

/* A switch says what is true right now and flips it. Only drawn where the state is actually
   known — a switch showing a guess would be a lie with a nice handle. */
.switch {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 10px;
    cursor: pointer;
    user-select: none;
}

.switch input {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
}

.switch__track {
    flex: none;
    position: relative;
    width: 36px;
    height: 20px;
    border-radius: var(--radius-pill);
    background: var(--border-strong);
    transition: background 160ms var(--step-cubic);
}

.switch__track::after {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 2px rgb(20 19 43 / 30%);
    transition: transform 160ms var(--step-cubic);
}

.switch input:checked + .switch__track {
    background: var(--q-violet);
}

.switch input:checked + .switch__track::after {
    transform: translateX(16px);
}

.switch input:focus-visible + .switch__track {
    box-shadow: var(--focus-ring);
}

.switch[data-disabled="true"] {
    cursor: not-allowed;
    opacity: .55;
}

.switch__text {
    font-size: 13px;
    color: var(--text-soft);
}

/* A switch presented as a setting: name and consequence on the left, the control on the right. */
.switchline {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface-sunken);
}

.switchline + .switchline {
    margin-top: 10px;
}

.switchline__text {
    min-width: 0;
}

.switchline__title {
    margin: 0 0 2px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13.5px;
    color: var(--text);
}

.switchline__hint {
    margin: 0;
    font-size: 12.5px;
    color: var(--text-muted);
}

/* The on/off pair for state the API refuses to report back: both sides offered, neither
   claimed, until the user sets one. */
.segmented {
    display: inline-flex;
    flex: none;
    padding: 3px;
    gap: 2px;
    background: var(--surface-sunken);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-pill);
}

.switchline .segmented {
    background: var(--surface);
}

.segmented__option {
    padding: 6px 14px;
    border: 0;
    background: none;
    border-radius: var(--radius-pill);
    font: inherit;
    font-size: 12.5px;
    color: var(--text-muted);
    cursor: pointer;
    transition: background 140ms var(--step-cubic), color 140ms var(--step-cubic);
}

.segmented__option:hover:not(:disabled) {
    color: var(--text);
}

.segmented__option[aria-pressed="true"] {
    background: color-mix(in srgb, var(--q-violet) 14%, transparent);
    color: var(--q-violet);
    font-weight: 600;
}

.segmented__option:disabled {
    cursor: not-allowed;
    opacity: .55;
}

/* A form line whose fields and button share one baseline. The row owns the rhythm: fields inside
   drop their own bottom margin, so the button lines up with the inputs by construction. Hints
   belong under the row, not inside it, or the alignment is gone again. */
.formrow {
    display: flex;
    align-items: flex-end;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 14px;
}

.formrow .field {
    flex: 1 1 220px;
    min-width: 180px;
    margin-bottom: 0;
}

.formrow .field--s {
    flex: 0 1 140px;
    min-width: 110px;
}

.formrow .field--m {
    flex: 0 1 240px;
    min-width: 170px;
}

/* ------------------------------------------------------------ a rule hung under a field

   A note added inside a .field grows it, and .formrow is align-items: flex-end — so the moment
   somebody typed a bracket into a name, that one input lifted clear of every other box in the row
   and everything under the row dropped. Out of flow instead, with the row holding the space whether
   the note is showing or not, so appearing and disappearing moves nothing. */

.field--rule {
    position: relative;
}

.field--rule .propnote {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin-top: .25rem;
}

/* Two lines at .78rem/1.45, which is what the longest of these messages wraps to in a 240px field.
   Reserved on the row rather than on the field: the field has to keep the height of its box for
   flex-end to go on aligning the inputs with each other. */
.formrow--rule {
    padding-bottom: 38px;
}

/* A field that takes the row to itself. Prose-shaped answers — a label, a key, a host name, a URL —
   are as long as they are, and a 240px box with the end of the value scrolled out of sight makes
   somebody arrow along a line they should be able to read. Number and date answers stay small: a
   wide box around four characters says the four characters are the wrong length. */
.formrow .field--full {
    flex: 1 1 100%;
    min-width: 100%;
}

.formrow .button--inline {
    flex: none;
    height: 40px;
}

.formrow .checkline {
    flex: none;
    margin: 0 0 10px;
}

.formrow .switch {
    margin-bottom: 11px;
}

.formrow + .hint {
    margin-top: -6px;
    margin-bottom: 14px;
}

/* The manage tab's headline: which one thing is being worked on. */

/* A named run of fields inside one form.

   The technical tab carries three systems' settings — Quatro's, IBB's, Maxx-XS's — and every one
   of them has a host or an id and a key. Read as one run of a dozen inputs, the labels are the
   only thing telling you which system you are editing, and "API key" appears three times. The
   name says it once, at the top; the rule beside it is what makes the boundary visible without
   putting a box around every group. */
.fieldgroup {
    margin: 0 0 18px;
}

.fieldgroup__name {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0 0 10px;
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--text-muted);
    white-space: nowrap;
}

/* Runs from the name to the edge of the pane, so the eye has the width of the group. */
.fieldgroup__name::after {
    content: "";
    flex: 1 1 auto;
    height: 1px;
    min-width: 0;
    background: var(--border);
}

/* The last group in a form does not need to push the save button down. */
.fieldgroup:last-of-type {
    margin-bottom: 8px;
}

/* Nothing has been defined for this group yet — said in the group rather than left blank, so an
   empty heading does not read as a field that failed to draw. */
.fieldgroup__none {
    margin: 0 0 4px;
    font-size: 12.5px;
    color: var(--text-muted);
}

/* Deleting a record.

   This was a fenced red box at the foot of the pane, under a heading repeating the word on the
   button inside it. The fence was doing the work of a warning and the button was doing the work of
   the warning too, so the loudest thing on a short record was the one action nobody came for. What
   is left is the button, in the head of the record beside its name, where everything else that acts
   on this record already is. The red is on the button, which is the part that acts. */
.deleteaction {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.deleteaction__open {
    display: inline-flex;
    align-items: center;
    gap: 5px;
}

/* The question, once the button has been pressed. It reads across the head rather than wrapping
   under it, so the two buttons beside it stay on the same line as the name they are about. */
.deleteaction__ask {
    font-size: 12px;
    font-weight: 650;
    color: var(--q-red);
}

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

/* ---------------------------------------------------------------- administration density

   Administration is a workbench, not a reading page: someone here is scanning a few hundred
   rows for the one they want, not settling in with a paragraph. So it runs at its own
   density, and every rule below is scoped to .adminpage — the rest of the application keeps
   the roomier sizing it was drawn at.

   The numbers are chosen against one measure: how many rows fit above the fold on a 1366×768
   laptop. Thirty-two pixels a row is the floor a 22px avatar and a legible 12.5px name allow. */
.adminpage {
    --admin-row-h: 32px;
    --admin-pad-x: 10px;
    --admin-pad-y: 5px;
    --admin-gap: 10px;
}

.content:has(.adminpage) {
    padding: 12px 16px 20px;
}

/* One bar carries what four bands used to: the section, the tabs, the tenant everything
   answers for, and the level answering. It stays put under the topbar, because the tenant in
   scope is the one thing you must never lose track of while reading a list. */
.adminpage .adminbar {
    position: sticky;
    top: 52px;
    z-index: 20;
    align-items: center;
    gap: var(--admin-gap);
    margin: 0 0 var(--admin-gap);
    padding: 6px var(--admin-pad-x);
    border-radius: 10px;
}

.adminbar__heading {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 13px;
    letter-spacing: -.01em;
    color: var(--text);
    white-space: nowrap;
}

.adminpage .adminbar .pickerfield {
    flex: 0 1 220px;
    min-width: 150px;
    margin-bottom: 0;
}

.adminpage .adminbar__level {
    padding-bottom: 0;
}

/* The pill strip sets the bar's height, so it is the one thing worth a few pixels here. */
.adminpage .tabs {
    padding: 2px;
}

.adminpage .tabs__tab {
    padding: 5px 12px;
    font-size: 12.5px;
}

/* The list and the record it opens, side by side. Reading a list and editing a row are the
   same job, so losing the list to see the record was the panel's real cost — worse than any
   amount of padding. */
.masterdetail {
    display: grid;
    grid-template-columns: minmax(340px, 3fr) minmax(380px, 4fr);
    gap: var(--admin-gap);
    align-items: start;
}

/* Folded, the list is a rail wide enough for one button and nothing else. minmax(0, 1fr) rather
   than 1fr on the record: 1fr floors at the content's own min-width, and a wide table inside would
   push the column past the page instead of scrolling inside it. */
.masterdetail--folded {
    grid-template-columns: 28px minmax(0, 1fr);
}

/* With a record open, the list gives up about half its width. Only on a panel that offers the fold
   at all — the content types one, whose record is a list of fields beside an open field, three
   columns in the space of two. The list is still there and still browsable; it has simply stopped
   being the thing being read. The floor drops with it, or the column keeps its old width on exactly
   the middling screens that need this most. */
.masterdetail--narrow {
    grid-template-columns: minmax(200px, 1.5fr) minmax(380px, 4fr);
}

/* Sticks like the list it replaces, so the way back is on screen wherever the record has been
   scrolled to. Its own element rather than a floating button, so the grid keeps a column for it
   and the record's left edge does not move when the rail appears. */
.masterdetail__rail {
    position: sticky;
    top: 104px;
    display: flex;
    justify-content: center;
}

.masterdetail__unfold {
    display: grid;
    place-items: center;
    width: 28px;
    padding: 10px 0;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface);
    color: var(--text-muted);
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
}

.masterdetail__unfold:hover,
.masterdetail__unfold:focus-visible {
    border-color: var(--border-strong);
    background: var(--surface-sunken);
    color: var(--text);
}

/* Last in the head and pushed to its end, so folding sits at the edge it folds towards and the
   search box keeps the room it had. */
.workbenchhead__fold {
    margin-left: auto;
    padding: 2px 6px;
    border: 1px solid transparent;
    border-radius: 8px;
    background: none;
    color: var(--text-muted);
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
}

.workbenchhead__fold:hover,
.workbenchhead__fold:focus-visible {
    border-color: var(--border);
    color: var(--text);
}

/* The list holds its own scroll and sticks to the top of the column, so the search and the pager
   stay where they were put while a list of four hundred runs past between them — and stay there
   while the record beside it runs long. 104px is the chrome above: topbar, page padding, the bar
   and its gap; 132px leaves the bottom of the pane clear of the page's own padding. */
.masterdetail__list {
    display: flex;
    flex-direction: column;
    position: sticky;
    top: 104px;
    max-height: calc(100dvh - 132px);
}

/* The rows run to the card's edge rather than sitting in its padding, so a 340px pane spends all
   of itself on names. Anything else in here — a loading line, an empty state — is given the
   padding back. */
.masterdetail__scroll {
    min-height: 0;
    overflow: auto;
    margin-inline: calc(var(--admin-pad-x) * -1);
}

.masterdetail__scroll > p {
    margin-inline: var(--admin-pad-x);
}

/* The record is as tall as what it holds, and the page carries it — it does not scroll inside a
   box of its own. A pane that scrolls is a pane that clips, and what it was clipping were the
   menus opened inside it: the people picker and the role picker both hang below their trigger and
   both were being cut off at the card's edge, which is no way to choose from a list. The list
   pane above keeps its scroll and sticks, so nothing is lost by giving this one up. */
.masterdetail__detail {
    overflow: visible;

    /* The positioning context the working veil covers. */
    position: relative;
}

/* Two panes need roughly 800px of room. Below that they stack, and the record opens under
   the list rather than beside it — where a sticky list would pin itself over the record it just
   opened. */
@media (max-width: 1100px) {
    .masterdetail,
    .masterdetail--folded {
        grid-template-columns: 1fr;
    }

    /* Stacked, the list is above the record rather than beside it, so folding it away buys no
       width — it only hides the thing somebody navigates by. The rail is dropped and the fold
       button with it; the flag survives untouched, so widening the window brings both back. */
    .masterdetail__rail,
    .workbenchhead__fold {
        display: none;
    }

    .masterdetail__list {
        position: static;
        max-height: none;
    }

    .masterdetail__scroll {
        overflow: visible;
    }
}

/* Search, tally and the one button that makes a new record — the whole list header in the
   height the old panel spent on its title alone. */
.workbenchhead {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 8px;
}

/* Scoped past .adminpage .field, which is the same weight and sits further down. */
.adminpage .workbenchhead .field {
    flex: 1 1 150px;
    min-width: 110px;
    margin-bottom: 0;
}

/* Sized down to the search box beside it, so the header is one row tall rather than one row plus
   whatever a pill strip's default padding comes to. */
.workbenchhead .segmented {
    padding: 2px;
}

.workbenchhead .segmented__option {
    padding: 3px 10px;
    font-size: 12px;
}

.workbenchhead__count {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .04em;
    color: var(--text-muted);
    white-space: nowrap;
}

.detailhead {
    display: flex;
    align-items: center;
    gap: var(--admin-gap);
    flex-wrap: wrap;
    padding-bottom: 8px;
    margin-bottom: 8px;
    border-bottom: 1px solid var(--border);
}

.detailhead__main {
    flex: 1 1 160px;
    min-width: 0;
}

.detailhead__title {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 14px;
    letter-spacing: -.02em;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.detailhead__meta {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    margin: 2px 0 0;
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: .03em;
    color: var(--text-muted);
}

.detailempty {
    display: grid;
    place-items: center;
    min-height: 140px;
    padding: 16px;
    text-align: center;
    font-size: 12.5px;
    color: var(--text-muted);
    border: 1px dashed var(--border-strong);
    border-radius: 12px;
}

/* The 540px floor keeps a full-width table honest; in a 340px pane it would only force a
   horizontal scrollbar under every list. The cells already ellipsise. */
.adminpage .admintable {
    min-width: 0;
}

/* Inside a scrolling pane the table gives up its own frame — the card is the frame — and with it
   the overflow that would otherwise make the head stick to a box that never scrolls. */
.masterdetail__scroll .admintable {
    overflow: visible;
    border: 0;
    border-radius: 0;
    background: none;
}

.adminpage .admintable__head,
.adminpage .admintable__row {
    padding: var(--admin-pad-y) var(--admin-pad-x);
    gap: var(--admin-gap);
}

.adminpage .admintable__head {
    position: sticky;
    top: 0;
    z-index: 1;
    padding-block: 4px;
    font-size: 9.5px;
}

/* A row is the button that opens it, which is what makes the whole row a target and gets the
   keyboard for free. Nothing else in the row is clickable — every action lives on the record
   itself now, so there is nothing to nest inside a button and nothing to mis-click. */
.adminpage .admintable__row {
    width: 100%;
    min-height: var(--admin-row-h);
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}

.adminpage .admintable__row:focus-visible {
    outline: none;
    box-shadow: inset 0 0 0 2px var(--q-violet);
}

.adminpage .admintable__row[aria-selected="true"] {
    background: color-mix(in srgb, var(--q-violet) 10%, var(--surface));
    box-shadow: inset 2px 0 0 var(--q-violet);
}

.adminpage .admintable__row[aria-selected="true"]:focus-visible {
    box-shadow: inset 0 0 0 2px var(--q-violet);
}

/* A row that is data rather than a target — the storage routing rules, where the action is a
   button inside the row and the row itself opens nothing. Same grid, no invitation to click it. */
.adminpage .admintable__row--static {
    cursor: default;
}

.adminpage .admintable__row--static:hover {
    background: none;
}

.adminpage .admintable .avatar {
    width: 22px;
    height: 22px;
    font-size: 10px;
}

.adminpage .admintable__name {
    gap: 8px;
    font-size: 12.5px;
}

.adminpage .admintable__cell {
    font-size: 12px;
}

.adminpage .admintable__id {
    font-size: 10.5px;
}

.adminpage .admintable__foot {
    margin-top: 8px;
}

.adminpage .panelcard {
    padding: var(--admin-pad-x);
    border-radius: 12px;
}

/* One height for everything that stands on a line with a field: a button two pixels shorter than
   the input beside it reads as a mistake, not as a smaller button. The rows align on their bottom
   edge, and that only looks like alignment when the things sitting on it are the same height. */
.adminpage .button--inline,
.adminpage .input {
    height: 30px;
}

.adminpage .button--inline {
    padding: 0 11px;
    font-size: 12.5px;
}

.adminpage .input {
    font-size: 12.5px;
}

/* Two reasons a button beside a field used to land eight pixels low, both of them invisible.
   A flex row aligns margin boxes, and .adminpage .field re-added the bottom margin that
   .formrow .field had just taken off — same weight, and later in the file. And a field left as a
   block ends in a line box, which keeps a few pixels of descender space under the input. Column
   layout and no margin put the field's bottom edge exactly where the input's is. */
.adminpage .formrow .field {
    display: flex;
    flex-direction: column;
    margin-bottom: 0;
}

/* Room to breathe. The section runs dense on purpose — see the note above .adminpage — but a
   caption sitting almost on its box, and boxes sitting almost on each other, read as one
   undifferentiated block once a form has more than three of them in it. */
.adminpage .formrow {
    gap: 16px;
    margin-bottom: 18px;
}

.adminpage .field__label {
    margin-bottom: 8px;
}

/* What has no label above it is lifted to the middle of the field beside it — half of what a
   30px input has left once the control's own height is taken off. */
.adminpage .formrow .checkline {
    margin-bottom: 6px;
}

.adminpage .formrow .switch {
    margin-bottom: 5px;
}

.adminpage .input--area {
    height: auto;
}

.adminpage .field {
    margin-bottom: 8px;
}

.adminpage .formrow {
    gap: 8px;
    margin-bottom: 8px;
}

.adminpage .switchline {
    padding: 6px var(--admin-pad-x);
}

.adminpage .switchline + .switchline {
    margin-top: 5px;
}

/* ---------------------------------------------------------------- the modules card

   One switch per module on a tenant, a channel or a team. The modules are rows upstream, so the
   number of them has no ceiling and this cannot be a list that is read top to bottom: it is
   searched, folded, and counted at the heading so a closed section still says what is inside it. */
.modules__tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin: 0 0 10px;
}

.modules__find {
    flex: 1 1 150px;
    min-width: 110px;
}

.modules__section {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 12px 0 5px;
}

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

/* The heading is the fold control, so it is a button that still has to read as a heading: the
   type is the group name's, and only the colour moves under the pointer. */
.modules__heading {
    display: flex;
    flex: 1 1 auto;
    align-items: center;
    gap: 6px;
    min-width: 0;
    padding: 2px 0;
    border: 0;
    background: none;
    font: inherit;
    color: var(--text-muted);
    text-align: left;
    cursor: pointer;
}

.modules__heading:hover {
    color: var(--text);
}

.modules__headingname {
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: .14em;
    text-transform: uppercase;
}

.modules__headingcount {
    font-size: 11.5px;
    letter-spacing: 0;
    text-transform: none;
    color: var(--text-soft);
}

.modules__bulk {
    display: flex;
    flex: none;
    gap: 4px;
}

/* Glyph, name and switch on one line: the picture hugs the name it belongs to and the switch stays
   at the edge, where every other switch on the record is. */
.modules .switchline {
    gap: 10px;
}

.modules .switchline__text {
    flex: 1 1 auto;
}

.modules__icon {
    flex: none;
    color: var(--text-muted);
}

/* The key and, where there is one, what is already granted above this record. */
.modules .switchline__hint {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}

.adminpage .keys {
    gap: 5px;
}

.adminpage .keys__item {
    padding: 6px var(--admin-pad-x);
}

.adminpage .keys__empty {
    padding: 12px;
    font-size: 12.5px;
}

.adminpage .teams {
    gap: 5px;
}

.adminpage .teams__row {
    padding: var(--admin-pad-y) var(--admin-pad-x);
}

.adminpage .setting__subtitle {
    margin: 12px 0 6px;
}

.adminpage .subtabs {
    margin-bottom: var(--admin-gap);
}

.adminpage .subtabs__tab {
    padding: 5px 10px 6px;
    font-size: 12.5px;
}

.adminpage .notice {
    margin-bottom: 8px;
}

/* ------------------------------------------------------------------------------ working

   Whether the pane on the right is doing something. Until now only the buttons knew: they went
   disabled and nothing else moved, so a save that took two seconds and a save that never left
   looked identical. The veil sits over the record rather than replacing it, because what is
   underneath is what the wait is about — and going blank for a moment then coming back is a
   worse answer than staying put.

   The pane it covers is the positioning context, set on .masterdetail__detail. */
.paneworking {
    position: absolute;
    inset: 0;
    z-index: 3;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: inherit;
    background: color-mix(in srgb, var(--surface) 72%, transparent);
    backdrop-filter: blur(1px);

    /* Nothing underneath is meant to be reachable while it is up, and the buttons are disabled
       anyway — this stops a click landing on a text field behind the veil. */
    cursor: progress;
}

/* Held back so that the quick majority of actions never flash a spinner: anything that answers
   inside a third of a second is better off saying nothing at all. */
@keyframes paneworking-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

.paneworking {
    animation: paneworking-in .12s ease-out .3s both;
}

@media (prefers-reduced-motion: reduce) {
    .paneworking {
        animation-delay: .3s;
        animation-duration: 1ms;
    }
}

/* ------------------------------------------------------------------------------ mam

   The section strip and the switch that sets how the whole of MAM is worked in, on one line —
   the strip says where you are and the switch how you are working, and neither is worth a row
   of its own. */
.mamstrip {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--admin-gap);
    flex-wrap: wrap;
}

.mamstrip .subtabs {
    margin-bottom: 0;
}

.adminpage .mamstrip {
    margin-bottom: var(--admin-gap);
}

/* A group heading inside the fields table, spanning every column. One table with bands in it
   rather than a table per group: the four headings are the same for every group, so saying
   them once is both shorter and easier to read down. */
.admintable__band {
    grid-column: 1 / -1;
    padding: 10px var(--admin-pad-x) 3px;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.admintable__band:first-of-type {
    padding-top: 4px;
}

/* The fields beside the open field. The record pane is a .panelcard and so already a query
   container, which is what this measures — the viewport says nothing useful about a column whose
   width comes from a grid two levels up and from whether the content-type list is folded. */
.cmsfields {
    display: grid;
    grid-template-columns: minmax(300px, 2fr) minmax(360px, 3fr);
    gap: var(--admin-gap);
    align-items: start;
    margin-top: var(--admin-gap);
}

/* Two columns need 780px between them, and the number is the list's three columns rather than a
   guess: Field, Kind and Holds want about 330px with their gaps and padding, the form beside them
   about 360, and there is a 10px gutter. Measured after 620px turned out to be too generous —
   below 1100px the record pane goes full width, cleared 620 at about 700, and split into a list
   column too narrow to draw Holds in, which was then quietly clipped rather than scrolled.
   Under 780 the field opens below its list, which is where it used to open. */
@container (max-width: 780px) {
    .cmsfields {
        grid-template-columns: 1fr;
    }
}

.cmsfields__list {
    display: flex;
    flex-direction: column;
    min-width: 0;
    padding: 10px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface-sunken);
}

.cmsfields__listhead {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 6px;
}

.cmsfields__tally {
    margin-right: auto;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .04em;
    color: var(--text-muted);
    white-space: nowrap;
}

/* The one scroll in here. The pane beside it holds the group picker, which hangs below its
   trigger, and a scrolling ancestor clips it — the same reason .masterdetail__detail gave up its
   own scroll. 60vh rather than a fixed height so a short list stays short. */
.cmsfields__scroll {
    min-height: 0;
    max-height: 60vh;
    overflow: auto;
    margin-inline: -10px;
}

.cmsfields__scroll > p {
    margin-inline: 10px;
}

/* Same bargain as .masterdetail__scroll .admintable: inside a scrolling pane the card is the
   frame, so the table drops its own. */
.cmsfields__scroll .admintable {
    overflow: visible;
    border: 0;
    border-radius: 0;
    background: none;
}

/* Field names break mid-word here, which no other admin table asks for. They are identifiers
   rather than prose — Customer_Video_Asset_Screenshot has nowhere to wrap and in a column this
   narrow it ran straight over the Kind cell beside it. Broken rather than ellipsised because the
   ones that collide differ only in their tail: cutting Customer_Video_Asset_… short hides the one
   part that says which field it is. */
.cmsfields__scroll .admintable__name > span {
    min-width: 0;
    overflow-wrap: anywhere;
}

/* A query container of its own, so the properties table below can measure the column it is
   actually in. The record pane is already a container, but it is the whole pane — this column is a
   fraction of it, and which fraction depends on whether the content-type list is folded. */
.cmsfields__editor {
    container-type: inline-size;
    min-width: 0;
}

/* The properties table asks for three columns whose minimums add up to more than this one has:
   140 + 140 + 290 plus gaps is past 600px, and the column is nearer 440. Rather than scroll
   sideways to reach a Remove button, the actions drop to a line of their own under the value.
   Overriding grid-template-columns rather than --cols on purpose — --cols is set inline by the
   panel, and inline wins on the property but says nothing about the longhand. */
@container (max-width: 560px) {
    .cmsfields__editor .cmsprops .admintable__head,
    .cmsfields__editor .cmsprops .admintable__row {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr);
        row-gap: 8px;
    }

    .cmsfields__editor .cmsprops .admintable__row > :nth-child(3) {
        grid-column: 1 / -1;
    }

    /* The head's third cell is empty — a blank line under Property and Value is furniture. */
    .cmsfields__editor .cmsprops .admintable__head > :nth-child(3) {
        display: none;
    }
}

/* In a pane of its own the field needs no frame of its own — the column is the frame, and a
   border inside a border reads as a box somebody forgot to close. Stacked, it gets one back:
   there it sits under the list again, and without it reads as one more row. */
.cmsfields__editor > .cmsfield {
    margin-top: 0;
    padding: 0;
    border: 0;
    background: none;
}

@container (max-width: 780px) {
    .cmsfields__editor > .cmsfield {
        padding: 14px;
        border: 1px solid var(--border);
    }
}

/* An open field, set apart from the list it was opened from — it sits below that list rather
   than beside it, so without a border it reads as one more row. */
.cmsfield {
    margin-top: var(--admin-gap);
    padding: 14px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--bg-accent);
}

.cmsfield .detailhead {
    margin-bottom: 10px;
}

/* Every property a field carries, folded away. What the form above wrote is the usual handful;
   this is the rest of the forty, and is not what anyone came to read. */
.cmsprops {
    margin-top: 12px;
}

/* A real control, not a line of text that happens to respond. It was neither obviously
   clickable nor obviously a disclosure, so it now looks like the button it is, spans the width
   it opens, and turns its own chevron. */
.cmsprops__toggle {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 8px 12px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface);
    font: inherit;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--text-soft);
    cursor: pointer;
    transition: border-color .12s ease, color .12s ease;
}

.cmsprops__toggle:hover {
    border-color: var(--border-strong);
    color: var(--text);
}

.cmsprops__toggle::before {
    content: "▸";
    font-size: 10px;
    color: var(--text-muted);
    transition: transform .12s ease;
}

.cmsprops__toggle[aria-expanded="true"] {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    color: var(--text);
}

.cmsprops__toggle[aria-expanded="true"]::before {
    transform: rotate(90deg);
}

/* The count sits at the far end, where it reads as "how many are in here". */
.cmsprops__toggle .subtabs__count {
    margin-left: auto;
}

.cmsprops__toggle[aria-expanded="true"] + .admintable,
.cmsprops[data-open="true"] > .admintable {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

.cmsprops .admintable {
    margin-top: 8px;
}

/* The "more room" tick beside a text value. A plain checkbox rather than the switch used
   elsewhere: this changes the shape of the box above it, not the state of anything saved. */
.cmsvalue__more {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    margin-top: 4px;
    font-size: 11px;
    color: var(--text-muted);
    cursor: pointer;
}

.cmsvalue__more input {
    accent-color: var(--q-violet);
    margin: 0;
}

/* The item a property names, and the way to change it. Drawn as the box it replaces so a row
   of properties keeps one baseline whatever each of them holds. */
.cmsvalue__picked {
    display: flex;
    align-items: center;
    gap: 8px;
}

.cmsvalue__pick {
    flex: 1 1 auto;
    min-width: 0;
    text-align: left;
    cursor: pointer;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cmsvalue__pick:hover:not(:disabled) {
    border-color: var(--border-strong);
}

/* ------------------------------------------------------------------------------ modal

   The one place this application takes over the screen. Everything else is somewhere you go;
   choosing an item is a detour from editing a property, and you are coming back. */
.modal {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    background: color-mix(in srgb, #000 55%, transparent);
}

.modal__panel {
    display: flex;
    flex-direction: column;
    width: min(880px, 100%);
    max-height: min(680px, 100%);
    padding: 16px;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: var(--surface);
    box-shadow: 0 24px 60px rgb(0 0 0 / .38);
}

.modal__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 12px;
}

.modal__title {
    margin: 0;
    font-size: 15px;
}

.modal__trail {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 4px;
    margin-bottom: 10px;
}

.modal__crumb {
    padding: 3px 7px;
    border: 0;
    border-radius: 6px;
    background: none;
    font: inherit;
    font-size: 12px;
    color: var(--q-violet);
    cursor: pointer;
}

.modal__crumb:hover:not(:disabled) {
    background: var(--bg-accent);
}

.modal__crumb:disabled {
    color: var(--text-muted);
    cursor: default;
}

.modal__sep {
    font-size: 11px;
    color: var(--text-muted);
}

.modal__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}

/* Folders down the left, what is in the opened one on the right. Each pane scrolls on its own,
   so a deep tree does not push the contents of the folder being read off the bottom. */
.modal__body--split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr);
    gap: 0;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: 12px;
}

.explorer__pane {
    min-width: 0;
    padding: 6px;
    overflow-y: auto;
}

.explorer__pane + .explorer__pane {
    border-left: 1px solid var(--border);
    background: var(--surface-sunken);
}

/* The way back to the top of the tree, above the folders rather than among them — it is not a
   folder, and giving it a row of its own is what makes that visible. */
.explorer__root {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 6px 8px;
    border: 0;
    border-radius: 8px;
    background: none;
    font: inherit;
    font-size: 12.5px;
    text-align: left;
    color: var(--text-soft);
    cursor: pointer;
}

.explorer__root:hover {
    background: var(--bg-accent);
    color: var(--text);
}

.explorer__root[aria-current="true"] {
    background: color-mix(in srgb, var(--q-violet) 12%, transparent);
    color: var(--q-violet);
}

/* Which folder the right-hand pane is answering for, and the way to take that folder itself. */
.explorer__head {
    position: sticky;
    top: -6px;
    z-index: 1;
    display: flex;
    align-items: center;
    gap: 8px;
    margin: -6px -6px 4px;
    padding: 8px 10px;
    background: var(--surface-sunken);
    border-bottom: 1px solid var(--border);
}

.explorer__title {
    flex: 1 1 auto;
    min-width: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* One row per item: what it is, what it is called, and where it sits. */
.itemtree__row {
    display: flex;
    align-items: stretch;
    gap: 4px;
    border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
}

.itemtree__pick {
    display: grid;
    grid-template-columns: 24px minmax(120px, 1.2fr) minmax(0, 2fr);
    align-items: center;
    gap: 8px;
    flex: 1 1 auto;
    min-width: 0;
    padding: 7px 8px;
    border: 0;
    border-radius: 8px;
    background: none;
    font: inherit;
    text-align: left;
    color: var(--text);
    cursor: pointer;
}

.itemtree__pick:hover {
    background: var(--bg-accent);
}

.itemtree__pick[aria-current="true"] {
    background: color-mix(in srgb, var(--q-violet) 12%, transparent);
    color: var(--q-violet);
}

/* Held while several are being taken, before the dialog is finished with. Stronger than the
   aria-current tint above, which means "you are looking at this" rather than "you have taken it". */
.itemtree__pick[data-on="true"] {
    background: color-mix(in srgb, var(--q-violet) 20%, transparent);
    box-shadow: inset 2px 0 0 var(--q-violet);
}

/* Already on the field before this dialog opened. A quieter mark than the one above, and a
   different colour, because it is a fact about the record rather than something done just now. */
.itemtree__pick[data-held="true"] {
    box-shadow: inset 2px 0 0 var(--q-green, #2f855a);
}

.itemtree__held {
    padding: 1px 6px;
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--q-green, #2f855a) 16%, transparent);
    font-size: 10px;
    letter-spacing: .03em;
    color: var(--q-green, #2f855a);
    white-space: nowrap;
}

/* ------------------------------------------------------------------ what a field points at */

/* One per line, not a run of chips. These hold content item titles, which are prose rather than
   one-word vocabulary values, and a wrapped row of those is a wall to read and a small target. */
.fieldlinks {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin: 0 0 8px;
    padding: 0;
    list-style: none;
}

.fieldlinks__row {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 6px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--surface-sunken);
}

.fieldlinks__go {
    display: flex;
    align-items: center;
    gap: 7px;
    flex: 1 1 auto;
    min-width: 0;
    color: var(--text);
    text-decoration: none;
}

.fieldlinks__go:hover .fieldlinks__name {
    color: var(--q-violet);
    text-decoration: underline;
}

.fieldlinks__name {
    overflow: hidden;
    font-size: 13px;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.fieldlinks__off {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--text-muted);
    cursor: pointer;
}

.fieldlinks__off:hover:not(:disabled) {
    background: var(--q-red);
    color: #fff;
}

.fieldlinks__off:disabled {
    cursor: not-allowed;
    opacity: .45;
}

.fieldlinks__none {
    padding: 4px 0;
    font-size: 12px;
    color: var(--text-muted);
}

.fieldlinks__thumb {
    width: 28px;
    height: 28px;
    flex: 0 0 auto;
    border-radius: 4px;
    object-fit: cover;
}

.fieldlinks__meta {
    margin-left: auto;
    padding-left: 8px;
    font-family: var(--font-mono);
    font-size: 10px;
    color: var(--text-muted);
    white-space: nowrap;
}

/* ------------------------------------------------------------------ choosing a content type */

/* The list's own box is the dropdown panel's now — see .pickdrop__panel. What is left here is the
   row, which is the part worth keeping and the part every caller recognises. */
.typepick {
    min-width: 0;
}

.typepick__row {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 6px 8px;
    border: 0;
    border-radius: 8px;
    background: none;
    font: inherit;
    text-align: left;
    color: var(--text);
    cursor: pointer;
}

.typepick__row:hover:not(:disabled) {
    background: var(--bg-accent);
}

.typepick__row[data-on="true"] {
    background: color-mix(in srgb, var(--q-violet) 14%, transparent);
    box-shadow: inset 2px 0 0 var(--q-violet);
}

.typepick__row:disabled {
    cursor: not-allowed;
    opacity: .55;
}

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

.typepick__name {
    font-size: 13px;
}

.typepick__sub {
    overflow: hidden;
    font-size: 11px;
    color: var(--text-muted);
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* How many fields the type has, where the caller passes it. Only the new item dialog does: it is
   choosing what to make, and a type with two fields and a type with sixty are different answers. */
.typepick__count {
    flex: none;
    margin-left: auto;
    padding-left: 8px;
    font-size: 11px;
    color: var(--text-muted);
    white-space: nowrap;
}

/* Nothing chosen yet. Reads as a placeholder rather than as a value. */
.typepick__name--none {
    color: var(--text-muted);
}

/* ------------------------------------------------------------------ a picker behind a trigger

   The trigger is an .input so it lines up with every other control on a form, and the panel is a
   .menu--floating so q90.openMenu can place it in the top layer. The panel is deliberately not
   sized from the trigger: a row carries a name and a description and wants more room than the
   button showing one of them. */

.pickdrop {
    min-width: 0;
}

.pickdrop__trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    width: 100%;
    text-align: left;
    cursor: pointer;
}

.pickdrop__trigger:disabled {
    cursor: not-allowed;
}

/* The chosen thing, drawn by whoever is choosing — an icon and a name, a row of chips. Truncated
   rather than wrapped: a trigger that grows a second line moves every control under it. */
.pickdrop__what {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* The layout goes on :popover-open and not on the element, which is the whole of what keeps a shut
   panel out of the page. A closed popover is hidden by the browser's own
   `[popover]:not(:popover-open) { display: none }` — an author `display` beats that rule, so setting
   it unconditionally left an empty 420px box sitting under every trigger. Every other .menu in this
   stylesheet gets this right by never declaring display at all. */
.pickdrop__panel {
    width: min(420px, calc(100vw - 24px));
    padding: 8px;
}

.pickdrop__panel:popover-open {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.pickdrop__list {
    display: flex;
    flex-direction: column;
    gap: 2px;
    max-height: 320px;
    overflow-y: auto;
}

/* ------------------------------------------------------------------ the reference selectors

   The rows are the content type picker's — .typepick__row and its two lines of text — because they
   are the same shape and one row style across every picker in the form is the point. What is here
   is only the trigger's contents, which differ: several may be chosen, and the chosen set is the
   answer rather than something to count. */

.refpick {
    min-width: 0;
}

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

.refpick__chip {
    padding: 1px 7px;
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    font-size: 12px;
    white-space: nowrap;
}

/* Stored, and not in the list this control can see. Marked rather than dropped, and marked rather
   than drawn as though it resolved: the value is real and the name for it is not available. */
.refpick__unknown {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--text-muted);
}

/* ------------------------------------------------------------------ choosing an asset */

/* The switch is not a field and must not be stretched like one: .formrow .field grows to fill, and
 * with nothing holding the switch's own width it shrank to almost nothing and drew its track and its
 * label on top of the select beside it. */
.assetpick__tools {
    align-items: center;
    gap: 10px;
}

.assetpick__tools .field {
    flex: 1 1 14rem;
    min-width: 0;
}

.assetpick__tools .field--s {
    flex: 0 1 12rem;
}

/* A select sizes itself to its widest option and ignores the cell it is in: with an asset type named
 * "Customer video asset screenshot" it came out 257px wide inside a 192px field and ran straight
 * through the switch beside it. The name is truncated instead — the full one is still in the list
 * when it is open, which is where somebody reads it. */
.assetpick__tools .field > .input {
    width: 100%;
    min-width: 0;
    max-width: 100%;
    text-overflow: ellipsis;
}

/* Never squeezed, and allowed to drop to its own line. A label holding a track and a word has no
 * useful narrow form: shrink it and the two draw over whatever is beside them, which is what it did
 * on top of the kind filter. Wrapping is the honest answer at widths where all three will not fit. */
.assetpick__tools > .switch {
    flex: 0 0 auto;
    white-space: nowrap;
}

/* Why the listing is narrower than the folder, and what is odd about a choice. Both sit under the
 * tools rather than beside them: a filter that narrows the list without a word reads as an empty
 * folder, and a mismatch nothing upstream refuses has nowhere else to be said. */
.assetpick__note {
    margin: 0;
    padding: 6px 2px 0;
    font-size: 12px;
    color: var(--text-muted);
}

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

.assetpick__note[data-tone="warning"] {
    color: color-mix(in srgb, var(--q-amber) 72%, var(--text));
}

.assetpick__note[data-tone="warning"] strong {
    color: var(--q-amber);
}

.assetpick__pager {
    display: flex;
    align-items: center;
    gap: 10px;
    padding-top: 8px;
}

.assetpick__count {
    margin-right: auto;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .04em;
    color: var(--text-muted);
}

/* An attached asset that the explorer can be opened on. */
.fieldassets__name--go {
    color: var(--text);
    text-decoration: none;
}

.fieldassets__name--go:hover {
    color: var(--q-violet);
    text-decoration: underline;
}

/* Opens a branch in place. A spacer of the same width stands in for it on anything that has no
   branch, so every name down a level starts at the same place. */
.itemtree__twist {
    flex: none;
    width: 20px;
    border: 0;
    border-radius: 6px;
    background: none;
    font-size: 10px;
    color: var(--text-muted);
    cursor: pointer;
    transition: transform 120ms var(--step-cubic), color 120ms var(--step-cubic);
}

button.itemtree__twist:hover {
    color: var(--text);
}

.itemtree__twist[aria-expanded="true"] {
    transform: rotate(90deg);
    color: var(--text);
}

.itemtree__name {
    min-width: 0;
    font-size: 12.5px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.itemtree__meta {
    min-width: 0;
    font-size: 11px;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    direction: rtl;
    text-align: left;
}

.itemtree__open {
    flex: 0 0 auto;
    width: 30px;
    border: 0;
    border-radius: 8px;
    background: none;
    font-size: 15px;
    color: var(--text-muted);
    cursor: pointer;
}

.itemtree__open:hover {
    background: var(--bg-accent);
    color: var(--text);
}

/* ------------------------------------------------------------------------------ icons

   A grid, because an icon is recognised rather than read. */
.iconpick {
    position: relative;
    display: flex;
    align-items: center;
    gap: 8px;
}

.iconpick__current {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-width: 150px;
    padding: 7px 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius-field);
    background: var(--surface);
    font: inherit;
    font-size: 12.5px;
    color: var(--text);
    cursor: pointer;
}

.iconpick__current:hover:not(:disabled) {
    border-color: var(--border-strong);
}

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

.iconpick__panel {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 40;
    width: 320px;
    max-width: 80vw;
    padding: 10px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface);
    box-shadow: 0 14px 34px rgb(0 0 0 / .3);
}

.iconpick__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(38px, 1fr));
    gap: 4px;
    max-height: 220px;
    margin-top: 8px;
    overflow-y: auto;
}

.iconpick__option {
    display: grid;
    place-items: center;
    aspect-ratio: 1;
    border: 1px solid transparent;
    border-radius: 8px;
    background: none;
    color: var(--text-soft);
    cursor: pointer;
}

.iconpick__option:hover {
    border-color: var(--border-strong);
    background: var(--surface);
    color: var(--text);
}

.iconpick__option[aria-selected="true"] {
    border-color: var(--q-violet);
    color: var(--q-violet);
}

/* The icon column in the content type list — its own column rather than a mark beside each
   name, so the icons line up under one another and a type without one is visibly without one. */
.admintable__glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--text-muted);
}

/* ------------------------------------------------------------------------------ glyphs

   A content item drawn the way a file explorer draws one: the shape says what it holds, the
   content type's own icon rides on the face of that shape.

   Which of the two is read first decides how they are coloured. The shape is one bit of
   information and is answered by its colour alone — amber or violet, told apart across a whole
   column without looking at any of them. The emblem is the one that has to be recognised, so it
   is the darkest mark in the glyph and the only one drawn in the text colour. Both grey and both
   the same size, which is what they were, left the emblem competing with the outline it sits on
   at a size where neither won. */
.glyph {
    position: relative;
    display: inline-grid;
    place-items: center;
    flex: none;
    color: var(--glyph-item);
}

.glyph[data-kind="folder"] {
    color: var(--glyph-folder);
}

.glyph__emblem {
    position: absolute;
    left: 50%;
    top: 58%;
    display: inline-flex;
    transform: translate(-50%, -50%);
    color: var(--text);
}

/* A folder's face is the lower part of the shape; a document's body is nearly all of it. */
.glyph[data-kind="folder"] .glyph__emblem {
    top: 63%;
}

/* Both forms of the icon in the tree they will be seen in — a grid of icons at 20px says
   nothing about how one reads on the face of a folder. */
.glyphtree {
    padding: 7px 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius-field);
    background: var(--surface);
}

.glyphtree__row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 3px 0;
}

.glyphtree__row--child {
    padding-left: 20px;
}

.glyphtree__twist {
    flex: none;
    width: 12px;
    font-size: 10px;
    color: var(--text-muted);
    text-align: center;
}

.glyphtree__name {
    min-width: 0;
    font-size: 12.5px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.glyphtree__kind {
    margin-left: auto;
    padding-left: 12px;
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* ------------------------------------------------------------------------------ waiting

   Wherever something is on its way. A line of text saying "Loading…" reads the same whether the
   read is in flight or died three minutes ago; a mark that moves does not. */
.loading {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 14px 2px;
    font-size: 12.5px;
    color: var(--text-muted);
}

.loading[data-compact="true"] {
    padding: 5px 2px;
    font-size: 12px;
}

.loading__spin {
    flex: none;
    width: 15px;
    height: 15px;
    border-radius: 50%;
    border: 2px solid color-mix(in srgb, var(--q-violet) 24%, transparent);
    border-top-color: var(--q-violet);
    animation: q90-spin 720ms linear infinite;
}

.loading[data-compact="true"] .loading__spin {
    width: 12px;
    height: 12px;
}

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

@keyframes q90-pulse {
    50% {
        opacity: .2;
    }
}

/* Still obviously alive, without a thing turning in the corner of someone's eye. */
@media (prefers-reduced-motion: reduce) {
    .loading__spin {
        border-color: transparent;
        background: var(--q-violet);
        animation: q90-pulse 1.2s ease-in-out infinite;
    }
}

/* The whole window, once an action has been running long enough that showing nothing would read
   as nothing happening. Set from the document by q90.js rather than rendered, so it appears on
   every page and whether or not a circuit is listening — including while one is still starting,
   which is exactly when a read is most likely to be slow. */
:root[data-q90-busy="true"]::after {
    content: "";
    position: fixed;
    inset: 0 0 auto 0;
    height: 2px;
    z-index: 90;
    pointer-events: none;
    background-image: linear-gradient(90deg, transparent, var(--q-lime), transparent);
    background-size: 42% 100%;
    background-repeat: no-repeat;
    animation: q90-sweep 1.1s var(--step-cubic) infinite;
}

@keyframes q90-sweep {
    from {
        background-position: -45% 0;
    }

    to {
        background-position: 145% 0;
    }
}

@media (prefers-reduced-motion: reduce) {
    :root[data-q90-busy="true"]::after {
        background-image: linear-gradient(90deg, var(--q-lime), var(--q-lime));
        background-size: 100% 100%;
        animation: q90-pulse 1.4s ease-in-out infinite;
    }
}

/* ------------------------------------------------------------------------------ status bar

   Along the bottom: what the application is doing, who is signed in, which clock the times on
   screen are on, and what has gone wrong. */
.statusbar {
    position: sticky;
    bottom: 0;
    z-index: 35;
    display: flex;
    align-items: center;
    gap: 8px;
    height: var(--statusbar-h);
    padding: 0 12px;
    background: var(--surface);
    border-top: 1px solid var(--border);
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: .04em;
    color: var(--text-muted);
}

.statusbar__spacer {
    flex: 1;
}

.statusbar__sep {
    opacity: .5;
}

.statusbar__fact {
    max-width: 220px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.statusbar__state {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex: none;
}

.statusbar__state[data-busy="true"] {
    color: var(--q-violet);
}

/* Lit rather than shouting: idle is the usual state and should not draw the eye. */
.statusbar__dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--q-lime);
}

.statusbar__spin {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    border: 1.5px solid color-mix(in srgb, var(--q-violet) 30%, transparent);
    border-top-color: var(--q-violet);
    animation: q90-spin 720ms linear infinite;
}

@media (prefers-reduced-motion: reduce) {
    .statusbar__spin {
        border-color: transparent;
        background: var(--q-violet);
        animation: q90-pulse 1.2s ease-in-out infinite;
    }
}

.statusbar__problems {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex: none;
    padding: 2px 8px;
    border: 1px solid transparent;
    border-radius: var(--radius-pill);
    background: none;
    font: inherit;
    color: var(--text-muted);
    cursor: pointer;
}

.statusbar__problems:hover {
    border-color: var(--border);
    color: var(--text);
}

.statusbar__problems[data-any="true"] {
    background: color-mix(in srgb, var(--q-red) 14%, transparent);
    color: var(--q-red);
}

/* Opens upwards, over the page rather than the bar — the bar is one line high and the log is
   the thing being read. */
.statuslog {
    position: absolute;
    right: 8px;
    bottom: calc(var(--statusbar-h) + 6px);
    z-index: 61;
    display: flex;
    flex-direction: column;
    width: min(680px, calc(100vw - 32px));
    max-height: min(420px, 70dvh);
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface);
    box-shadow: 0 18px 44px rgb(0 0 0 / .34);
    font-family: var(--font-body);
    letter-spacing: normal;
}

.statuslog__head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 12px;
    border-bottom: 1px solid var(--border);
}

.statuslog__title {
    margin: 0;
    font-size: 13px;
}

.statuslog__tally {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--text-muted);
}

.statuslog__body {
    flex: 1 1 auto;
    min-height: 0;
    padding: 4px;
    overflow-y: auto;
}

.statuslog__row + .statuslog__row {
    border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
}

.statuslog__line {
    display: grid;
    grid-template-columns: 62px 44px minmax(0, 1fr);
    align-items: baseline;
    gap: 10px;
    width: 100%;
    padding: 7px 8px;
    border: 0;
    border-radius: 8px;
    background: none;
    font: inherit;
    font-size: 12.5px;
    text-align: left;
    color: var(--text);
    cursor: pointer;
}

.statuslog__line:hover {
    background: var(--bg-accent);
}

.statuslog__at {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--text-muted);
}

.statuslog__code {
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 700;
}

.statuslog__code[data-tone="danger"] {
    color: var(--q-red);
}

.statuslog__code[data-tone="muted"] {
    color: var(--text-muted);
}

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

.statuslog__detail {
    padding: 0 8px 10px;
}

.statuslog__where {
    display: block;
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--q-violet);
    overflow-wrap: anywhere;
}

/* The body as it arrived. Wrapped rather than scrolled sideways: what is wrong with it is
   usually a sentence somewhere in the middle. */
.statuslog__raw {
    max-height: 190px;
    margin: 6px 0 0;
    padding: 8px 10px;
    border-radius: 8px;
    background: var(--surface-sunken);
    font-family: var(--font-mono);
    font-size: 11px;
    line-height: 1.5;
    color: var(--text-soft);
    overflow: auto;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.statuslog__none {
    margin: 6px 0 0;
    font-size: 11.5px;
    color: var(--text-muted);
}

/* ------------------------------------------------------------------------------ utilities

   Present to a screen reader, absent to everyone else — for a label a control still needs to
   name it by, on a bar with no room to draw one. */
.a11y-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* ------------------------------------------------------------------------------ media explorer

   A workbench that fills the window rather than a page that scrolls: a file explorer is somewhere
   you stay, and the tree, the listing and the thing you opened all have to be reachable without
   losing either of the others. So the three panes each scroll on their own and the page itself
   does not scroll at all.

   Everything here is scoped to .mediapage, because that height behaviour is exactly wrong for the
   card pages and they share a lot of the same class names. */
.mediapage {
    --media-gap: 12px;
    display: flex;
    flex-direction: column;
    gap: var(--media-gap);
    /* The window, less the top bar, the status bar and the padding this page sits in. */
    height: calc(100dvh - 64px - var(--impersonating-h) - var(--statusbar-h) - 32px);
    min-height: 420px;
}

/* This page manages its own scrolling, so the shell must not add any of its own. */
.content:has(.mediapage) {
    padding: 16px 20px 8px;
    overflow: hidden;
}

.page__header--tight {
    margin-bottom: 0;
}

.page__header--tight .page__eyebrow {
    margin-bottom: 2px;
}

/* ------------------------------------------------------------------------ the bar */

.mediabar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--media-gap);
    flex-wrap: wrap;
}

.mediabar__tools {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.input--search {
    width: 220px;
    height: 32px;
    padding-block: 0;
}

/* Where you are, as a path you can step back up. Buttons rather than text, because every crumb
   but the last one is somewhere to go. */
.mediacrumbs {
    display: flex;
    align-items: center;
    gap: 2px;
    flex-wrap: wrap;
    min-width: 0;
}

.mediacrumbs__crumb {
    padding: 4px 8px;
    border: 0;
    border-radius: 7px;
    background: none;
    font: inherit;
    font-size: 12.5px;
    color: var(--text-soft);
    cursor: pointer;
    white-space: nowrap;
    transition: background 120ms var(--step-cubic), color 120ms var(--step-cubic);
}

.mediacrumbs__crumb:hover {
    background: var(--bg-accent);
    color: var(--text);
}

.mediacrumbs__crumb[aria-current="true"] {
    color: var(--text);
    font-weight: 700;
}

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

/* --------------------------------------------------------------------- the panes */

/* The columns themselves are set further down, from the --pane-widths custom property the
   resize handles write to. */
.mediapanes {
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    gap: var(--media-gap);
}

/* Below this there is no room for three, and the detail pane is the one that can be reached
   another way, so it drops under the listing rather than squeezing all three. */
@media (max-width: 1500px) {
    .mediapanes {
        grid-template-rows: minmax(0, 1fr) minmax(0, 0.9fr);
    }

    .mediapanes__detail {
        grid-column: 1 / -1;
    }

    /* The detail pane is a share of a row down here rather than the full height of the page, and
       the preview does not scroll away: at 46dvh it would take the whole pane and leave the column
       under it nothing to scroll in. */
    .mediadetail__stage {
        --preview-max: 24dvh;
    }
}

@media (max-width: 900px) {
    .mediapage {
        height: auto;
    }

    .content:has(.mediapage) {
        overflow: visible;
    }

    .mediapanes {
        grid-template-rows: none;
    }

    .mediapanes__tree,
    .mediapanes__list,
    .mediapanes__detail {
        max-height: 70dvh;
    }

    .mediadetail__stage {
        --preview-max: 34dvh;
    }
}

.mediapanes__tree,
.mediapanes__list,
.mediapanes__detail {
    display: flex;
    flex-direction: column;
    min-height: 0;
    min-width: 0;
    padding: 10px;
    gap: 8px;
}

.mediapanes__head {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--border);
}

.mediapanes__title {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: -.01em;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mediapanes__selected {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--q-violet);
}

.mediapanes__acts {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-left: auto;
    flex-wrap: wrap;
}

.mediapanes__scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
}

.mediapanes__foot {
    display: flex;
    gap: 6px;
    padding-top: 8px;
    border-top: 1px solid var(--border);
    flex-wrap: wrap;
}

/* An inline row for naming a folder, rather than a dialog. Making a folder is a half-second
   thought in the middle of filing something; a scrim over the whole window is not. */
.mediainline {
    display: flex;
    gap: 6px;
    align-items: center;
}

.mediainline .input {
    flex: 1 1 auto;
    min-width: 0;
    height: 30px;
    padding-block: 0;
}

/* The name on one line and where it is going on the next, because the second is a sentence and
   would otherwise squeeze the box you are typing in. */
.mediainline--stack {
    flex-direction: column;
    align-items: stretch;
    gap: 5px;
}

.mediainline--stack > .mediainline {
    width: 100%;
}

.mediainline__where {
    font-size: 11.5px;
    color: var(--text-muted);
}

.mediatoggle {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 11.5px;
    color: var(--text-muted);
    cursor: pointer;
    white-space: nowrap;
}

.mediatoggle input {
    accent-color: var(--q-violet);
    margin: 0;
}

/* --------------------------------------------------------------------- the tree */

.mediatree {
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.mediatree__branch {
    display: contents;
}

/* The indent is a variable rather than nested padding, so a row keeps its whole width as a drop
   target however deep it sits: a target that narrows as you go down is one you keep missing. */
.mediatree__row {
    display: flex;
    align-items: center;
    gap: 2px;
    padding-left: calc(var(--depth, 0) * 13px);
    border-radius: 8px;
    border: 1px solid transparent;
}

.mediatree__row[data-drop="true"] {
    border-color: var(--q-violet);
    background: color-mix(in srgb, var(--q-violet) 14%, transparent);
}

.mediatree__twist {
    flex: none;
    width: 18px;
    height: 26px;
    border: 0;
    border-radius: 6px;
    background: none;
    font-size: 10px;
    color: var(--text-muted);
    cursor: pointer;
    transition: transform 120ms var(--step-cubic), color 120ms var(--step-cubic);
}

.mediatree__twist:hover {
    color: var(--text);
}

.mediatree__twist[aria-expanded="true"] {
    transform: rotate(90deg);
    color: var(--text);
}

.mediatree__spinner {
    display: inline-block;
    width: 9px;
    height: 9px;
    border: 1.5px solid color-mix(in srgb, var(--q-violet) 34%, transparent);
    border-top-color: var(--q-violet);
    border-radius: 50%;
    animation: mediaspin 640ms linear infinite;
}

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

.mediatree__pick {
    display: flex;
    align-items: center;
    gap: 7px;
    flex: 1 1 auto;
    min-width: 0;
    padding: 4px 7px;
    border: 0;
    border-radius: 7px;
    background: none;
    font: inherit;
    font-size: 12.5px;
    text-align: left;
    color: var(--text);
    cursor: pointer;
}

.mediatree__pick:hover:not(:disabled) {
    background: var(--bg-accent);
}

.mediatree__pick:disabled {
    color: var(--text-muted);
    cursor: not-allowed;
    opacity: .55;
}

.mediatree__pick[aria-current="true"] {
    background: color-mix(in srgb, var(--q-violet) 13%, transparent);
    color: var(--q-violet);
    font-weight: 600;
}

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

.mediatree__empty,
.mediatree__none {
    margin: 4px 0;
    padding-left: calc(var(--depth, 0) * 13px + 22px);
    font-size: 11.5px;
    color: var(--text-muted);
}

.mediatree--picker {
    max-height: 46dvh;
    overflow: auto;
}

/* ------------------------------------------------------------------ the listing */

.medialist {
    height: 100%;
}

.medialist:focus-visible {
    outline: none;
}

.medialist__empty {
    margin: 22px 0;
    text-align: center;
    font-size: 12.5px;
    color: var(--text-muted);
}

/* Tiles. auto-fill rather than a fixed count, so a narrowed pane re-flows instead of scrolling
   sideways; 16:9 frames, because that is the shape almost everything in here actually is. */
.mediagrid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
    gap: 10px;
    padding: 2px;
}

.mediatile {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 6px;
    border: 1px solid transparent;
    border-radius: 12px;
    cursor: pointer;
    user-select: none;
    transition: background 120ms var(--step-cubic), border-color 120ms var(--step-cubic);
}

.mediatile:hover {
    background: var(--bg-accent);
}

.mediatile[data-selected="true"] {
    background: color-mix(in srgb, var(--q-violet) 12%, transparent);
    border-color: color-mix(in srgb, var(--q-violet) 42%, transparent);
}

/* Open and selected are different states and are drawn differently: a ring for the one being
   read, a wash for the ones an action would apply to. */
.mediatile[data-open="true"] {
    border-color: var(--q-violet);
    box-shadow: var(--focus-ring);
}

.mediatile__frame {
    position: relative;
    display: grid;
    place-items: center;
    aspect-ratio: 16 / 9;
    border-radius: 9px;
    overflow: hidden;
    background:
        linear-gradient(140deg,
            color-mix(in srgb, var(--q-violet) 12%, var(--surface-sunken)),
            var(--surface-sunken));
    color: var(--text-muted);
}

.mediatile__image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    background: #000;
}

/* Under whatever picture loads, so a failed load leaves the extension showing rather than an
   empty box: the same trick the avatar plays with initials. */
.mediatile__kindmark {
    position: absolute;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .14em;
    color: var(--text-muted);
    opacity: .75;
}

.mediatile__glyph {
    position: relative;
    color: var(--glyph-item);
}

.mediatile__badge {
    position: absolute;
    left: 5px;
    bottom: 5px;
    padding: 1px 6px;
    border-radius: var(--radius-pill);
    font-family: var(--font-mono);
    font-size: 8.5px;
    letter-spacing: .1em;
    text-transform: uppercase;
    background: color-mix(in srgb, var(--q-amber) 82%, transparent);
    color: #14132B;
}

.mediatile__foot {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}

.mediatile__name {
    font-size: 12px;
    line-height: 1.3;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mediatile__meta {
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .06em;
    color: var(--text-muted);
}

/* Rows, for when the answer is a date or a size rather than a picture. */
.mediarows {
    display: grid;
    grid-template-columns: var(--cols);
    align-items: center;
}

.mediarows__head {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
    padding: 4px 8px;
    border-bottom: 1px solid var(--border);
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--text-muted);
    position: sticky;
    top: 0;
    background: var(--surface);
    z-index: 1;
}

.mediarows__row {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
    align-items: center;
    gap: 8px;
    padding: 5px 8px;
    border-radius: 7px;
    border: 1px solid transparent;
    cursor: pointer;
    user-select: none;
    font-size: 12.5px;
}

.mediarows__row:hover {
    background: var(--bg-accent);
}

.mediarows__row[data-selected="true"] {
    background: color-mix(in srgb, var(--q-violet) 12%, transparent);
}

.mediarows__row[data-open="true"] {
    border-color: var(--q-violet);
}

.mediarows__glyph {
    display: inline-flex;
    color: var(--glyph-item);
}

.mediarows__name {
    display: flex;
    align-items: center;
    gap: 7px;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mediarows__cell {
    font-size: 11.5px;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mediapager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding-top: 8px;
    border-top: 1px solid var(--border);
    font-size: 12px;
    color: var(--text-muted);
}

/* ---------------------------------------------------------------- dropping files */

/* The whole page is the target, not a strip of it: somebody dragging a folder in from the desktop
   aims at the window, and a narrow drop zone is one more thing to have to hit. */
.mediapage[data-dropping="true"] .mediapanes__scroll--drop {
    outline: 2px dashed var(--q-violet);
    outline-offset: -6px;
    border-radius: 10px;
    background: color-mix(in srgb, var(--q-violet) 7%, transparent);
}

.mediahidden {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.mediauploads {
    display: flex;
    flex-direction: column;
    gap: 4px;
    max-height: 132px;
    overflow-y: auto;
    padding: 8px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface-sunken);
}

.mediauploads__row {
    display: grid;
    grid-template-columns: minmax(0, 1.4fr) minmax(70px, 1fr) 64px;
    align-items: center;
    gap: 8px;
    font-size: 11.5px;
}

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

.mediauploads__state {
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .06em;
    color: var(--text-muted);
    text-align: right;
}

.mediauploads__row[data-state="failed"] .mediauploads__state {
    color: var(--q-red);
}

/* One meter, used by uploads, ingest steps and the run timeline alike. Named against its parents
   because .mediabar on its own is the toolbar at the top of the page. */
.mediauploads .mediabar,
.mediasteps .mediabar,
.mediatimeline .mediabar,
.mediaruns .mediabar {
    display: block;
    height: 4px;
    border-radius: var(--radius-pill);
    background: var(--bg-accent);
    overflow: hidden;
    margin: 0;
}

.mediabar__fill {
    display: block;
    height: 100%;
    border-radius: inherit;
    background: var(--q-violet);
    transition: width 180ms var(--step-cubic);
}

.mediabar__fill[data-tone="good"] { background: var(--q-lime); }
.mediabar__fill[data-tone="warn"] { background: var(--q-amber); }
.mediabar__fill[data-tone="bad"] { background: var(--q-red); }
.mediabar__fill[data-tone="muted"] { background: var(--border-strong); }

/* ------------------------------------------------------------------- the detail */

/* The pane is a preview that stays and a column that scrolls: the stage keeps its own height and
   everything below it shares one scrollbar. flex: 1 1 auto on that column with min-height: 0 is
   what makes it scroll rather than grow — left to its default basis a flex child is as tall as its
   contents and simply runs off the bottom of the pane, and the overflow never has anything to act
   on. Nothing inside it scrolls on its own, so an expanded panel is read by scrolling the pane. */
.mediadetail {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    gap: 10px;
    min-height: 0;
    overflow: hidden;
}

.mediadetail__scroll {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    gap: 10px;
    min-height: 0;
    overflow-y: auto;
    scrollbar-gutter: stable;
    padding-right: 2px;
}

/* Nothing in this column gives up height, and this is the line that made the pane look broken.
   A flex item is normally floored at its own content, which is why the facts and the derived list
   were fine — but that floor is zero for an item whose overflow is not visible, and a folding
   panel clips to its rounded corners. So the two panels, and only the two panels, were shrunk by
   the flex algorithm to whatever was left over and had the rest of themselves cut off: a run was
   read through a slot, and the pane's own scrollbar never appeared because as far as it was
   concerned everything fitted. Shrinking off, the column is as tall as what is in it and the
   overflow has something to act on. */
.mediadetail__scroll > * {
    flex: 0 0 auto;
}

/* Black in both themes: a preview is a picture, and the surface it sits on has to be neutral or
   every frame is judged against a tinted ground.

   No fixed aspect ratio. The stage used to be 16:9 whatever it held, so a 4:3 master or a portrait
   clip was letterboxed into a shape it never had and the pane gave up that height regardless. The
   media now decides its own height and is centred in whatever width there is; the floor keeps the
   "no preview" states from collapsing, and the ceiling stops a tall clip taking the whole pane.

   flex: none because it is the pane's head now: it does not scroll away and it does not give up
   height to what is under it. The ceiling is what keeps that honest — see the breakpoints, where
   the pane is short enough that 46dvh would leave the column nothing to scroll in. */
.mediadetail__stage {
    --preview-max: 46dvh;
    position: relative;
    display: grid;
    place-items: center;
    flex: none;
    min-height: 132px;
    border-radius: 11px;
    overflow: hidden;
    background: #05050B;
    color: var(--rail-muted);
}

/* max-* only, and no width/height: an img and a video both carry their own dimensions, so
   constraining the box on both axes is what keeps the ratio without object-fit having to
   letterbox anything back.

   "auto 16 / 9" means use the real ratio and fall back to 16:9 — which only bites before a video
   has read its metadata, when the element is otherwise 300×150 by definition and would resize
   under the reader a moment later. */
.mediadetail__image,
.mediadetail__video {
    max-width: 100%;
    max-height: var(--preview-max);
    aspect-ratio: auto 16 / 9;
    background: #05050B;
}

.mediadetail__audio {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 0 16px;
    color: var(--rail-text);
}

.mediadetail__audio audio {
    width: 100%;
}

.mediadetail__absent {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    text-align: center;
    padding: 12px;
}

.mediadetail__absent span {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--rail-text);
}

.mediadetail__absent p {
    margin: 0;
    font-size: 11.5px;
    color: var(--rail-muted);
    max-width: 30ch;
}

.mediadetail__head {
    display: flex;
    flex-direction: column;
    gap: 5px;
}

.mediadetail__title {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    line-height: 1.35;
    overflow-wrap: anywhere;
}

.mediadetail__marks {
    display: flex;
    gap: 5px;
    flex-wrap: wrap;
}

.mediadetail__actions {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
}

/* Label and value, in two columns while there is room and stacked when there is not. */
.mediafacts {
    display: grid;
    grid-template-columns: minmax(88px, max-content) minmax(0, 1fr);
    gap: 3px 12px;
    margin: 0;
    font-size: 12px;
}

.mediafacts__pair {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
    align-items: baseline;
}

.mediafacts dt {
    color: var(--text-muted);
    font-size: 11.5px;
}

.mediafacts dd {
    margin: 0;
    min-width: 0;
    overflow-wrap: anywhere;
}

/* The note that a QC timestamp is the QC server clock, kept quiet beside the value it qualifies. */
.mediafacts dd em {
    font-style: normal;
    font-family: var(--font-mono);
    font-size: 9px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-left: 6px;
}

.mediafacts--tight {
    font-size: 11.5px;
    margin-top: 6px;
}

.mediafacts__id {
    font-family: var(--font-mono);
    font-size: 10px;
    color: var(--text-muted);
}

@container (max-width: 300px) {
    .mediafacts {
        grid-template-columns: minmax(0, 1fr);
    }
}

.mediaderived {
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.mediaderived__title {
    margin: 4px 0 2px;
    font-size: 11px;
    font-family: var(--font-mono);
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.mediaderived__row {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 4px 6px;
    border: 0;
    border-radius: 7px;
    background: none;
    font: inherit;
    font-size: 12px;
    text-align: left;
    color: var(--text);
    cursor: pointer;
}

.mediaderived__row:hover {
    background: var(--bg-accent);
}

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

.mediaderived__meta {
    font-family: var(--font-mono);
    font-size: 9.5px;
    color: var(--text-muted);
}

/* ------------------------------------------------------------- folding panels */

.mediapanel {
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
}

.mediapanel__toggle,
.mediafold__toggle {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 8px 10px;
    border: 0;
    background: var(--surface-sunken);
    font: inherit;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--text-soft);
    cursor: pointer;
    text-align: left;
}

.mediapanel__toggle:hover,
.mediafold__toggle:hover {
    color: var(--text);
}

.mediapanel__toggle::before,
.mediafold__toggle::before {
    content: "\25B8";
    font-size: 9px;
    color: var(--text-muted);
    transition: transform 120ms var(--step-cubic);
}

.mediapanel__toggle[aria-expanded="true"]::before,
.mediafold__toggle[aria-expanded="true"]::before {
    transform: rotate(90deg);
}

.mediapanel__toggle .subtabs__count,
.mediafold__toggle .subtabs__count {
    margin-left: auto;
}

/* An opened panel is as tall as what is in it. It used to be a 52dvh box with its own scrollbar,
   which meant a run with six QC reports was read through a slot: two scrollbars a few pixels
   apart, the one under the pointer never the one that needed moving, and a panel whose bottom was
   cut off even when there was room below it. The pane is the only thing that scrolls now, so
   everything expanded is reachable by scrolling the one column it is in. */
.mediapanel__body {
    padding: 10px;
    border-top: 1px solid var(--border);
}

.mediafold {
    margin-top: 8px;
    border: 1px solid var(--border);
    border-radius: 9px;
    overflow: hidden;
}

.mediafold__toggle {
    font-size: 12px;
    font-weight: 500;
}

.mediafold__body {
    padding: 8px 10px;
    border-top: 1px solid var(--border);
}

/* ------------------------------------------------------------------ step lists */

.mediasteps,
.mediaruns {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.mediasteps__item,
.mediaruns__row {
    display: grid;
    grid-template-columns: 10px minmax(0, 1fr) max-content;
    align-items: start;
    gap: 8px;
}

.mediaruns__row {
    grid-template-columns: 10px minmax(0, 1fr);
}

/* One lamp, four colours. The state is a colour wherever a column of them is scanned rather than
   read; the word for it is still on the pill beside it. */
.mediasteps__lamp {
    width: 8px;
    height: 8px;
    margin-top: 5px;
    border-radius: 50%;
    background: var(--border-strong);
}

.mediasteps__lamp[data-tone="good"] { background: var(--q-lime); }
.mediasteps__lamp[data-tone="warn"] { background: var(--q-amber); }
.mediasteps__lamp[data-tone="bad"] { background: var(--q-red); }

.mediasteps__lamp[data-tone="busy"] {
    background: var(--q-violet);
    animation: mediapulse 1.4s ease-in-out infinite;
}

@keyframes mediapulse {
    0%, 100% { opacity: 1; }
    50% { opacity: .35; }
}

.mediasteps__main,
.mediaruns__main {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.mediasteps__head {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}

.mediasteps__name {
    font-size: 12px;
    font-weight: 600;
    min-width: 0;
    overflow-wrap: anywhere;
}

.mediasteps__note {
    margin: 0;
    font-size: 11.5px;
    color: var(--text-muted);
    overflow-wrap: anywhere;
}

.mediasteps__note[data-tone="bad"] {
    color: color-mix(in srgb, var(--q-red) 76%, var(--text));
}

.mediasteps__when {
    font-family: var(--font-mono);
    font-size: 9.5px;
    color: var(--text-muted);
    white-space: nowrap;
    padding-top: 3px;
}

/* --------------------------------------------------------------- the timeline */

/* A rail down the left with the steps threaded on it: the six are a sequence, and a plain list
   does not say so. */
.mediatimeline {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin: 0;
    padding: 0 0 0 18px;
    list-style: none;
}

.mediatimeline::before {
    content: "";
    position: absolute;
    left: 4px;
    top: 5px;
    bottom: 5px;
    width: 1px;
    background: var(--border);
}

.mediatimeline__step {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr) max-content;
    gap: 2px 8px;
}

.mediatimeline__lamp {
    position: absolute;
    left: -18px;
    top: 4px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--surface);
    border: 2px solid var(--border-strong);
}

.mediatimeline__step[data-tone="good"] .mediatimeline__lamp {
    border-color: var(--q-lime);
    background: var(--q-lime);
}

.mediatimeline__step[data-tone="warn"] .mediatimeline__lamp {
    border-color: var(--q-amber);
    background: var(--q-amber);
}

.mediatimeline__step[data-tone="bad"] .mediatimeline__lamp {
    border-color: var(--q-red);
    background: var(--q-red);
}

.mediatimeline__step[data-tone="busy"] .mediatimeline__lamp {
    border-color: var(--q-violet);
    background: var(--q-violet);
    animation: mediapulse 1.4s ease-in-out infinite;
}

.mediatimeline__legend {
    font-size: 12.5px;
    font-weight: 700;
    font-family: var(--font-display);
}

.mediatimeline__state,
.mediatimeline__percent {
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--text-muted);
    text-align: right;
    white-space: nowrap;
}

.mediatimeline__summary {
    grid-column: 1 / -1;
    margin: 0;
    font-size: 11.5px;
    color: var(--text-muted);
}

.mediatimeline .mediabar {
    grid-column: 1 / -1;
    margin-top: 3px;
}

/* Neither lamp is the only carrier of its state, so stilling them costs nothing. */
@media (prefers-reduced-motion: reduce) {
    .mediasteps__lamp[data-tone="busy"],
    .mediatimeline__step[data-tone="busy"] .mediatimeline__lamp {
        animation: none;
    }

    .mediatree__spinner {
        animation-duration: 2.4s;
    }
}

/* --------------------------------------------------------------------- the QC */

.mediaqc,
.mediatranscode {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.mediaqc__verdict {
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex-wrap: wrap;
    padding: 8px 10px;
    border-radius: 9px;
    border: 1px solid var(--border);
    background: var(--surface-sunken);
}

.mediaqc__verdict[data-tone="good"] {
    border-color: color-mix(in srgb, var(--q-lime) 42%, transparent);
    background: color-mix(in srgb, var(--q-lime) 11%, var(--surface));
}

.mediaqc__verdict[data-tone="warn"] {
    border-color: color-mix(in srgb, var(--q-amber) 42%, transparent);
    background: color-mix(in srgb, var(--q-amber) 11%, var(--surface));
}

.mediaqc__verdict[data-tone="bad"] {
    border-color: color-mix(in srgb, var(--q-red) 40%, transparent);
    background: color-mix(in srgb, var(--q-red) 9%, var(--surface));
}

.mediaqc__result {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 15px;
    letter-spacing: -.02em;
}

.mediaqc__was {
    font-size: 11px;
    color: var(--text-muted);
}

.mediaqc__note {
    flex-basis: 100%;
    font-size: 11.5px;
    color: var(--text-muted);
}

/* At the size the QC server made it, not stretched to the pane. These are thumbnails — a few
   hundred pixels across — and blowing one up to full width costs a screenful of soft pixels that
   say no more than the original did. Capped so an unexpectedly large one still cannot overflow. */
.mediaqc__frame {
    max-width: 100%;
    height: auto;
    align-self: start;
    border-radius: 9px;
    background: #000;
}

.mediaqc__pending {
    margin: 0;
    font-size: 11px;
    color: var(--text-muted);
}

.mediaalerts {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* The level is carried on the left edge as well as on the pill: a column of alerts is scanned for
   the bad ones, and an edge is what a scan actually lands on. */
.mediaalerts__item {
    padding: 7px 9px;
    border-radius: 8px;
    border: 1px solid var(--border);
    border-left-width: 3px;
    border-left-color: var(--border-strong);
    background: var(--surface-sunken);
}

.mediaalerts__item[data-tone="warn"] { border-left-color: var(--q-amber); }
.mediaalerts__item[data-tone="bad"] { border-left-color: var(--q-red); }

.mediaalerts__head {
    display: flex;
    align-items: center;
    gap: 7px;
    flex-wrap: wrap;
}

.mediaalerts__type {
    font-size: 12px;
    font-weight: 600;
}

.mediaalerts__tc {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: 10px;
    color: var(--text-muted);
    white-space: nowrap;
}

.mediaalerts__detail,
.mediaalerts__where {
    margin: 4px 0 0;
    font-size: 11.5px;
    color: var(--text-soft);
    overflow-wrap: anywhere;
}

.mediaalerts__where {
    color: var(--text-muted);
    font-size: 11px;
}

.mediaalerts__frames {
    display: flex;
    gap: 5px;
    margin-top: 6px;
    flex-wrap: wrap;
}

/* Likewise: the frames an alert captured, as captured. Three of them side by side at their own
   size is a row; three scaled to a fixed width is three rows of guesswork. */
.mediaalerts__frames img {
    max-width: 100%;
    height: auto;
    border-radius: 6px;
    background: #000;
}

/* ------------------------------------------------------------------- dialogues */

.modal__panel--narrow {
    width: min(520px, 100%);
}

.modal__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid var(--border);
}

.mediapicker__chosen {
    font-size: 12px;
    color: var(--text-muted);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The two ways of removing an asset, one above the other and never one control with a switch. */
.mediadelete {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.mediadelete__option {
    padding: 11px 12px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface-sunken);
}

.mediadelete__option[data-severe="true"] {
    border-color: color-mix(in srgb, var(--q-red) 40%, transparent);
    background: color-mix(in srgb, var(--q-red) 7%, var(--surface));
}

.mediadelete__option h4 {
    margin: 0 0 4px;
    font-family: var(--font-display);
    font-size: 13px;
}

.mediadelete__option p {
    margin: 0 0 9px;
    font-size: 12px;
    color: var(--text-soft);
}

.mediadelete__aside {
    font-size: 11px;
    color: var(--text-muted);
}

/* ------------------------------------------------------- tones this page adds */

.notice[data-tone="warn"] {
    background: color-mix(in srgb, var(--q-amber) 13%, var(--surface));
    border-color: color-mix(in srgb, var(--q-amber) 40%, transparent);
    color: color-mix(in srgb, var(--q-amber) 46%, var(--text));
}

.notice__close {
    margin-left: auto;
    border: 0;
    background: none;
    font-size: 17px;
    line-height: 1;
    color: inherit;
    opacity: .6;
    cursor: pointer;
}

.notice__close:hover {
    opacity: 1;
}

.pill[data-tone="warn"] {
    background: color-mix(in srgb, var(--q-amber) 18%, transparent);
    color: color-mix(in srgb, var(--q-amber) 48%, var(--text));
}

.pill[data-tone="bad"] {
    background: color-mix(in srgb, var(--q-red) 14%, transparent);
    color: color-mix(in srgb, var(--q-red) 72%, var(--text));
}

.pill[data-tone="busy"],
.pill[data-tone="accent"] {
    background: color-mix(in srgb, var(--q-violet) 16%, transparent);
    color: color-mix(in srgb, var(--q-violet) 62%, var(--text));
}

/* ------------------------------------------------------------------ media explorer, part two

   Resizable seams, grouped facts, and the full QC report. */

/* Each width is its own custom property, so a drag is three inline writes from media.js and the
   grid keeps owning the layout — the handle only moves numbers.

   The seams are real tracks rather than absolutely-positioned decoration, and they occupy what
   would otherwise be the column gap. So the column gap is zero and the seam is the gap; the row
   gap stays, for the widths where the detail pane wraps to a second row. */
.mediapanes {
    --pane-a: minmax(210px, 1fr);
    --pane-b: minmax(0, 3.1fr);
    --pane-c: minmax(300px, 1.5fr);
    grid-template-columns: var(--pane-a) var(--media-gap) var(--pane-b) var(--media-gap) var(--pane-c);
    column-gap: 0;
    row-gap: var(--media-gap);
}

/* A seam. It fills the track it sits in, which is the gap between two panes, so the whole gap is
   grabbable — a 1px target is one nobody hits. The visible line is drawn by ::before. */
.mediapanes__grip {
    position: relative;
    cursor: col-resize;
    align-self: stretch;
    touch-action: none;
    z-index: 2;
}

.mediapanes__grip::before {
    content: "";
    position: absolute;
    inset-block: 10px;
    left: 50%;
    width: 2px;
    transform: translateX(-50%);
    border-radius: var(--radius-pill);
    background: transparent;
    transition: background 120ms var(--step-cubic);
}

.mediapanes__grip:hover::before,
.mediapanes__grip:focus-visible::before,
.mediapanes[data-resizing="true"] .mediapanes__grip::before {
    background: color-mix(in srgb, var(--q-violet) 55%, transparent);
}

.mediapanes__grip:focus-visible {
    outline: none;
}

/* While a seam is being dragged nothing else should react to the pointer — a text selection
   dragged across the listing, or a tile lighting up under the cursor, both read as the drag
   having gone wrong. */
.mediapanes[data-resizing="true"] {
    user-select: none;
    cursor: col-resize;
}

.mediapanes[data-resizing="true"] * {
    pointer-events: none;
}

/* Two panes and one seam below the three-column breakpoint. The second seam goes away entirely
   rather than being disabled: the detail pane is on a row of its own down here, so there is
   nothing beside it to trade width with. */
@media (max-width: 1500px) {
    .mediapanes {
        grid-template-columns: var(--pane-a) var(--media-gap) var(--pane-b);
    }

    .mediapanes__grip[data-seam="1"] {
        display: none;
    }
}

@media (max-width: 900px) {
    .mediapanes {
        grid-template-columns: minmax(0, 1fr);
        row-gap: var(--media-gap);
    }

    .mediapanes__grip {
        display: none;
    }
}

/* --------------------------------------------------------------- grouped facts */

/* A run of facts under a heading. The detail pane outgrew one flat list: a checksum, a codec
   and a storage account answer different questions, and a reader after one should not have to
   scan past the other two. */
.mediafactgroup {
    margin-top: 10px;
}

.mediafactgroup__title {
    margin: 0 0 4px;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* A value read character by character rather than as a sentence — a checksum, an id, a path.
   Allowed to break anywhere, because none of them have spaces to break at and a 64-character
   hash would otherwise push the pane sideways. */
.mediafacts__token {
    font-family: var(--font-mono);
    font-size: 10.5px;
    line-height: 1.45;
    overflow-wrap: anywhere;
    color: var(--text-soft);
}

/* -------------------------------------------------------------- the full report */

/* A height rather than a ceiling. With only a max-height the panel was as tall as whatever had
 * loaded so far, so it opened as a strip saying "Reading this folder…" and then jumped to full size
 * — and everything in it moved under the cursor while somebody was aiming at it. */
.modal__panel--wide {
    width: min(1080px, 100%);
    height: min(88dvh, 760px);
    max-height: 100%;
}

/* --------------------------------------------------------------------- an attached asset */

/* A stage at the size a preview is worth having, and the facts underneath. Sized in viewport units
   because what is being judged is the picture: a 200px-tall player answers nothing about a master. */
.assetpreview {
    display: grid;
    place-items: center;
    min-height: 220px;
    max-height: 52dvh;
    padding: 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius-field);
    background: var(--surface-sunken);
    overflow: hidden;
}

.assetpreview__image,
.assetpreview__video {
    max-width: 100%;
    max-height: 50dvh;
    border-radius: 8px;
}

.assetpreview__video {
    width: 100%;
    background: #000;
}

.assetpreview__audio {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 0 8px;
    color: var(--text-muted);
}

.assetpreview__audio audio {
    flex: 1 1 auto;
    min-width: 0;
}

/* No picture and nothing to play. Says which of the two it is rather than drawing a broken image. */
.assetpreview__none {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    color: var(--text-muted);
    font-size: 12px;
}

.assetpreview__facts {
    margin-top: 12px;
}

/* The tally and the filter on one line: how bad it is, and the way to see only that. */
.mediaqc__filter {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 12px;
    padding-top: 10px;
    border-top: 1px solid var(--border);
}

.mediaqc__filter .segmented__option {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

.mediaqc__open,
.mediaruns__open {
    margin-left: auto;
}

/* In the run's asset list the button sits at the end of a wrapping row of pills, so it needs to
   stop being pushed onto a line of its own the moment the name is long. */
.mediaruns__open {
    flex: none;
}

.mediasteps__pct {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .06em;
    color: var(--text-muted);
}

/* The QC verdict at the top of the detail pane. A strip rather than another folding panel: it is
   one line and it is the line people came for, so it is never something to open. */
.mediaqcchip {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    padding: 7px 9px;
    border: 1px solid var(--border);
    border-left-width: 3px;
    border-left-color: var(--border-strong);
    border-radius: 9px;
    background: var(--surface-sunken);
}

.mediaqcchip[data-tone="good"] { border-left-color: var(--q-lime); }
.mediaqcchip[data-tone="warn"] { border-left-color: var(--q-amber); }
.mediaqcchip[data-tone="bad"] { border-left-color: var(--q-red); }

.mediaqcchip__count {
    font-size: 11.5px;
    color: var(--text-muted);
}

.mediaqcchip__open {
    margin-left: auto;
}

/* ==========================================================================
   The processing board — /processing
   ========================================================================== */

/* Its own stack rather than the arrangeable card canvas the settings pages use. There is nothing
   here to arrange: the three sections are in the order the questions get asked, and a board whose
   Stuck section could be dragged below Done would be a board that hides the thing it exists for. */
.boardpage {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

/* ------------------------------------------------------------------------- the bar */

.boardbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
}

.boardbar__live {
    display: flex;
    align-items: center;
    gap: 8px;
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* The one thing on the page that says the numbers beside it are still being asked for. A page that
   silently stopped refreshing looks exactly like a pipeline that went quiet. */
.boardbar__pulse {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--border-strong);
}

.boardbar__live[data-live="true"] .boardbar__pulse {
    background: var(--q-lime);
    animation: mediapulse 2s ease-in-out infinite;
}

.boardbar__tools {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

/* --------------------------------------------------------------------- the tally */

/* Four numbers, each answering a different question about the same pipeline. Big enough to read
   across a desk, because that is where a board like this is usually being watched from.

   NOT .tally, which this stylesheet already gave to the monitor rail's auth strip near the top —
   a flex row with align-items:center, a dark band and padding of its own. Re-using the name here
   inherited every property this block does not restate: the row grew a black band, and centred
   items instead of stretching them, so the one tile with a third line stood taller than the other
   three. */
.boardstat {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
    gap: 10px;
    /* The grid default, said out loud: these four are read as a row and must agree on a baseline
       whether or not they all carry the same number of lines. */
    align-items: stretch;
}

.boardstat__tile {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-left-width: 3px;
    border-left-color: var(--border-strong);
    border-radius: 12px;
    background: var(--surface);
}

.boardstat__tile[data-tone="busy"] { border-left-color: var(--q-violet); }
.boardstat__tile[data-tone="warn"] { border-left-color: var(--q-amber); }
.boardstat__tile[data-tone="bad"] { border-left-color: var(--q-red); }

.boardstat__count {
    font-family: var(--font-display);
    font-size: 30px;
    font-weight: 800;
    line-height: 1.05;
    font-variant-numeric: tabular-nums;
}

.boardstat__label {
    font-size: 12.5px;
    color: var(--text-soft);
}

/* Pushed to the foot of its tile, so the qualifier under one number does not shove that tile's
   label out of line with the other three. */
.boardstat__note {
    margin-top: auto;
    padding-top: 2px;
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* ------------------------------------------------------------------ the sections */

.board {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.board__head {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.board__title {
    margin: 0;
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: 800;
}

/* What the cap left out, said where the count is. */
.board__capped {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Wide enough that six tiles across it are readable rather than merely present. Every narrowing of
   this truncated the summaries the projection publishes, and a board of ellipses answers nothing —
   so one wide card per row on an ordinary window, two only on a very wide one. A run is a pipeline
   read left to right; it is the shape that wants the width, not the number of cards. */
.board__cards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 1200px), 1fr));
    gap: 10px;
}

.board__table {
    overflow-x: auto;
}

/* ---------------------------------------------------------------------- one card */

.runcard {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 12px 14px 14px;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: var(--surface);
    box-shadow: 0 1px 2px rgb(20 19 43 / 4%);
    /* The flow inside asks how wide its card is, not how wide the window is — a card can be narrow
       on a wide screen and the tiles have to answer to the space they actually get. */
    container-type: inline-size;
}

/* An idle card is edged rather than filled: the six tiles inside it are already carrying colour,
   and a tinted card behind them makes both harder to read. */
.runcard[data-idle="true"] {
    border-color: color-mix(in srgb, var(--q-amber) 55%, var(--border));
    box-shadow: inset 3px 0 0 var(--q-amber), 0 1px 2px rgb(20 19 43 / 4%);
}

/* The whole header line opens the run, so the target is the width of the card rather than a button
   somebody has to find. */
.runcard__open {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    text-align: left;
    color: inherit;
    cursor: pointer;
}

.runcard__open:hover .runcard__name {
    color: var(--q-violet);
}

.runcard__name {
    flex: 1 1 auto;
    min-width: 0;
    font-family: var(--font-display);
    font-size: 13.5px;
    font-weight: 700;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.runcard__when {
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-muted);
    white-space: nowrap;
}

.runcard__waiting {
    margin: 0;
    font-size: 12px;
    color: color-mix(in srgb, var(--q-amber) 70%, var(--text));
}

.runcard__waiting[data-tone="bad"] {
    color: color-mix(in srgb, var(--q-red) 76%, var(--text));
}

.runcard__nosteps {
    margin: 0;
    font-size: 12px;
    color: var(--text-muted);
}

/* The stop row, on idle cards only. It wraps rather than squeezing the reason field to nothing,
   because the field is the only part of it somebody has to read what they typed in. */
.runcard__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}

/* Enough to read a short phrase back, and no wider. Left to grow it would push Confirm to the far
   edge of the card, away from the row it belongs to and a long mouse travel from the Stop that armed
   it. */
.runcard__reason {
    flex: 0 1 260px;
    min-width: 0;
    height: 28px;
    padding: 0 8px;
    font-size: 12px;
}

/* -------------------------------------------------------------- the board's notice */

/* What the last stop or resume answered, above the sections rather than on the card: the card it was
   about has usually moved to another section by the time the answer lands. */
.boardnotice {
    margin: 0;
    padding: 8px 12px;
    border: 1px solid var(--border);
    border-radius: var(--radius-field);
    font-size: 12.5px;
    color: var(--text);
    background: var(--surface-sunken);
}

.boardnotice[data-tone="good"] {
    border-color: color-mix(in srgb, var(--q-lime) 40%, var(--border));
    background: color-mix(in srgb, var(--q-lime) 12%, var(--surface-sunken));
}

.boardnotice[data-tone="bad"] {
    border-color: color-mix(in srgb, var(--q-red) 40%, var(--border));
    background: color-mix(in srgb, var(--q-red) 10%, var(--surface-sunken));
}

/* --------------------------------------------------------------------- the flow */

/* Six tiles in a row, one grid track each, so the QC tile is in the same place on every card and a
   column of twenty can be swept without reading a word. They stay a single row at every card width
   the board draws — a flow that wrapped would stop being a sequence. */
.flow {
    display: grid;
    grid-template-columns: repeat(var(--flow-steps, 6), minmax(0, 1fr));
    gap: 4px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.flow__step {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
    padding: 6px 7px 7px;
    border-radius: 8px;
    border-top: 2px solid var(--border-strong);
    background: var(--surface-sunken);
}

.flow__step[data-tone="good"] { border-top-color: var(--q-lime); }
.flow__step[data-tone="warn"] { border-top-color: var(--q-amber); }
.flow__step[data-tone="bad"] { border-top-color: var(--q-red); }

.flow__step[data-tone="busy"] {
    border-top-color: var(--q-violet);
    background: color-mix(in srgb, var(--q-violet) 7%, var(--surface-sunken));
}

/* Below about a hundred and thirty pixels a tile cannot hold "passed · 3 alerts" on two lines, and
   the card starts hiding what it exists to report. Rather than let it truncate, the flow folds to
   three across and two down — still read left to right and then on, still one tile per step, and
   nothing lost. Keyed to the card's own width, so it fires on a narrow window or a narrow column
   alike. */
@container (max-width: 780px) {
    .flow {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

@container (max-width: 380px) {
    .flow {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/* Only the running tile moves. Six pulsing tiles across twenty cards is a board nobody can look
   at, and the one that is working is the only one worth drawing an eye to. */
.flow__step[data-tone="busy"] .flow__state {
    color: var(--q-violet);
    animation: mediapulse 1.8s ease-in-out infinite;
}

/* The one thing in a tile allowed to wrap. It is two words at most, it names the step, and the
   grid stretches every tile to the tallest anyway — so wrapping costs one line across the card and
   an ellipsis in a heading costs the reader the word. */
.flow__legend {
    font-family: var(--font-display);
    font-size: 10.5px;
    font-weight: 700;
    line-height: 1.25;
    overflow-wrap: anywhere;
}

.flow__state {
    font-family: var(--font-mono);
    font-size: 8.5px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Two lines, then an ellipsis. What the projection publishes here is a sentence — "passed · 3
   alerts", "2 / 6 completed" — and one line cut it in half on every tile. Clamped rather than left
   to wrap freely because the six tiles are one grid row: unbounded wrapping lets a single long
   summary set the height of every card on the board. The whole of it is in the title either way. */
.flow__summary {
    margin: 0;
    font-size: 11px;
    line-height: 1.35;
    color: var(--text-soft);
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    overflow-wrap: anywhere;
}

/* The Received tile's facts stay on one line each. They are a tenant and a filename — identifiers
   read to tell two cards apart rather than sentences read for their sense — and there are two of
   them, so clamping each to two lines would let one tile run to four. */
.flow__summary--fact {
    -webkit-line-clamp: 1;
    line-clamp: 1;
    overflow-wrap: normal;
    word-break: break-all;
}

.flow .mediabar {
    margin-top: 4px;
    height: 3px;
    border-radius: 2px;
    background: var(--border);
    overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
    .boardbar__live[data-live="true"] .boardbar__pulse,
    .flow__step[data-tone="busy"] .flow__state {
        animation: none;
    }
}

/* A dialog head that carries a mark beside its title keeps the two together, so the close button
   is still the thing on the right rather than the mark being pushed into the middle. */
.modal__heading {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
}


/* ==========================================================================
   Asset management
   The metadata side of a tenant's content: a tree beside a stack of open
   documents. Two panes rather than the file explorer's three, and a task bar
   above the right-hand one — panes here are documents somebody opened, not
   sections of a fixed screen, so the strip is the navigation and the pane
   below it is whatever is in front.
   ========================================================================== */

.assetspage {
    --asset-gap: 12px;
    display: flex;
    flex-direction: column;
    gap: var(--asset-gap);
    /* The window, less the top bar, the status bar and the padding this page sits in. Same
       arithmetic as the file explorer, for the same reason: this page scrolls its own panes. */
    height: calc(100dvh - 64px - var(--impersonating-h) - var(--statusbar-h) - 32px);
    min-height: 420px;
}

.content:has(.assetspage) {
    padding: 16px 20px 8px;
    overflow: hidden;
}

/* ------------------------------------------------------------------------ the bar */

.assetbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--asset-gap);
    flex-wrap: wrap;
}

.assetbar__tools {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

/* A select or an input that sits in a toolbar rather than in a form: the height the buttons beside
   it are, so a row of them lines up. */
.input--slim {
    height: 32px;
    padding-block: 0;
    width: auto;
    min-width: 96px;
}

/* --------------------------------------------------------------------- the panes */

/* Each width is its own custom property, so a drag is two inline writes from panes.js and the grid
   keeps owning the layout — the handle only moves numbers.

   The seam is a real track rather than absolutely-positioned decoration, and it occupies what would
   otherwise be the column gap. So the column gap is zero and the seam is the gap. */
.assetspanes {
    --apane-a: minmax(240px, 1fr);
    --apane-b: minmax(0, 2.7fr);
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    grid-template-columns: var(--apane-a) var(--asset-gap) var(--apane-b);
    column-gap: 0;
    row-gap: var(--asset-gap);
}

.assetspanes__pane {
    display: flex;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
    padding: 12px 14px;
    overflow: hidden;
}

/* The work pane owns its own head — the task bar — so it gets no padding above it. */
.assetspanes__work {
    padding-top: 0;
}

.assetspanes__head {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--border);
}

.assetspanes__title {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13px;
    letter-spacing: -.01em;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.assetspanes__acts {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-left: auto;
    flex-wrap: wrap;
}

.assetspanes__scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    padding-top: 8px;
}

/* A seam. It fills the track it sits in, which is the gap between the two panes, so the whole gap
   is grabbable — a 1px target is one nobody hits. The visible line is drawn by ::before. */
.assetspanes__grip {
    position: relative;
    cursor: col-resize;
    align-self: stretch;
    touch-action: none;
    z-index: 2;
}

.assetspanes__grip::before {
    content: "";
    position: absolute;
    inset-block: 10px;
    left: 50%;
    width: 2px;
    transform: translateX(-50%);
    border-radius: var(--radius-pill);
    background: transparent;
    transition: background 120ms var(--step-cubic);
}

.assetspanes__grip:hover::before,
.assetspanes__grip:focus-visible::before,
.assetspanes[data-resizing="true"] .assetspanes__grip::before {
    background: color-mix(in srgb, var(--q-violet) 55%, transparent);
}

.assetspanes__grip:focus-visible {
    outline: none;
}

/* While a seam is being dragged nothing else should react to the pointer — a text selection dragged
   across a form, or a tree row lighting up under the cursor, both read as the drag having gone
   wrong. */
.assetspanes[data-resizing="true"] {
    user-select: none;
    cursor: col-resize;
}

.assetspanes[data-resizing="true"] * {
    pointer-events: none;
}

/* Below this there is no room for two side by side, and the tree is the one that can be short, so
   they stack with the tree on top and a share of the height rather than all of it. */
@media (max-width: 1100px) {
    .assetspanes {
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: minmax(0, 0.7fr) minmax(0, 1fr);
    }

    .assetspanes__grip {
        display: none;
    }
}

/* --------------------------------------------------------------------- the tree */

.assettree {
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.assettree__branch {
    display: contents;
}

/* The indent is a variable rather than nested padding, so a row keeps its whole width as a drop
   target however deep it sits: a target that narrows as you go down is one you keep missing. */
.assettree__row {
    display: flex;
    align-items: center;
    gap: 2px;
    padding-left: calc(var(--depth, 0) * 13px);
    border-radius: 8px;
    border: 1px solid transparent;
}

.assettree__row[data-drop="true"] {
    border-color: var(--q-violet);
    background: color-mix(in srgb, var(--q-violet) 14%, transparent);
}

/* A folder that cannot take what is being dragged says so under the cursor rather than doing
   nothing: a drop that silently fails is one nobody learns from. */
.assettree__row[data-refuse="true"] {
    border-color: var(--q-red);
    background: color-mix(in srgb, var(--q-red) 10%, transparent);
    cursor: not-allowed;
}

.assettree__row[data-deleted="true"] .assettree__name {
    text-decoration: line-through;
    color: var(--text-muted);
}

/* The line between two rows, and only while something is being dragged. Tall enough to hit and thin
   enough not to move the tree while it is there.

   Those two pull against each other, and the negative margins are how both are had: the band is 11px
   of hit area that costs 1px of layout, because it overlaps the rows on either side and wins the
   pointer across the whole of it. Seven was the band before, and seven is not enough — the pointer
   is moving when it crosses, every dragenter is a round trip, and a target that thin is one somebody
   skips over and lands on the row instead. It is now a little under half the row pitch, which leaves
   the middle of a row wide enough to stay the other gesture: dropping into the folder itself.

   The left margin lands the gap where a row's name starts rather than where its row starts: the
   chevron column is 18px and the row's own gap is 2px, so a gap indented by the depth plus 20 lines
   up with the label of the rows above and below it. The preview inside it inherits that, which is
   what makes the indent readable as "this is the folder it goes into". */
.assettree__gap {
    height: 11px;
    margin: -5px 0 -5px calc(var(--depth, 0) * 13px + 20px);
    border-radius: var(--radius-pill);
    background: transparent;
    transition: height 130ms var(--step-cubic), margin 130ms var(--step-cubic),
                background 100ms var(--step-cubic);
}

/* The gap under the pointer opens to a row's height to hold the preview.

   It grows downward from a top edge that does not move, which is what keeps the pointer inside the
   band that just grew. A gap that opened around its own midpoint would slide out from under the
   pointer, hand the next dragenter to its neighbour, and the two would take turns growing for as
   long as the mouse was held still. */
.assettree__gap[data-over="true"] {
    height: 30px;
    /* margin-top stays at -5px on purpose: it is what holds the top edge still. Zeroing it moves the
       edge down as the gap opens, which drops a pointer sitting in that sliver onto the row
       above — that row claims the drop, the gap shrinks, the pointer is back inside it, and the two
       trade the pointer back and forth for as long as the mouse is held still. */
    margin-bottom: 0;
    border-radius: 8px;
}

/* The dragged row, drawn where it would land. Dashed and see-through because it is a preview and not
   the thing itself — drawn solid it reads as a row that is already there, and the tree would appear
   to have gained an item that no drop had yet created. */
.assettree__ghost {
    display: flex;
    align-items: center;
    gap: 7px;
    height: 100%;
    padding: 0 7px;
    box-sizing: border-box;
    /* Dashed and stated clearly. A faint 1px dash does not resolve against this background, and a
       preview that reads as a solid row is worse than no preview: the tree looks like it has already
       gained an item nobody dropped. */
    border: 1px dashed color-mix(in srgb, var(--q-violet) 90%, transparent);
    border-radius: 8px;
    background: color-mix(in srgb, var(--q-violet) 10%, transparent);
    color: var(--text-muted);
    font-size: 12.5px;
    white-space: nowrap;
    overflow: hidden;
    /* The preview sits inside the gap that is listening for the drop. Allowed to take pointer events
       it would steal the dragenter from its own gap the moment it appeared, and the preview would
       flicker out from under the pointer that asked for it. */
    pointer-events: none;
}

/* Where the row came from, while it is in the air. Dimmed rather than pulled out of the tree: the
   only thing that should move during a drag is the gap that opens, and a tree that also closed up
   behind the pointer would be two reflows fighting over the same few pixels. */
.assettree__row[data-dragging="true"] {
    opacity: .4;
}

/* How many more are coming, when several were picked up. Sits inside the preview because it is part
   of the same statement: this row, and that many behind it. */
.assettree__ghostmore {
    flex: none;
    padding: 1px 6px;
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--q-violet) 30%, transparent);
    color: var(--text);
    font-size: .68rem;
    letter-spacing: .02em;
}

/* A marked row, when more than one is marked. Drawn only then: a single mark is what the current-row
   highlight already says, and painting both on one row would be two words for the same thing.

   Deliberately quieter than data-current. The anchor is still the row the details pane is showing,
   and losing track of which one that is, in a set of nine, is how the wrong record gets edited. */
.assettree__row[data-marked="true"] {
    background: color-mix(in srgb, var(--q-violet) 13%, transparent);
    border-color: color-mix(in srgb, var(--q-violet) 28%, transparent);
}

.assettree__twist {
    flex: none;
    width: 18px;
    height: 26px;
    border: 0;
    border-radius: 6px;
    background: none;
    font-size: 10px;
    color: var(--text-muted);
    cursor: pointer;
    transition: transform 120ms var(--step-cubic), color 120ms var(--step-cubic);
}

.assettree__twist:hover {
    color: var(--text);
}

.assettree__twist[aria-expanded="true"] {
    transform: rotate(90deg);
    color: var(--text);
}

/* A leaf keeps the space a chevron would have taken, so every name in a level starts at the same
   place. Names that shuffle left and right by eighteen pixels are much harder to scan. */
.assettree__twist--none {
    display: inline-block;
    cursor: default;
}

.assettree__spinner {
    display: inline-block;
    width: 9px;
    height: 9px;
    border: 1.5px solid color-mix(in srgb, var(--q-violet) 34%, transparent);
    border-top-color: var(--q-violet);
    border-radius: 50%;
    animation: mediaspin 640ms linear infinite;
}

.assettree__pick {
    display: flex;
    align-items: center;
    gap: 7px;
    flex: 1 1 auto;
    min-width: 0;
    padding: 4px 7px;
    border: 0;
    border-radius: 7px;
    background: none;
    font: inherit;
    font-size: 12.5px;
    text-align: left;
    color: var(--text);
    cursor: pointer;
}

.assettree__pick:hover:not(:disabled) {
    background: var(--bg-accent);
}

.assettree__pick[aria-current="true"] {
    background: color-mix(in srgb, var(--q-violet) 13%, transparent);
    color: var(--q-violet);
    font-weight: 600;
}

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

/* Where an item sits in a series, after its title. Mono, because it is a pair of numbers compared
   down the column rather than read as words. */
.assettree__number {
    flex: none;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .02em;
    padding: 1px 4px;
    border-radius: 4px;
    background: var(--bg-accent);
    color: var(--text-muted);
}

.assettree__pick[aria-current="true"] .assettree__number {
    color: inherit;
}

/* Has media. A mark rather than a word: it is a fact about the row worth noticing in passing and
   not worth reading. */
.assettree__mark {
    flex: none;
    font-size: 9px;
    color: var(--q-lime);
}

/* Anything but Active. Named rather than coloured, because "archived" and "inactive" are not
   something a hue can tell apart and both matter. */
.assettree__status {
    flex: none;
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .06em;
    text-transform: uppercase;
    padding: 1px 5px;
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    color: var(--text-muted);
}

/* Opening a form is the second thing anybody does to a row, so it has a button of its own rather
   than only a double-click nobody discovers. Hidden until the row is under the pointer, because
   forty of them down the tree would be forty things competing with the names. */
.assettree__edit {
    flex: none;
    display: grid;
    place-items: center;
    width: 22px;
    height: 22px;
    margin-right: 2px;
    border: 0;
    border-radius: 6px;
    background: none;
    color: var(--text-muted);
    cursor: pointer;
    opacity: 0;
    transition: opacity 120ms var(--step-cubic), background 120ms var(--step-cubic);
}

.assettree__row:hover .assettree__edit,
.assettree__edit:focus-visible {
    opacity: 1;
}

.assettree__edit:hover {
    background: var(--bg-accent);
    color: var(--text);
}

.assettree__empty,
.assettree__none {
    margin: 4px 0;
    padding-left: calc(var(--depth, 0) * 13px + 22px);
    font-size: 11.5px;
    color: var(--text-muted);
}

/* --------------------------------------------------------------------- search results */

/* A flat list, because a search crosses the tree and the answer is "here are the records", not
   "here is where they sit". The content type rides along for the same reason: without the tree
   around it, that is what tells two records with the same title apart. */
.assetfound {
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.assetfound__row {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 5px 7px;
    border: 0;
    border-radius: 7px;
    background: none;
    font: inherit;
    font-size: 12.5px;
    text-align: left;
    color: var(--text);
    cursor: pointer;
}

.assetfound__row:hover {
    background: var(--bg-accent);
}

.assetfound__row[aria-selected="true"] {
    background: color-mix(in srgb, var(--q-violet) 13%, transparent);
    color: var(--q-violet);
    font-weight: 600;
}

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

.assetfound__where {
    flex: none;
    font-size: 11px;
    color: var(--text-muted);
}

/* --------------------------------------------------------------------- the task bar */

/* Tabs that scroll sideways rather than wrapping. A strip that grew to three rows would push the
   document under it off the screen, and the tabs at the end are the ones just opened — which is
   where somebody is looking. */
.taskbar {
    display: flex;
    align-items: stretch;
    gap: 2px;
    flex: none;
    padding: 8px 0 0;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
    border-bottom: 1px solid var(--border);
}

.taskbar__slot {
    display: flex;
    align-items: stretch;
    flex: none;
    border-radius: 8px 8px 0 0;
}

.taskbar__tab {
    display: flex;
    align-items: center;
    gap: 6px;
    flex: none;
    max-width: 220px;
    padding: 7px 10px;
    border: 1px solid transparent;
    border-bottom: 0;
    border-radius: 8px 8px 0 0;
    background: none;
    font: inherit;
    font-size: 12.5px;
    color: var(--text-muted);
    cursor: pointer;
    transition: background 120ms var(--step-cubic), color 120ms var(--step-cubic);
}

.taskbar__tab:hover {
    color: var(--text);
    background: var(--bg-accent);
}

/* The open one is the surface the pane below it is: the two are one shape, which is what makes a
   tab read as the front of something rather than as a button that happens to be highlighted. */
.taskbar__tab[aria-selected="true"] {
    border-color: var(--border);
    background: var(--surface);
    color: var(--text);
    font-weight: 600;
    margin-bottom: -1px;
    padding-bottom: 8px;
}

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

.taskbar__lang {
    flex: none;
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .06em;
    text-transform: uppercase;
    padding: 1px 4px;
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    color: var(--text-muted);
}

/* Unsaved work, as a dot. A tab that spelled it out would be wider than the title it is meant to
   be identifying. */
.taskbar__dot {
    flex: none;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--q-amber);
}

.taskbar__close {
    flex: none;
    width: 20px;
    margin-left: -6px;
    border: 0;
    background: none;
    font-size: 15px;
    line-height: 1;
    color: var(--text-muted);
    cursor: pointer;
    border-radius: 6px;
}

.taskbar__close:hover {
    background: var(--q-red);
    color: #fff;
}

/* --------------------------------------------------------------------- a document */

.assetdoc {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.assetdoc__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
}

.assetdoc__title {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    min-width: 0;
    flex: 1 1 260px;
}

.assetdoc__titlebox {
    min-width: 0;
    flex: 1 1 auto;
}

.assetdoc__name {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 18px;
    letter-spacing: -.02em;
    overflow-wrap: anywhere;
}

/* The title is a field, so it is drawn as one — but larger than the rest of the form, because it is
   the name of the thing every other field is about. */
.input--title {
    width: 100%;
    height: 34px;
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 17px;
    letter-spacing: -.02em;
}

.assetdoc__sub {
    margin: 3px 0 0;
    font-size: 11.5px;
    color: var(--text-muted);
    display: flex;
    align-items: center;
    gap: 5px;
    flex-wrap: wrap;
}

.assetdoc__acts {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    margin-left: auto;
}

/* --------------------------------------------------------------------- where it sits */

.assetcrumbs {
    display: flex;
    align-items: center;
    gap: 2px;
    flex-wrap: wrap;
    min-width: 0;
    font-size: 11.5px;
}

.assetcrumbs__crumb {
    padding: 2px 6px;
    border: 0;
    border-radius: 6px;
    background: none;
    font: inherit;
    color: var(--text-muted);
    cursor: pointer;
}

button.assetcrumbs__crumb:hover {
    background: var(--bg-accent);
    color: var(--text);
}

.assetcrumbs__crumb[aria-current="true"] {
    color: var(--text);
    font-weight: 600;
    cursor: default;
}

.assetcrumbs__sep {
    color: var(--text-muted);
    opacity: .6;
}

/* --------------------------------------------------------------------- the languages */

/* Which languages this record has, and which one is on screen. The empty mark is load-bearing: a
   read in a language with no version quietly answers in another one, so a code with nothing behind
   it has to look different from one that has something or the pane would show the wrong text and
   say nothing about it. */
.assetlangs {
    display: flex;
    align-items: center;
    gap: 4px;
    flex-wrap: wrap;
}

.assetlangs__pick {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    padding: 3px 9px;
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-muted);
    cursor: pointer;
}

.assetlangs__pick:hover:not(:disabled) {
    color: var(--text);
    border-color: var(--border-strong);
}

.assetlangs__pick[aria-pressed="true"] {
    background: color-mix(in srgb, var(--q-violet) 16%, transparent);
    border-color: var(--q-violet);
    color: var(--q-violet);
}

.assetlangs__pick[data-empty="true"] {
    border-style: dashed;
}

.assetlangs__none {
    opacity: .7;
}

/* --------------------------------------------------------------------- a panel */

/* The details pane is a stack of four answers — which record this is, who last saved it, what it
   says, what is attached — and a stack of hairline-separated runs made them read as one long list.
   A sunken panel each gives the eye the boundary the content already has. */
.assetpanel {
    padding: 11px 13px;
    border: 1px solid var(--border);
    border-radius: var(--radius-field);
    background: var(--surface-sunken);
    min-width: 0;
}

/* A folding panel owns its own padding through the button, so the section gives it none: the whole
   header strip has to be clickable, and a padded section would leave a dead margin around it. */
.assetpanel--fold {
    padding: 0;
    overflow: hidden;
}

.assetpanel--fold .assetgroup__toggle {
    border-top: 0;
    padding: 9px 13px;
}

/* Actions on the thing a modal is about, rather than on the modal. Pushed to the left of the footer
   so they do not read as alternatives to Close, which is what every other button in a modal foot is. */
.modal__acts {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-right: auto;
}

/* The rows and the type headings share the panel's inset. The heading is a sibling of the list
   rather than a child of it — one heading, one list, repeated per type — so it has to be told the
   same padding, or it sits flush against the box while every row beneath it is indented. */
.assetpanel--fold .fieldassets,
.assetpanel--fold .fieldassets__group {
    padding-left: 13px;
    padding-right: 13px;
}

/* The panel's own bottom edge, after the last list rather than after every one. */
.assetpanel--fold .fieldassets:last-child {
    padding-bottom: 11px;
}

.assetpanel__title,
.assetfacts__title,
.assetgroup__title {
    margin: 0 0 5px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.assetfacts__list,
.assetvalues {
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.assetfacts__row,
.assetvalues__row {
    display: grid;
    grid-template-columns: minmax(90px, 38%) 1fr;
    gap: 8px;
    font-size: 12px;
}

.assetfacts__row dt,
.assetvalues__row dt {
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.assetfacts__row dd,
.assetvalues__row dd {
    margin: 0;
    min-width: 0;
    color: var(--text);
    overflow-wrap: anywhere;
}

.assetfacts__row dd[data-mono="true"] {
    font-family: var(--font-mono);
    font-size: 11px;
}

/* A field somebody put on this page and then left empty. Said with a dash rather than dropped —
   the emptiness is part of what a curated page reports — but said quietly. */
.assetvalues__row dd[data-empty="true"] {
    color: var(--text-muted);
}

/* --------------------------------------------------------------------- who last saved it */

.assetsaved__row {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}

.assetsaved__who {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.35;
}

.assetsaved__name {
    font-weight: 600;
    font-size: 12.5px;
    overflow-wrap: anywhere;
}

.assetsaved__when {
    font-size: 11.5px;
    color: var(--text-muted);
}

/* The version number and the way into its history, on one line. */
.assetsaved__head {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}

/* Sitting inside a heading now, which means opting out of two things the heading imposes: its casing,
   which would shout the label, and its letter-spacing. Smaller than an ordinary inline button too —
   at full height it set the height of the version line by itself and stopped reading as part of it. */
.assetsaved__more {
    height: 26px;
    padding: 0 10px;
    gap: 6px;
    font-size: 11.5px;
    text-transform: none;
    letter-spacing: 0;
}

/* --------------------------------------------------------------------- a group of fields */

.assetgroup {
    padding-top: 10px;
    border-top: 1px solid var(--border);
}

.assetgroup--form {
    padding-top: 0;
    border-top: 0;

    /*  So the columns below can ask how wide this group actually is. The pane is a fraction of the
        window — a tree beside it, a nav beside that — so a window query answers the wrong question:
        on a wide screen with a narrow pane it would keep two columns in a space with room for one. */
    container-type: inline-size;
}

/* Said quietly, because it is a caveat rather than a heading: these fields are together because this
   application put them together, and the content type said nothing about it. */
.assetgroup__note {
    margin-left: 6px;
    font-family: var(--font-body);
    font-weight: 400;
    font-size: 10px;
    letter-spacing: 0;
    text-transform: none;
    color: var(--text-muted);
    opacity: .8;
}

.assetgroup__none,
.assetgroup__rest {
    margin: 2px 0 0;
    font-size: 11.5px;
    color: var(--text-muted);
}

/* A whole section folds shut, and the count on the button is what makes that safe: a heading that
   said only its name would hide the fact that eight of its twelve fields are filled in. */
.assetgroup__toggle {
    display: flex;
    align-items: center;
    gap: 7px;
    width: 100%;
    padding: 8px 6px;
    border: 0;
    border-top: 1px solid var(--border);
    background: none;
    font: inherit;
    text-align: left;
    color: var(--text-muted);
    cursor: pointer;
}

.assetgroup__toggle:hover {
    color: var(--text);
    background: var(--bg-accent);
}

.assetgroup__toggle .assetgroup__title {
    margin: 0;
}

.assetgroup__count {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: 10.5px;
    color: var(--text-muted);
}

/* --------------------------------------------------------------------- one field */

/* Two columns while there is room for them, because a metadata form is read down its labels: a label
   above its box doubles the height of a sixty-field record for nothing. */
/* One column unless a field in the group says otherwise, which is every group that predates
   Control.Column (56) — and a one-column grid lays out exactly as the flex column this replaced.
   --field-cols is written by AssetsEditPane from the highest column the group's own fields ask for. */
.fieldform {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 9px 28px;
    align-items: start;
    padding: 8px 6px 12px;

    /*  Dense, so a column starts at the top of the group rather than level with wherever its first
        field happens to fall in display order. Ordinary placement never moves the cursor backwards:
        with three fields in column 1 followed by two in column 2, column 2 would begin on row four
        and the group would read as a staircase. Dense backfills, and the two columns start together.

        The cost is that the drawn order is not the DOM order, so tab order follows DisplayOrder while
        the eye follows the columns. That is the bargain a content type strikes by asking for columns
        at all, and DisplayOrder is still the order somebody typing through the form gets. */
    grid-auto-flow: row dense;
}

/*  More than one column, so the tracks stop being a share of the pane and become a width.
    1fr each would put two columns at opposite ends of a two-thousand-pixel pane -- which is the
    same waste this whole change is about, only now with a canyon down the middle. 840px is a
    200px label, the gap, and the widest control tier, so the widest field still fits in a column
    and the group stays a group. minmax's zero floor is what lets them shrink on a smaller pane. */
.fieldform--columns {
    grid-template-columns: repeat(var(--field-cols, 1), minmax(0, 840px));
}

/* Placed by the field, and as many columns across as it asked for.

   The fallback is 1 rather than auto, and that is the whole of what makes the property predictable.
   Left to flow, a field that says nothing lands in whatever cell is free — so giving two fields
   column 2 would pull a third, which had said nothing at all, over to the right to fill the hole
   they left. Setting a property on one field would silently move another. Column 1 is what "I did
   not ask" means, and only a field that asked ends up anywhere else.

   In a group nobody has given columns to this is a single track, where `1 / span 1` and `auto` are
   the same cell — so those groups lay out exactly as they always have. */
.fieldrow {
    grid-column: var(--field-col, 1) / span var(--field-span, 1);
}

/* A heading, a divider or a note spans the group rather than picking a side of it. It gave up its
   label cell to span the form; spanning half of one would be a strange thing to have asked for. */
.fieldrow[data-full="true"] {
    grid-column: 1 / -1;
}

/* Below this there is not room for two of anything: the value column is already at its 120px floor
   by the time the pane is this narrow. Columns collapse and the group is one stack again — the
   property is advisory, and this is the client declining it. */
@container (max-width: 1100px) {
    .fieldform,
    .fieldform--columns {
        grid-template-columns: minmax(0, 1fr);
    }

    /*  grid-column: 1 is start 1, end auto — one track. So this drops the span as well as the
        column, which it has to: a field spanning 2 of 1 column would stretch the group back out. */
    .fieldrow,
    .fieldrow[data-full="true"] {
        grid-column: 1;
    }
}

.fieldrow {
    display: grid;
    /* A cap, not a share. At 26% of the pane the label column grew with the window — so on a wide
       screen a six-letter caption was followed by five hundred pixels of nothing before its box.
       200px is about twenty-eight characters at this size, which takes the long captions without
       wrapping and leaves the control where the eye already is. Longer ones wrap, which is the
       right way round: a rare second line beats a permanent gap on every other row. */
    grid-template-columns: minmax(120px, 200px) minmax(0, 1fr);
    gap: 10px;
    align-items: start;
}

@media (max-width: 760px) {
    .fieldrow {
        grid-template-columns: minmax(0, 1fr);
        gap: 4px;
    }
}

.fieldrow__label {
    padding-top: 7px;
    font-size: 12px;
    color: var(--text-soft);
    overflow-wrap: anywhere;
}

.fieldrow__req {
    margin-left: 3px;
    color: var(--q-red);
}

.fieldrow__control {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.fieldrow__control > .input {
    width: 100%;
}

/* --------------------------------------------------- how wide a control is

   A control filled the value column, and the value column is as wide as the pane — so a year, a
   checkbox and a synopsis all got the same six hundred pixels and the form read as a column of
   identical boxes holding wildly different things. A width per control instead, from the word the
   row already carries in data-control, so nothing here has to be told twice.

   Four sizes, and the reason for four rather than one: the box is a promise about what goes in it.
   A field that wants four digits should not offer room for a sentence.

   --field-w is the content type's own answer, written on the row by AssetsFieldEditor where the
   field names one. It is not set otherwise, which is what lets the fallback below apply — and what
   makes a stated width win without needing to out-specify anything.

   max-width rather than width: every one of these still shrinks, so a narrow window and the
   single-column breakpoint above go on working untouched. */
.fieldrow {
    /* Text, and the pickers that read as text — a name, a choice, a list of tags. Long enough for a
       title, short enough that the eye still finds the end of it. */
    --field-w-default: 380px;
}

/* A count, a measure. Four or five characters and a pair of spinners. */
.fieldrow[data-control="number"],
.fieldrow[data-control="unit"] {
    --field-w-default: 160px;
}

/* A rating is exactly as wide as its stars and its readout, and both are settings — five stars or
   ten, at whatever size Control.Width asked for. So the content is the measurement, which is what
   max-content says, and no number written here could be right for every rating. Still a cap: the
   column under it is minmax(0, 1fr), so a narrow pane shrinks this like anything else. */
.fieldrow[data-control="rating"] {
    --field-w-default: max-content;
}

/* Fixed-shape values: a date is always a date's width, and a swatch or a switch is smaller than
   that. Sized to the widest of them so they line up down the form rather than stepping. */
.fieldrow[data-control="date"],
.fieldrow[data-control="time"],
.fieldrow[data-control="colour"],
.fieldrow[data-control="toggle"],
.fieldrow[data-control="switch"] {
    --field-w-default: 180px;
}

/* A slider is a track and a readout side by side, and 180px was never enough for both: the track
   alone has a 6rem floor, the readout reserves 4ch and there is a .75rem gap between them, so the
   readout was pushed under the track on every slider in the product. This is those three plus room
   for the track to be a track rather than a nub. */
.fieldrow[data-control="slider"] {
    --field-w-default: 300px;
}

/* Prose, and the controls that stack their options. These earn the room: a synopsis typed four
   words to a line is unreadable, and a checklist wrapped mid-option is worse. */
.fieldrow[data-control="textarea"],
.fieldrow[data-control="richtext"],
.fieldrow[data-control="checklist"],
.fieldrow[data-control="radiolist"],
.fieldrow[data-control="relation"],
.fieldrow[data-control="media"],
.fieldrow[data-control="assetselector"],
.fieldrow[data-control="itemselector"],
.fieldrow[data-control="classificationselector"] {
    --field-w-default: 620px;
}

/* Last, so it beats any tier above it on the same specificity. A presentation control has already
   given up its label cell to span the form; capping it here would take back what data-full gave. */
.fieldrow[data-full="true"] {
    --field-w-default: none;
}

.fieldrow__control {
    max-width: var(--field-w, var(--field-w-default));
}

/* A button is as wide as its word. The control column is a flex column, so without this every
   button in it is stretched to the column's full width — which is how "Attach…" came to be a
   six-hundred-pixel target for an eight-character label. */
.fieldrow__control > .button,
.fieldrow__control > .fieldlinks__add {
    align-self: flex-start;
}

.input--num {
    width: 100%;
    max-width: 160px;
}

.fieldrow__hint,
.fieldrow__none,
.fieldrow__ro {
    margin: 0;
    font-size: 11.5px;
    color: var(--text-muted);
}

.fieldrow__ro {
    padding: 7px 0;
    color: var(--text);
    overflow-wrap: anywhere;
}

.fieldrow__badges {
    display: flex;
    align-items: center;
    gap: 4px;
    flex-wrap: wrap;
    margin: 0;
}

/* What is true about a field beyond its value — that it is linked to another, that it notifies, that
   it can expire. Behaviour attached elsewhere in the platform and invisible from the value, so it is
   drawn rather than dropped. */
.fieldbadge {
    padding: 1px 6px;
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Unset, yes and no. A boolean column that has never been written is null, and a two-state switch
   would turn every untouched flag into a deliberate no the moment anybody saved. */
.fieldtri {
    display: inline-flex;
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    overflow: hidden;
    width: fit-content;
}

.fieldtri__option {
    padding: 5px 13px;
    border: 0;
    background: none;
    font: inherit;
    font-size: 12px;
    color: var(--text-muted);
    cursor: pointer;
}

.fieldtri__option + .fieldtri__option {
    border-left: 1px solid var(--border);
}

.fieldtri__option:hover:not(:disabled) {
    color: var(--text);
}

.fieldtri__option[aria-pressed="true"] {
    background: var(--q-violet);
    color: #fff;
    font-weight: 600;
}

/* A value made of parts — days, hours and minutes, or a day and a month. Small boxes rather than one,
   because 31536000 is not a number anybody can check by eye and 29-02 is not a date. */
.fieldspan {
    display: flex;
    align-items: flex-end;
    gap: 8px;
    flex-wrap: wrap;
}

.fieldspan__part {
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-size: 10.5px;
    color: var(--text-muted);
}

.fieldspan__part .input {
    width: 84px;
}

/* --------------------------------------------------------------------- chips */

.fieldchips {
    display: flex;
    align-items: center;
    gap: 5px;
    flex-wrap: wrap;
}

/* What a field points at, plus the way to change it. Unlike fieldchips above, these are the
   answers rather than the candidates -- the candidates are a tenant's whole tree and live in a
   dialog. */
.fieldpick {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}

/* Adding a value to a classification set. Set apart from the chips above it rather than sitting
   among them, because it changes a vocabulary the whole tenant shares -- which is a different act
   from choosing one of the values already in it. */
.fieldadd {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px dashed var(--border);
}

.fieldadd .input {
    max-width: 220px;
}

/* ------------------------------------------------------------------ tooltips and validation */

/* The sentence a content type wrote about how to fill a field in. A button rather than a title
   attribute, so a keyboard and a touch screen can both reach it. */
.fieldtip {
    position: relative;
    display: inline-flex;
    margin-left: 4px;
}

.fieldtip__button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--text-muted);
    cursor: pointer;
}

.fieldtip__button:hover,
.fieldtip__button[aria-expanded="true"] {
    color: var(--q-violet);
}

.fieldtip__note {
    position: absolute;
    top: calc(100% + 6px);
    left: -8px;
    z-index: 20;
    width: max-content;
    max-width: 260px;
    padding: 8px 10px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--surface);
    box-shadow: var(--shadow-pop, 0 6px 18px rgb(0 0 0 / .18));
    font-size: 12px;
    font-weight: 400;
    line-height: 1.45;
    color: var(--text);
    white-space: normal;
}

/* Why a value is refused. Same scale as the hint under it, and a different tone, because one is
   advice and the other is a refusal. */
.fieldrow__bad {
    margin: 4px 0 0;
    font-size: 12px;
    color: var(--q-red);
}

.fieldrow[data-invalid="true"] .fieldrow__control > .input {
    border-color: var(--q-red);
}

/* The chip is a span elsewhere in this application; here it is a button, so it needs to say so. */
button.chip {
    cursor: pointer;
    font: inherit;
    font-size: 12px;
    padding: 4px 11px;
    transition: background 120ms var(--step-cubic), border-color 120ms var(--step-cubic),
                color 120ms var(--step-cubic);
}

button.chip:hover:not(:disabled) {
    border-color: var(--border-strong);
}

button.chip[data-on="true"] {
    background: color-mix(in srgb, var(--q-violet) 16%, transparent);
    border-color: var(--q-violet);
    color: var(--q-violet);
    font-weight: 600;
}

button.chip:disabled {
    cursor: not-allowed;
    opacity: .5;
}

/* --------------------------------------------------------------------- attached media */

.fieldassets {
    display: flex;
    flex-direction: column;
    gap: 5px;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* The asset type a run of these rows belongs to. Quiet and small: it is a divider between near
   identical file names rather than a heading anybody reads for its own sake, and the panel already
   has a heading of its own two lines up. */
.fieldassets__group {
    display: flex;
    align-items: baseline;
    gap: 7px;
    margin: 12px 0 5px;
    color: var(--text-muted);
}

/* First of the run sits under the fold's own heading, which is spacing enough. */
.fieldassets__group:first-of-type {
    margin-top: 4px;
}

.fieldassets__groupname {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 10.5px;
    letter-spacing: .07em;
    text-transform: uppercase;
}

.fieldassets__groupcount {
    font-size: 10.5px;
    font-variant-numeric: tabular-nums;
    opacity: .75;
}

.fieldassets__item {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    font-size: 12px;
}

/* A row that opens something. A button rather than a decorated list item, so the keyboard reaches
   each one and the pointer says what will happen. */
.fieldassets__item--open {
    width: 100%;
    padding: 3px 5px;
    border: 1px solid transparent;
    border-radius: 8px;
    background: none;
    font: inherit;
    font-size: 12px;
    color: inherit;
    text-align: left;
    cursor: pointer;
}

.fieldassets__item--open:hover,
.fieldassets__item--open:focus-visible {
    background: var(--bg-accent);
    border-color: var(--border);
}

.fieldassets__thumb {
    flex: none;
    width: 40px;
    height: 28px;
    border-radius: 6px;
    object-fit: cover;
    background: var(--surface-sunken);
    border: 1px solid var(--border);
}

/* Nothing to show, so the shape says what kind of thing it is instead of pretending to a preview. */
.fieldassets__thumb--none {
    display: grid;
    place-items: center;
}

.fieldassets__thumb--none::after {
    content: "file";
    font-family: var(--font-mono);
    font-size: 8px;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.fieldassets__thumb--none[data-kind="video"]::after { content: "video"; }
.fieldassets__thumb--none[data-kind="audio"]::after { content: "audio"; }
.fieldassets__thumb--none[data-kind="image"]::after { content: "image"; }
.fieldassets__thumb--none[data-kind="document"]::after { content: "doc"; }

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

.fieldassets__size {
    flex: none;
    font-family: var(--font-mono);
    font-size: 10.5px;
    color: var(--text-muted);
}

/* --------------------------------------------------------------------- history */

.versions {
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.versions__head,
.versions__row {
    display: grid;
    grid-template-columns: 62px 150px minmax(120px, 190px) minmax(0, 1fr) auto;
    gap: 10px;
    align-items: center;
    padding: 5px 7px;
    font-size: 12px;
}

/* A face and a name, not a login. The name ellipses rather than wrapping: the row is one line high
   and a two-line byline would push the whole table out of step with its own header. */
.versions__by {
    display: flex;
    align-items: center;
    gap: 7px;
    min-width: 0;
}

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

.versions__head {
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-muted);
    border-bottom: 1px solid var(--border);
}

.versions__row {
    border-radius: 7px;
    border: 1px solid transparent;
}

.versions__row[data-picked="true"] {
    border-color: var(--q-violet);
    background: color-mix(in srgb, var(--q-violet) 10%, transparent);
}

/* The version a plain read of the item returns. Marked because "newest" and "current" are not the
   same claim, and only one row in the table is the one the details pane is showing. */
.versions__row[data-current="true"] .versions__num {
    color: var(--q-lime);
}

.versions__num {
    font-family: var(--font-mono);
    font-size: 11.5px;
    color: var(--q-violet);
}

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

.versions__pick,
.versions__pickhead {
    display: flex;
    align-items: center;
    gap: 5px;
    justify-content: flex-end;
}

@media (max-width: 900px) {
    .versions__head {
        display: none;
    }

    .versions__row {
        grid-template-columns: 62px minmax(0, 1fr);
        row-gap: 2px;
    }

    .versions__pick {
        grid-column: 1 / -1;
        justify-content: flex-start;
    }
}

/* --------------------------------------------------------------------- comparing two versions */

.versiondiff {
    padding-top: 10px;
    border-top: 1px solid var(--border);
}

.versiondiff__head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 6px;
}

.versiondiff__tally {
    margin: 0;
    font-size: 11.5px;
    color: var(--text-muted);
}

.versiondiff__head .button {
    margin-left: auto;
}

.versiondiff__grid {
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.versiondiff__row {
    display: grid;
    grid-template-columns: minmax(120px, 24%) 1fr 1fr;
    gap: 10px;
    padding: 5px 7px;
    border-radius: 7px;
    font-size: 12px;
    align-items: start;
}

.versiondiff__row--head {
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-muted);
    border-bottom: 1px solid var(--border);
    border-radius: 0;
}

/* Only the changed rows are tinted, and both halves of the pair are marked — the old value in the
   colour of something taken away and the new one in the colour of something added, which is the one
   diff convention every reader already has. */
.versiondiff__row[data-changed="true"] {
    background: color-mix(in srgb, var(--q-amber) 9%, transparent);
}

.versiondiff__label {
    color: var(--text-muted);
    overflow-wrap: anywhere;
}

.versiondiff__was,
.versiondiff__now {
    min-width: 0;
    overflow-wrap: anywhere;
    white-space: pre-wrap;
}

.versiondiff__row[data-changed="true"] .versiondiff__was {
    color: var(--q-red);
    text-decoration: line-through;
    text-decoration-color: color-mix(in srgb, var(--q-red) 45%, transparent);
}

.versiondiff__row[data-changed="true"] .versiondiff__now {
    color: color-mix(in srgb, var(--q-lime) 78%, var(--text));
    font-weight: 600;
}

@media (max-width: 900px) {
    .versiondiff__row {
        grid-template-columns: minmax(0, 1fr);
        row-gap: 2px;
    }

    .versiondiff__row--head {
        display: none;
    }
}

/* --------------------------------------------------------------------- the dialogs */

/* A destination picker is a tree, and a tree needs room to be one: a dialog that showed four rows at
   a time would make somebody scroll to find a folder they can already see in the pane behind. */
.modal__panel--tall {
    width: min(560px, 94vw);
    max-height: 84dvh;
}

.modal__body--scroll {
    overflow: auto;
    min-height: 0;
}

/* Why a folder cannot take it, on the row itself. Greying it out silently would leave somebody
   clicking a destination that looks available. */
.itemtree__why {
    margin-left: auto;
    flex: none;
    font-size: 10.5px;
    color: var(--text-muted);
}

@media (prefers-reduced-motion: reduce) {
    .assettree__spinner {
        animation: none;
    }
}

/* ==========================================================================
   The playout board — /playout and /playout/{id}
   ========================================================================== */

/* A wall of cards, each the colour of how the Quatro Monitor finds that channel. The colour IS the
   information here: this board is meant to be left open on a second screen and glanced at, and the
   only question a glance can answer is "is anything not green".

   The fills are darker cousins of the brand tokens rather than a second palette — the bars are
   literally the brand lime, amber and violet — so a screen full of these still reads as this
   application. They are dark enough for white text in both themes, which is why the tones are
   stated once here and then only nudged for dark. */
:root {
    --playout-ok: #2F8F3E;
    --playout-ok-bar: var(--q-lime);
    --playout-warn: #B07407;
    --playout-warn-bar: var(--q-amber);
    --playout-error: #A11919;
    --playout-error-bar: #F05252;
    --playout-ingest: #4B3FB0;
    --playout-ingest-bar: var(--q-violet-light);
    --playout-unknown: #3B3A55;
    --playout-unknown-bar: #8A8BAB;
}

:root[data-theme="dark"] {
    /* Pulled down a little: the same fills over a near-black page are brighter than they are over a
       light one, and thirty of them at that weight is a glare rather than a board. */
    --playout-ok: #276F32;
    --playout-warn: #8C5C05;
    --playout-error: #831414;
    --playout-ingest: #3B3190;
    --playout-unknown: #2B2A3F;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --playout-ok: #276F32;
        --playout-warn: #8C5C05;
        --playout-error: #831414;
        --playout-ingest: #3B3190;
        --playout-unknown: #2B2A3F;
    }
}

/* ------------------------------------------------------------------------- the bar */

/* The board's controls sit on one row with the freshness stamp, so the search box is sized for a
   toolbar rather than for a form. */
.input--inline {
    width: auto;
    min-width: 190px;
    height: 30px;
    padding: 0 10px;
    font-size: 12.5px;
}

/* The column-count picker: six one-character buttons, which at the segmented control's ordinary
   padding would be six wide buttons carrying one digit each. */
.segmented__option--narrow {
    min-width: 26px;
    padding-inline: 0;
    font-variant-numeric: tabular-nums;
}

/* ----------------------------------------------------------------------- the grid */

/* How many across is the reader's own answer, held in their browser: a wall on a 4K panel wants six
   and the same board in a side window wants one. The minmax(0, 1fr) matters — without the zero
   minimum a long file name would push its column wider than its share and the grid would stop being
   a grid. */
.playoutgrid {
    display: grid;
    gap: 10px;
    grid-template-columns: repeat(var(--across, 3), minmax(0, 1fr));
}

.playoutgrid[data-across="1"] { --across: 1; }
.playoutgrid[data-across="2"] { --across: 2; }
.playoutgrid[data-across="3"] { --across: 3; }
.playoutgrid[data-across="4"] { --across: 4; }
.playoutgrid[data-across="5"] { --across: 5; }
.playoutgrid[data-across="6"] { --across: 6; }

/* Whatever was asked for, a card narrower than this is unreadable. The chosen count is kept for the
   window that can hold it rather than overridden permanently. */
@media (max-width: 1500px) {
    .playoutgrid[data-across="5"],
    .playoutgrid[data-across="6"] { --across: 4; }
}

@media (max-width: 1100px) {
    .playoutgrid[data-across="3"],
    .playoutgrid[data-across="4"] { --across: 2; }
}

@media (max-width: 720px) {
    .playoutgrid { --across: 1; }
}

/* ---------------------------------------------------------------------- one card */

.playoutcard {
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-height: 158px;
    padding: 16px 18px 14px;
    border-radius: 14px;
    background: var(--playout-unknown);
    color: #FFFFFF;
    text-decoration: none;
    box-shadow: 0 1px 2px rgb(20 19 43 / 12%);
    transition: transform 140ms var(--step-cubic), box-shadow 140ms var(--step-cubic);
}

.playoutcard:hover {
    transform: translateY(-1px);
    box-shadow: 0 2px 4px rgb(20 19 43 / 16%), 0 16px 32px -20px rgb(20 19 43 / 40%);
}

.playoutcard:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.playoutcard[data-tone="ok"] { background: var(--playout-ok); }
.playoutcard[data-tone="warn"] { background: var(--playout-warn); }
.playoutcard[data-tone="error"] { background: var(--playout-error); }
.playoutcard[data-tone="ingest"] { background: var(--playout-ingest); }

.playoutcard[data-tone="ok"] .playoutbar__fill { background: var(--playout-ok-bar); }
.playoutcard[data-tone="warn"] .playoutbar__fill { background: var(--playout-warn-bar); }
.playoutcard[data-tone="error"] .playoutbar__fill { background: var(--playout-error-bar); }
.playoutcard[data-tone="ingest"] .playoutbar__fill { background: var(--playout-ingest-bar); }

/* A channel the monitor is not reporting is drawn as an outline rather than a fill. It is the one
   card whose colour would otherwise be a claim about a channel nobody has heard from. */
.playoutcard[data-tone="unknown"] {
    background: transparent;
    border: 1px dashed var(--border-strong);
    color: var(--text-muted);
}

.playoutcard__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 10px;
}

.playoutcard__title {
    margin: 0;
    font-family: var(--font-display);
    font-size: 17px;
    font-weight: 800;
    line-height: 1.2;
    /* Two lines and then an ellipsis: channel names run long, and a card that grew to fit one would
       break the row it is in. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* The house code, boxed. On a wall of thirty cards this is what somebody is actually scanning for,
   so it is the one thing on the card set apart from the prose. */
.playoutcard__code {
    flex: none;
    padding: 2px 8px;
    border-radius: 6px;
    background: rgb(0 0 0 / 22%);
    font-family: var(--font-mono);
    font-size: 11.5px;
    font-weight: 600;
    letter-spacing: .04em;
    white-space: nowrap;
}

.playoutcard[data-tone="unknown"] .playoutcard__code {
    background: var(--surface-sunken);
}

.playoutbar {
    height: 8px;
    border-radius: 999px;
    background: rgb(0 0 0 / 22%);
    overflow: hidden;
}

.playoutcard[data-tone="unknown"] .playoutbar {
    background: var(--surface-sunken);
}

.playoutbar__fill {
    display: block;
    height: 100%;
    background: var(--playout-unknown-bar);
    /* Eased rather than jumping: the board re-reads every few seconds, and a bar that stepped would
       read as the page stuttering rather than as the item advancing. */
    transition: width 600ms linear;
}

.playoutcard__now {
    margin: 0;
    font-size: 13px;
    font-weight: 700;
    color: rgb(255 255 255 / 82%);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.playoutcard[data-tone="unknown"] .playoutcard__now {
    color: var(--text-muted);
}

.playoutcard__notes {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-size: 12px;
    color: rgb(255 255 255 / 88%);
}

.playoutcard__note-label {
    display: inline-block;
    min-width: 62px;
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: rgb(255 255 255 / 62%);
}

.playoutcard[data-tone="unknown"] .playoutcard__notes,
.playoutcard[data-tone="unknown"] .playoutcard__note-label {
    color: var(--text-muted);
}

/* Pushed to the bottom of the card so a row of cards agrees on where its footer is, whatever number
   of notes the cards above it happen to carry. */
.playoutcard__foot {
    margin-top: auto;
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex-wrap: wrap;
    padding-top: 4px;
    font-size: 11px;
    color: rgb(255 255 255 / 68%);
}

.playoutcard[data-tone="unknown"] .playoutcard__foot {
    color: var(--text-muted);
}

.playoutcard__channel {
    font-weight: 700;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.playoutcard__tenant::before,
.playoutcard__host::before {
    content: "·";
    margin-right: 6px;
    opacity: .6;
}

.playoutcard__host {
    font-family: var(--font-mono);
    font-size: 10px;
}

.playoutcard__system {
    padding-top: 8px;
    border-top: 1px solid rgb(255 255 255 / 18%);
}

.playoutcard[data-tone="unknown"] .playoutcard__system {
    border-top-color: var(--border);
}

/* --------------------------------------------------------------------- the gauges */

.gauges {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 6px;
}

.gauge {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}

/* One conic gradient and no script. A board can carry thirty of these refreshing every couple of
   seconds, and thirty animated widgets would cost more than the board they describe.

   The hole in the middle is a second element painted the card's own colour rather than a mask,
   because the card's colour is a variable and a mask cannot be one. */
.gauge__ring {
    position: relative;
    display: grid;
    place-items: center;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    background: conic-gradient(currentColor calc(var(--fill, 0) * 1%), rgb(255 255 255 / 22%) 0);
    color: rgb(255 255 255 / 78%);
}

.gauge__ring[data-tone="warn"] { color: var(--q-amber); }
.gauge__ring[data-tone="bad"] { color: #F05252; }

.gauge__ring::after {
    content: "";
    position: absolute;
    inset: 4px;
    border-radius: 50%;
    background-color: var(--playout-unknown);
}

.playoutcard[data-tone="ok"] .gauge__ring::after { background-color: var(--playout-ok); }
.playoutcard[data-tone="warn"] .gauge__ring::after { background-color: var(--playout-warn); }
.playoutcard[data-tone="error"] .gauge__ring::after { background-color: var(--playout-error); }
.playoutcard[data-tone="ingest"] .gauge__ring::after { background-color: var(--playout-ingest); }

.playoutcard[data-tone="unknown"] .gauge__ring {
    background: conic-gradient(currentColor calc(var(--fill, 0) * 1%), var(--border) 0);
    color: var(--text-muted);
}

.playoutcard[data-tone="unknown"] .gauge__ring::after {
    background-color: var(--bg);
}

.gauge__value {
    position: relative;
    z-index: 1;
    font-family: var(--font-mono);
    font-size: 10.5px;
    font-weight: 600;
    color: #FFFFFF;
    font-variant-numeric: tabular-nums;
}

.playoutcard[data-tone="unknown"] .gauge__value {
    color: var(--text);
}

.gauge__label {
    font-family: var(--font-mono);
    font-size: 8.5px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: rgb(255 255 255 / 62%);
}

.playoutcard[data-tone="unknown"] .gauge__label {
    color: var(--text-muted);
}

/* ------------------------------------------------------------------ one channel */

.playouthead {
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
}

.playouthead .page__header {
    margin: 0;
}

.playouthead__code {
    display: inline-block;
    margin-right: 8px;
    padding: 1px 8px;
    border-radius: 6px;
    background: var(--bg-accent);
    font-family: var(--font-mono);
    font-size: 15px;
    font-weight: 600;
    vertical-align: 2px;
}

.playouthead .boardbar__live {
    margin-left: auto;
}

/* The card and the facts side by side: the card is the channel's state and the facts are its detail,
   and on a detail page both are wanted at once rather than one under the other. */
.playoutdetail {
    display: grid;
    grid-template-columns: minmax(300px, 1fr) minmax(0, 2fr);
    gap: 12px;
    align-items: start;
}

@media (max-width: 1000px) {
    .playoutdetail {
        grid-template-columns: minmax(0, 1fr);
    }
}

.playoutdetail__card .playoutcard {
    min-height: 0;
}

.playoutfacts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 12px;
    padding: 14px 16px;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: var(--surface);
}

.playoutfacts__list {
    margin: 0;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 2px 12px;
    align-content: start;
}

/* display: contents so each label-and-value pair keeps its own row in the grid while still being one
   element in the markup — a definition list whose pairs are not grouped is a list nothing can style
   per row. */
.playoutfacts__list > div {
    display: contents;
}

.playoutfacts__list dt {
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--text-muted);
    align-self: center;
}

.playoutfacts__list dd {
    margin: 0;
    font-size: 13px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.playoutfacts__wide dt {
    grid-column: 1;
}

.playoutfacts .mono,
.admintable .mono {
    font-family: var(--font-mono);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
}

/* -------------------------------------------------------------------- the schedule */

/* The rundowns on the left and the chosen rundown's items on the right, which is the shape the
   monitor's own console uses and the shape the work has: pick the hour, read the items in it. */
.playoutschedule {
    display: grid;
    grid-template-columns: minmax(320px, 1fr) minmax(0, 2fr);
    gap: 12px;
    align-items: start;
}

@media (max-width: 1100px) {
    .playoutschedule {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* A rundown row is a button, because picking one is what the table is for. Reset to lay out exactly
   like the rows around it — a button that looked like a button here would give the table two visual
   languages for one list. */
.admintable__row--button {
    appearance: none;
    border: 0;
    border-radius: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
    width: 100%;
}

.admintable__row--button:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.admintable__row[data-selected="true"] {
    background: var(--bg-accent);
    box-shadow: inset 3px 0 0 var(--q-violet);
}

/* The item on air, and the rundown it is in. Marked rather than coloured over: these rows sit in an
   ordinary table and the mark has to survive the row being hovered, selected, or both. */
.admintable__row[data-onair="true"] .admintable__name {
    font-weight: 700;
}

.admintable__row[data-onair="true"] {
    box-shadow: inset 3px 0 0 var(--q-lime);
}

.admintable__row[data-selected="true"][data-onair="true"] {
    background: var(--bg-accent);
    box-shadow: inset 3px 0 0 var(--q-lime);
}

/* An item whose file is not on the box will play as black. It is the one thing in the items table
   that can take a channel off air, so the row says so and not only the cell. */
.admintable__row[data-missing="true"] .admintable__name {
    color: var(--q-red);
}

@media (prefers-reduced-motion: reduce) {
    .playoutcard,
    .playoutbar__fill {
        transition: none;
    }

    .playoutcard:hover {
        transform: none;
    }
}

/* ================================================================= ad insertion */

/* The board's cards borrow the playout monitor's colour tokens on purpose: two monitoring screens
   in one application that meant different things by green would be worse than a little repetition
   here. What they do not share is their shape — an ad insertion card carries breaks and spots
   rather than a progress bar, so the markup is its own. */

.aiboard {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 14px;
    margin-top: 16px;
}

.aicard {
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-height: 132px;
    padding: 16px 18px 14px;
    border-radius: 14px;
    background: var(--playout-unknown);
    color: #FFFFFF;
    text-decoration: none;
    box-shadow: 0 1px 2px rgb(20 19 43 / 12%);
    transition: transform 140ms var(--step-cubic), box-shadow 140ms var(--step-cubic);
}

.aicard:hover {
    transform: translateY(-1px);
    box-shadow: 0 2px 4px rgb(20 19 43 / 16%), 0 16px 32px -20px rgb(20 19 43 / 40%);
}

.aicard:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.aicard[data-tone="ok"] { background: var(--playout-ok); }
.aicard[data-tone="warn"] { background: var(--playout-warn); }
.aicard[data-tone="error"] { background: var(--playout-error); }

/* A channel the monitor is not reporting is an outline rather than a fill, for the same reason the
   playout board draws one: any colour would be a claim about a channel nobody has heard from. */
.aicard[data-tone="unknown"] {
    background: transparent;
    border: 1px dashed var(--border-strong);
    color: var(--text-muted);
}

.aicard__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 10px;
}

.aicard__title {
    margin: 0;
    font-size: 15px;
    font-weight: 600;
    line-height: 1.25;
}

/* How many spots are waiting to be looked at — the one number worth reading across a wall. */
.aicard__count {
    flex: none;
    min-width: 22px;
    padding: 1px 7px;
    border-radius: 999px;
    background: rgb(255 255 255 / 22%);
    font-family: var(--font-mono);
    font-size: 11.5px;
    text-align: center;
}

.aicard__now {
    margin: 0;
    font-size: 12.5px;
    opacity: .92;
}

.aicard__notes {
    display: flex;
    flex-direction: column;
    gap: 3px;
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: 11.5px;
    opacity: .92;
}

.aicard__note-label {
    display: inline-block;
    min-width: 74px;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .08em;
    text-transform: uppercase;
    opacity: .8;
}

/* The same list on the channel's own page, where it sits on a panelcard rather than on colour. */
.aicard__notes--plain {
    margin-top: 14px;
    color: var(--text-muted);
    opacity: 1;
}

.aicard__foot {
    display: flex;
    align-items: baseline;
    gap: 10px;
    margin-top: auto;
    padding-top: 6px;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .06em;
    opacity: .78;
}

.aicard__name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.aicard__server {
    margin-left: auto;
    flex: none;
}

/* One channel's page: the two summaries side by side across the whole width, and the spots beneath
   them with everything that is left.

   Two explicit columns rather than auto-fit, which is what this used to be and got wrong: a child
   spanning 1 / -1 pins the track count, so auto-fit could not collapse the tracks the summaries did
   not fill and the two of them huddled at the left of a mostly empty row.

   Deliberately not .settings either, which is a column flow — a spot table cut in half and
   continued in the next column is a table nobody can read down. */
.aipage {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px;
    align-items: start;
    margin-top: 16px;
}

.aipage__wide {
    grid-column: 1 / -1;
}

/* Below this there is no room for two of anything. */
@media (max-width: 900px) {
    .aipage {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* The summary's facts, across the pane rather than down it. Nine of them, none more than a few
   characters wide: as a column they cost the height the spot table wants, and as a wrapped row they
   cost two lines. */
.aifacts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: 12px 20px;
}

.aifact {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}

/* The one that carries a sentence rather than a word. */
.aifact--wide {
    grid-column: span 2;
}

.aifact__label {
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.aifact__value {
    font-size: 13.5px;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* A real table, unlike .admintable — which despite the name is a grid of divs, so a <table> wearing
   it gets the border and none of the rest. Kept as a table because these two are tabular: the spot
   list is read down a column, and columns that line themselves up are the whole point. */
.aitable {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}

.aitable th {
    position: sticky;
    top: 0;
    z-index: 1;
    padding: 7px 10px;
    text-align: left;
    white-space: nowrap;
    background: var(--surface-sunken);
    border-bottom: 1px solid var(--border);
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 9.5px;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Only where the table is inside something that scrolls; a four-row table has nothing to stick to
   and a sticky header over a page scroll would float across the card above it. */
.aitable:not(.aitable--sticky) th {
    position: static;
}

.aitable td {
    padding: 6px 10px;
    border-top: 1px solid var(--border);
    vertical-align: middle;
}

.aitable tbody tr:hover {
    background: color-mix(in srgb, var(--q-violet) 4%, transparent);
}

/* A day nobody has been asked to check yet: present, but not competing with the one that matters. */
.aitable tr[data-quiet="true"] {
    opacity: .5;
}

/* A long table scrolls inside its own card rather than taking the page with it. On the wrapper and
   not on the table, because a table told to scroll has to stop being a table to do it, and its
   columns stop lining up the moment it does.
   Sized against the window rather than at a fixed height: the two summaries above it are now two
   rows of facts, and what they save should go to the spots rather than to blank page. */
.aiscroll {
    max-height: max(320px, calc(100vh - 400px));
    overflow: auto;
}

.ailist {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin: 12px 0 0;
    padding: 0;
    list-style: none;
    font-family: var(--font-mono);
    font-size: 11.5px;
    color: var(--text-muted);
}

.ailist__quiet {
    font-family: var(--font-mono);
    font-size: 10.5px;
    color: var(--text-muted);
}

/* Fixed-width CCMS text, shown as the ad server wrote it. It scrolls in both directions rather
   than wrapping: a wrapped line of a column-aligned file is no longer readable as one. */
.aifile {
    margin: 0;
    padding: 12px 14px;
    max-height: 60vh;
    overflow: auto;
    border-radius: 10px;
    background: var(--surface-sunken);
    font-family: var(--font-mono);
    font-size: 11.5px;
    line-height: 1.5;
    white-space: pre;
}

/* =================================================================== teleshopping */

/* The same colour tokens again, for the third monitoring screen. Green here means something the
   other two do not: not "this is well" but "tomorrow is ready", because these checks run against a
   day that has not aired. The shape is its own — a teleshopping card carries a state and nothing
   else, since a day is either prepared or it is not and the reasons are a page away. */

.tsboard {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
    margin-top: 16px;
}

.tscard {
    display: flex;
    flex-direction: column;
    gap: 6px;
    justify-content: space-between;
    min-height: 104px;
    padding: 16px 18px 14px;
    border-radius: 14px;
    background: var(--playout-unknown);
    color: #FFFFFF;
    text-decoration: none;
    box-shadow: 0 1px 2px rgb(20 19 43 / 12%);
    transition: transform 140ms var(--step-cubic), box-shadow 140ms var(--step-cubic);
}

.tscard:hover {
    transform: translateY(-1px);
    box-shadow: 0 2px 4px rgb(20 19 43 / 16%), 0 16px 32px -20px rgb(20 19 43 / 40%);
}

.tscard:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.tscard[data-tone="ok"] { background: var(--playout-ok); }
.tscard[data-tone="warn"] { background: var(--playout-warn); }
.tscard[data-tone="error"] { background: var(--playout-error); }

/* A state this application cannot read is an outline rather than a fill, exactly as an unreported
   channel is on the other two boards: any colour would be a claim about a word nobody has checked. */
.tscard[data-tone="unknown"] {
    background: transparent;
    border: 1px dashed var(--border-strong);
    color: var(--text-muted);
}

.tscard__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 10px;
}

.tscard__title {
    margin: 0;
    font-size: 15px;
    font-weight: 600;
    line-height: 1.25;
}

/* Upstream's own word for the day, printed as it sends it — the colour is this application's
   reading of it, and the word is here so the two can be seen to agree. */
.tscard__state {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 11.5px;
    letter-spacing: .06em;
    text-transform: uppercase;
    opacity: .92;
}

/* The four stages down the page rather than across it. Each is a table as wide as it needs, and two
   side by side would put every one of them into its own horizontal scroller. */
.tspage {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 14px;
    align-items: start;
    margin-top: 16px;
}

/* On the dashboard the card carries the day under it: a green card here means tomorrow is ready,
   which is not what a glance at a monitoring widget otherwise assumes. */
.tswidget {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.tswidget__day {
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-muted);
    text-align: right;
}

/* ==================================================================== the dashboard */

/* The widgets a dashboard is made of, and the menu that chooses them. A widget is an ordinary card
   on the ordinary canvas — these are only the parts that are not already a panelcard, a datalist or
   somebody else's card being reused whole. */

/* Anything that opens a menu below itself. The top bar's own controls get this from .profile;
   the arrange bar's needs it without being a profile. */
.menuanchor {
    position: relative;
    flex: none;
}

/* A plain list of what can be added, now that what each widget is pointed at lives on the widget.
   Wide enough for the longest name and no wider. */
.widgetmenu {
    min-width: 230px;
}

/* One widget's own settings, on its own card, while the canvas is being arranged.

   Over the drag overlay rather than under it — the overlay is the whole card, so anything beneath
   it cannot be clicked — and only as tall as its controls, so the rest of the card still drags.
   Enough of a surface of its own to be legible against whatever the card is showing behind it,
   which for the live video widget is a moving picture. */
.widgetsetup {
    position: absolute;
    inset: 0 0 auto;
    z-index: 8;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    padding: 8px 8px 8px 12px;
    border: 1px solid color-mix(in srgb, var(--q-violet) 35%, transparent);
    border-radius: 14px 14px 10px 10px;
    background: color-mix(in srgb, var(--surface) 93%, var(--q-violet) 7%);
    box-shadow: 0 12px 26px -16px rgb(0 0 0 / 55%);
}

.widgetsetup__name {
    flex: none;
    font-size: 12.5px;
    font-weight: 650;
    white-space: nowrap;
}

.widgetsetup__controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    flex: 1;
    min-width: 0;
}

/* Choosers share the line they are on and wrap onto the next when there is no room, so a clock's
   three of them are one line on a wide card and two on a narrow one rather than always three. */
.widgetsetup__controls .input--sm {
    flex: 1 1 128px;
    min-width: 0;
}

/* A checkbox is not a chooser and does not stretch like one — it is as wide as its label. */
.widgetsetup__check {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    flex: none;
    font-size: 12.5px;
    white-space: nowrap;
    cursor: pointer;
}

.widgetsetup__check input {
    accent-color: var(--q-violet);
    margin: 0;
    cursor: pointer;
}

.widgetsetup__gear,
.widgetsetup__remove {
    flex: none;
    margin-left: auto;
}

/* Only the first of the pair pushes; the two then sit together at the right end. */
.widgetsetup__gear + .widgetsetup__remove {
    margin-left: 0;
}

.widgetsetup__gear[data-on="true"] {
    background: color-mix(in srgb, var(--q-violet) 16%, transparent);
    color: var(--q-violet);
}

/* A chooser inside a setup strip, where a full-height field would make every card's strip a
   paragraph tall. */
.input--sm {
    height: 30px;
    padding: 0 8px;
    font-size: 12.5px;
    border-radius: 9px;
}

.menu__meta {
    margin-left: auto;
    flex: none;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* --------------------------------------------------------------------- one widget */

.widget__head {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 14px;
}

.widget__title {
    margin: 0;
    font-size: 14px;
    font-weight: 600;
}

/* What a widget is pointed at, said quietly beside its name: it is how you tell two of them
   apart, and it is not the heading. */
.widget__scope {
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: .04em;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The refresh sits at the far end of the head, whatever else is in it. */
.widget__head > .iconbutton {
    margin-left: auto;
}

/* A widget with nothing to draw says why, in the space the card would have taken. */
.widget__blank {
    display: flex;
    flex-direction: column;
    gap: 8px;
    justify-content: center;
    min-height: 126px;
}

.widget__note {
    margin: 0;
    font-size: 13px;
    color: var(--text-muted);
}

.widget__foot {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 14px;
    margin-top: 12px;
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: .04em;
    color: var(--text-muted);
}

.widget__foot > [data-tone="warn"] {
    color: var(--q-amber);
}

/* --------------------------------------------------------- the signed-in user */

.widget__who {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}

.widget__whoname {
    font-size: 16px;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
}

.widget__whorole {
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* ------------------------------------------------------------ processing runs */

.widget__runs {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.widget__run {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}

/* Which tenant a run is in, above its card and only while the widget is watching more than one.
   On a single-tenant widget the heading already says it and the line would be on every card. */
.widget__runtenant {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* ------------------------------------------------------------- notifications */

/* In a card the list has the whole card to grow into, rather than the height a popover may hang
   over the page. It still scrolls, because a card that grows with the feed would push everything
   arranged below it down the canvas every time a notification arrived. */
.notifications--card .menu__scroll {
    max-height: 420px;
}

.notifications--card .menu__heading {
    padding-left: 0;
    padding-right: 0;
}

/* ------------------------------------------------------------- latest assets */

/* A row is one control rather than a grid of cells with a button in it: every row on this card
   leads somewhere, and there is nothing on one to select. */

.latestassets__rows {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.latestassets__open {
    display: grid;
    grid-template-columns: 18px minmax(0, 1fr) auto auto;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 6px 8px;
    border: 0;
    border-radius: 9px;
    background: transparent;
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.latestassets__open:hover,
.latestassets__open[aria-expanded="true"] {
    background: var(--surface-sunken);
}

.latestassets__open:focus-visible {
    outline: 0;
    box-shadow: var(--focus-ring);
}

.latestassets__glyph {
    display: flex;
    color: var(--text-muted);
}

/* The name is the row, so it takes what is left and gives up first — a clip's name runs to a
   hundred characters and the size and the age are the two things that must not be pushed out. */
.latestassets__name {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: 13px;
}

.latestassets__cell {
    flex: none;
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: .04em;
    color: var(--text-muted);
    white-space: nowrap;
}

/* The pager sits at the far end of the foot, opposite what page it is. */
.latestassets__foot {
    align-items: center;
    justify-content: space-between;
}

.latestassets__pager {
    display: flex;
    gap: 4px;
}

/* On a narrow card the size goes before the age does: how long ago something arrived is what this
   card is sorted by, and the size is on the asset itself a click away. */
@container (max-width: 360px) {
    .latestassets__open {
        grid-template-columns: 18px minmax(0, 1fr) auto;
    }

    .latestassets__cell--size {
        display: none;
    }
}

/* ----------------------------------------------------------- the channel logo */

/* The picture is scaled to whatever the card was dragged to and keeps its own proportions, which
   is the whole of the widget: object-fit does that for the two logos, and the Quatro90 mark that
   stands in for them is inline svg, where a viewBox and no preserveAspectRatio already mean the
   same thing. */

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

.channellogo__frame {
    /* One cell, and everything in it stacked: the mark sits under the picture rather than beside
       it, so which of them shows is a matter of hiding one. */
    display: grid;
    grid-template: 1fr / 1fr;
    place-items: center;
    /* What a card nobody has resized gives the picture. A card given an explicit height overrides
       it below, because there the picture's room is whatever the head leaves. */
    height: 140px;
}

.channellogo__frame > * {
    grid-area: 1 / 1;
    min-width: 0;
    min-height: 0;
}

.cardgrid--free > .cardgrid__item[style*="height"] .channellogo__frame {
    flex: 1;
    height: auto;
    /* Without this the frame cannot shrink past the picture's own height — a flex item's implicit
       min-height is its content — and a card dragged shorter than its logo is square would grow a
       scrollbar instead of scaling the picture down. */
    min-height: 0;
}

.channellogo__image {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* The mark stands in for a picture rather than sitting behind one: a logo with transparency in it
   would otherwise have the Quatro90 card showing through. It comes back when every picture on the
   card has marked itself broken, which is what makes "no logo" true of an address that will not
   load as well as of an empty slot. */
.channellogo__image:not([data-broken]) ~ .q90logo {
    display: none;
}

.channellogo__image[data-broken] {
    display: none;
}

.channellogo .q90logo {
    width: 100%;
    height: 100%;
}

/* An unpinned card carries both background logos and the stylesheet picks, because whether the page
   is dark is only known here: data-theme is absent for "follow the system". The guard is the pair
   the palette itself uses — see the top of this file. */
.channellogo__image[data-theme-fit="dark"] {
    display: none;
}

:root[data-theme="dark"] .channellogo__image[data-theme-fit="light"] {
    display: none;
}

:root[data-theme="dark"] .channellogo__image[data-theme-fit="dark"] {
    display: block;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .channellogo__image[data-theme-fit="light"] {
        display: none;
    }

    :root:not([data-theme="light"]) .channellogo__image[data-theme-fit="dark"] {
        display: block;
    }
}

/* ------------------------------------------------------------------ the clock */

/* The faces are dark in every theme, for the reason the monitor rail is: a clock on a gallery wall
   is dark, and a light one beside a picture monitor is a lamp pointed at the operator. The card
   around it stays the surface every other widget is, so it is an instrument set into the dashboard
   rather than a hole in it. */

.clock__host {
    display: block;
}

.clock__digital,
.clock__timecode,
.clock__analogwrap {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 18px 14px;
    background: var(--rail-bg);
    border: 1px solid var(--rail-line);
    border-radius: 12px;
}

/* Sized against the card rather than the window: these are dragged to whatever width their owner
   wants them, and a clock that ignored that would be a clock with one right size. */
.clock__reading {
    font-family: var(--font-mono);
    font-size: clamp(24px, 13cqw, 54px);
    font-weight: 500;
    line-height: 1.05;
    font-variant-numeric: tabular-nums;
    letter-spacing: .01em;
    color: var(--rail-text);
    text-shadow: 0 0 26px rgb(116 94 255 / 45%);
}

/* The timecode carries eleven characters against the digital face's eight, so it is set smaller to
   hold the same width. */
.clock__reading--tc {
    font-size: clamp(18px, 9.4cqw, 40px);
}

.clock__frames {
    color: var(--q-violet-light);
}

.clock__date {
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--rail-muted);
}

.clock__analog {
    display: block;
    width: min(100%, 208px);
    height: auto;
}

.clock__dial {
    fill: var(--rail-panel);
    stroke: var(--rail-line);
    stroke-width: 1.4;
}

.clock__tick {
    stroke: var(--rail-muted);
    stroke-width: 1;
    stroke-linecap: round;
}

.clock__tick--hour {
    stroke: var(--rail-text);
    stroke-width: 2.2;
}

.clock__hand {
    stroke: var(--rail-text);
    stroke-linecap: round;
}

.clock__hand--hours {
    stroke-width: 4.6;
}

.clock__hand--minutes {
    stroke-width: 3.2;
}

/* The one moving part that is meant to be noticed: a sweeping hand is how you tell at a glance
   that the clock is running rather than stopped on the last frame it was painted. */
.clock__hand--seconds {
    stroke: var(--q-lime);
    stroke-width: 1.6;
}

.clock__cap {
    fill: var(--q-lime);
}

.clock__under {
    font-family: var(--font-mono);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    letter-spacing: .06em;
    color: var(--rail-muted);
}

/* ------------------------------------------------------------- the live video */

.player {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* The picture and whatever has to sit on top of it. */
.player__frame {
    position: relative;
    display: flex;
}

/* What a card that does not start itself offers instead of starting itself. Over the whole picture
   rather than in a corner, because there is nothing behind it to see yet — the stream has not been
   fetched, which is the point of turning autoplay off. */
.player__start {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 12px;
    background: color-mix(in srgb, #000 55%, transparent);
    color: #fff;
    cursor: pointer;
}

.player__start:hover {
    background: color-mix(in srgb, #000 40%, transparent);
}

.player__start::before {
    content: "";
    position: absolute;
    width: 58px;
    height: 58px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--q-violet) 85%, #000);
}

.player__start > * {
    position: relative;
}

/* Black rather than the rail's near-black: everything around a picture is a reference for what is
   in it, and letterbox bars that are lighter than the programme's own blacks make a correctly
   graded picture look washed out. */
.player__video {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    background: #000;
    border: 1px solid var(--rail-line);
    border-radius: 12px;
    object-fit: contain;
}

.player__problem {
    margin: 0;
    font-size: 12.5px;
    color: var(--text-muted);
}

/* ============================================================ named, shareable dashboards ==== */

/* The dashboard picker, in /home's header rather than in the arrange bar: switching board is
   something you do to read one, so it must be reachable without saying you want to rearrange
   anything first. The header becomes a row for it, and wraps rather than squeezing the title. */
.page__header--picked {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
}

.dashpicker__button {
    display: flex;
    align-items: center;
    gap: 9px;
    max-width: min(420px, 100%);
    padding: 8px 12px;
    border: 1px solid var(--border);
    border-radius: 11px;
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: 13.5px;
    cursor: pointer;
}

.dashpicker__button:hover {
    border-color: color-mix(in srgb, var(--q-violet) 40%, var(--border));
}

.dashpicker__name {
    font-family: var(--font-display);
    font-weight: 700;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Which team is sharing the board on screen. Second, and quieter, because the board's own name is
   what somebody is looking for and the owner is what disambiguates two boards with one name. */
.dashpicker__group {
    flex: none;
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.dashpicker__menu {
    right: 0;
    min-width: 280px;
}

/* The board already showing. A tint rather than a tick: the rows carry an icon already, and a
   second glyph in the same row reads as a second kind of thing rather than as a state. */
.dashpicker__menu .menu__item[data-on="true"] {
    background: color-mix(in srgb, var(--q-violet) 12%, transparent);
    font-weight: 700;
}

.menu__divider {
    height: 1px;
    margin: 6px 4px;
    background: var(--border);
}

/* ---------------------------------------------------------------- the /dashboards list */

.dashboards {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 10px;
}

.dashboards__item {
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface-sunken);
    overflow: hidden;
}

.dashboards__row {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    padding: 10px 14px;
}

/* The name is the link that opens the board, because opening it is what a row is for. */
.dashboards__name {
    flex: 1;
    min-width: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 13.5px;
    color: var(--text);
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.dashboards__name:hover {
    text-decoration: underline;
}

.dashboards__tags {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}

/* Create, rename and copy all take a name and one other thing, so they are one row rather than a
   panel: the field labels carry what each is, and the buttons say which act it is. */
.dashboards__form {
    display: flex;
    align-items: flex-end;
    gap: 10px;
    flex-wrap: wrap;
    padding: 12px 14px;
    border-top: 1px solid var(--border);
}

.dashboards__form .field {
    flex: 1;
    min-width: 160px;
    margin-bottom: 0;
}

/* Scoped rather than flagged: .dashboards__form .field already sets flex, so this has to out-rank
   it on specificity and not on !important. */
.dashboards__form .field--narrow {
    flex: 0 0 124px;
    min-width: 124px;
}

.dashboards__check {
    display: flex;
    align-items: center;
    gap: 7px;
    padding-bottom: 9px;
    font-size: 12.5px;
    color: var(--text-muted);
    white-space: nowrap;
    cursor: pointer;
}

/* How much of the answer this is. A list that stopped at the cap reads as a complete one, which is
   the reason to say so — same rule as the processing widget's tenant count. */
.dashboards__capped {
    margin: 0 0 12px;
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: .04em;
    color: var(--text-muted);
}

/* ------------------------------------------------------------------ context menu

   The menu a right-click on a content tree row opens. Fixed rather than absolute because it is
   placed from the pointer's window coordinates, and an absolutely positioned menu would be measured
   against whichever pane happens to be its offset parent.

   Above the modal layer (z-index 60) because the menu and the catcher have to sit above every other
   layer on the page, and that layer is the highest of them. Not so a menu can be opened over a
   dialog — the modal's own surface covers the window, so the right-click never reaches a row while
   one is up. It is the ordering that matters, not that case. */
.ctxmenu__catch {
    position: fixed;
    inset: 0;
    z-index: 70;
}

.ctxmenu {
    position: fixed;
    z-index: 71;
    /* Placed by AssetsContextMenu.razor.js once it has been measured. Off-screen until then, so the
       first paint does not show it at the top-left corner before it is moved. */
    left: -9999px;
    top: -9999px;
    display: flex;
    flex-direction: column;
    /* One width, whatever is on the menu. It used to be sized by the title at the top, so it was as
       wide as whatever had been right-clicked; without that the entries alone would size it, and the
       submenu arrow — which sits at the right edge of its row, and so at the right edge of the menu —
       would move from one opening to the next. A menu whose arrow is somewhere new each time is one
       nobody builds a habit for. */
    min-width: 236px;
    max-width: 320px;
    max-height: min(70vh, 520px);
    overflow-y: auto;
    padding: 5px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface);
    box-shadow: 0 18px 44px rgb(0 0 0 / .34);
}

.ctxmenu:focus-visible {
    /* It takes focus so Escape reaches it, which is not a thing worth drawing a ring around. */
    outline: none;
}

.ctxmenu__item {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 7px 9px;
    border: 0;
    border-radius: 8px;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: .84rem;
    text-align: left;
    cursor: pointer;
}

.ctxmenu__item:hover:not(:disabled),
.ctxmenu__item:focus-visible {
    background: color-mix(in srgb, var(--q-violet) 15%, transparent);
}

.ctxmenu__item:disabled {
    color: var(--text-muted);
    cursor: default;
}

/* Destructive entries read as destructive before they are clicked, not after. */
.ctxmenu__item[data-tone="danger"] {
    color: var(--q-red);
}

.ctxmenu__item[data-tone="danger"]:hover:not(:disabled),
.ctxmenu__item[data-tone="danger"]:focus-visible {
    background: color-mix(in srgb, var(--q-red) 14%, transparent);
}

/* The shortcut an entry answers to, at the far end of its row.

   Pushed over with an auto margin rather than by growing the label, so it lands in the same place on
   an entry that is a glyph and a label as on one that carries a tick column in front of it.

   Hidden from anything reading the page aloud: the entry states the same thing properly in
   aria-keyshortcuts, which is the attribute a screen reader already knows how to announce, and having
   both would read every entry's key twice. */
.ctxmenu__keys {
    flex: none;
    margin-left: auto;
    padding-left: 14px;
    color: var(--text-muted);
    font-size: .72rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.ctxmenu__sep {
    height: 1px;
    margin: 4px 6px;
    background: var(--border);
}

/* An entry that opens a panel of its own. Positioned so the panel can hang off it, and not clipped:
   the menu scrolls its own overflow, and a panel that opened sideways inside a scroll container would
   be cut off at the edge it opened towards. */
.ctxmenu__nest {
    position: relative;
}

/* The row inside that wrapper has to be told to fill it. Every other entry is a direct child of the
   menu — a column flex container, which stretches its children — but this one sits inside the wrapper
   that the panel hangs off, and a button whose width is auto shrinks to its own text however wide the
   box around it is. Left to itself it came out half the width of the menu, which put its arrow in the
   middle of the row: the one place on a menu where an arrow says nothing about which way the panel
   opens. */
.ctxmenu__nest > .ctxmenu__item {
    width: 100%;
}

.ctxmenu__nestname {
    flex: 1 1 auto;
}

.ctxmenu__nestmore {
    flex: none;
    color: var(--text-muted);
    font-size: .9rem;
}

/* The panel.

   Fixed, and placed from window coordinates handed over by flank(), because the menu scrolls its own
   overflow — and a scroll container clips its children on both axes. Positioned absolutely at
   left:100% it was cut off at the very edge it opened towards, and put a horizontal scrollbar on the
   menu for its trouble. Fixed escapes the clip; the price is that the coordinates cannot be expressed
   here as a percentage of the parent. */
.ctxmenu__sub {
    display: none;
    position: fixed;
    left: var(--at-left, 0);
    top: var(--at-top, 0);
    z-index: 72;
    flex-direction: column;
    min-width: 176px;
    max-width: 280px;
    max-height: min(60vh, 420px);
    overflow-y: auto;
    padding: 5px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface);
    box-shadow: 0 18px 44px rgb(0 0 0 / .34);
}

.ctxmenu__nest[data-open="true"] > .ctxmenu__sub {
    display: flex;
}

/* The row stays lit while its panel is open, so it is obvious which one the panel belongs to. */
.ctxmenu__nest[data-open="true"] > .ctxmenu__item {
    background: color-mix(in srgb, var(--q-violet) 15%, transparent);
}

/* A heading over a run of related entries. */
.ctxmenu__group {
    padding: 7px 9px 3px;
    color: var(--text-muted);
    font-size: .68rem;
    text-transform: uppercase;
    letter-spacing: .05em;
}

/* The tick column. Always present, empty or not, so the labels beside it line up whether anything is
   ticked or not — labels that shift sideways as the selection changes are much harder to read. */
.ctxmenu__tick {
    flex: none;
    width: 15px;
    text-align: center;
    color: var(--q-violet);
    font-size: .78rem;
}

/* The status every marked row already has. Said with the tick; this only firms up the text, because a
   row of four statuses where one is bold reads faster than one where the difference is a glyph. */
.ctxmenu__item[data-on="true"] {
    color: var(--text);
    font-weight: 600;
}


/* ------------------------------------------------------------------ uploading into the tree

   The file picker and the per-file list in the content tree's upload dialog. */

/* The real input is hidden but present and clickable through its label: the script reads the picked
   files straight off it, so it cannot be swapped for a button that only looks like one. */
.uploadpick {
    display: block;
    cursor: pointer;
}

.uploadpick__input {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.uploadpick__face {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    padding: 20px;
    border: 1px dashed var(--border);
    border-radius: 12px;
    background: color-mix(in srgb, var(--q-violet) 5%, transparent);
    color: var(--text);
    font-size: .88rem;
}

.uploadpick:hover .uploadpick__face,
.uploadpick__input:focus-visible + .uploadpick__face {
    border-color: color-mix(in srgb, var(--q-violet) 60%, transparent);
    background: color-mix(in srgb, var(--q-violet) 11%, transparent);
}

.uploadlist {
    display: flex;
    flex-direction: column;
    gap: 4px;
    max-height: min(46vh, 340px);
    overflow-y: auto;
    padding: 0;
    margin: 0;
    list-style: none;
}

/* Positioned so the progress fill can sit behind the text rather than beside it. */
.uploadlist__row {
    position: relative;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 7px 10px;
    border-radius: 8px;
    background: var(--surface-sunken);
    font-size: .82rem;
}

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

.uploadlist__state {
    flex: none;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* Behind the row's own text, so a filling bar never pushes a filename around. */
.uploadlist__fill {
    position: absolute;
    inset: 0 auto 0 0;
    z-index: 0;
    width: var(--done, 0%);
    border-radius: 8px;
    background: color-mix(in srgb, var(--q-violet) 16%, transparent);
    transition: width 200ms var(--step-cubic);
    pointer-events: none;
}

.uploadlist__name,
.uploadlist__state {
    position: relative;
    z-index: 1;
}

.uploadlist__row[data-state="done"] .uploadlist__state {
    color: var(--q-lime);
}

.uploadlist__row[data-state="failed"] .uploadlist__state {
    color: var(--q-red);
}

.uploadlist__row[data-state="failed"] .uploadlist__fill {
    background: color-mix(in srgb, var(--q-red) 13%, transparent);
}

/* --------------------------------------------------------------------------
   Toasts
   -------------------------------------------------------------------------- */

/* The stack in the top right. Above the modal layer and the context menu, because something the
   application has to say does not stop mattering because a dialog is open; below the busy sweep at
   90, which is a two-pixel line and belongs over everything.

   The stack takes no pointer events, so the empty strip beside a short toast does not swallow
   clicks meant for the page underneath. Each toast takes them back for itself. */
.toasts {
    position: fixed;
    top: 60px;
    right: 16px;
    z-index: 80;
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: min(380px, calc(100vw - 32px));
    pointer-events: none;
}

.toast {
    pointer-events: auto;
    display: flex;
    gap: 11px;
    align-items: flex-start;
    padding: 12px 14px;
    border-radius: var(--radius-field);
    border: 1px solid transparent;
    background: var(--surface);
    box-shadow: var(--shadow-card);
    font-size: 13px;
    animation: toast-in 220ms var(--step-cubic) both;
}

/* Set by the service one render before the row goes, so the browser has something to run. The
   220ms here and the wait in Toasts.Exit are the same number in two places and have to agree. */
.toast[data-leaving="true"] {
    animation: toast-out 220ms var(--step-cubic) forwards;
}

/* Holding a toast stops its clock, which happens on the server. This only says that it is held. */
.toast:hover,
.toast:focus-within {
    border-color: var(--border-strong);
}

.toast__icon {
    flex: none;
    width: 18px;
    height: 18px;
    margin-top: 1px;
}

/* Wraps rather than clips: a failure from upstream can be a sentence, and the half that got cut
   is usually the half that named the thing. */
.toast__text {
    flex: 1;
    min-width: 0;
    overflow-wrap: anywhere;
}

.toast__count {
    flex: none;
    align-self: center;
    font-family: var(--font-mono);
    font-size: 11px;
    opacity: .7;
}

.toast__close {
    flex: none;
    border: 0;
    background: none;
    font-size: 17px;
    line-height: 1;
    color: inherit;
    opacity: .6;
    cursor: pointer;
}

.toast__close:hover {
    opacity: 1;
}

.toast[data-tone="error"] {
    background: color-mix(in srgb, var(--q-red) 10%, var(--surface));
    border-color: color-mix(in srgb, var(--q-red) 34%, transparent);
    color: color-mix(in srgb, var(--q-red) 78%, var(--text));
}

.toast[data-tone="warn"] {
    background: color-mix(in srgb, var(--q-amber) 12%, var(--surface));
    border-color: color-mix(in srgb, var(--q-amber) 38%, transparent);
    color: color-mix(in srgb, var(--q-amber) 58%, var(--text));
}

.toast[data-tone="success"] {
    background: color-mix(in srgb, var(--q-lime) 14%, var(--surface));
    border-color: color-mix(in srgb, var(--q-lime) 40%, transparent);
    color: color-mix(in srgb, var(--q-lime) 52%, var(--text));
}

.toast[data-tone="info"] {
    background: color-mix(in srgb, var(--q-violet) 10%, var(--surface));
    border-color: color-mix(in srgb, var(--q-violet) 30%, transparent);
    color: color-mix(in srgb, var(--q-violet) 62%, var(--text));
}

/* Off the right edge and back, far enough out to clear its own shadow. */
@keyframes toast-in {
    from {
        opacity: 0;
        transform: translateX(calc(100% + 24px));
    }

    to {
        opacity: 1;
        transform: translateX(0);
    }
}

@keyframes toast-out {
    from {
        opacity: 1;
        transform: translateX(0);
    }

    to {
        opacity: 0;
        transform: translateX(calc(100% + 24px));
    }
}

@keyframes toast-fade-in {
    from { opacity: 0 }
    to { opacity: 1 }
}

@keyframes toast-fade-out {
    from { opacity: 1 }
    to { opacity: 0 }
}

/* Somebody who has asked for less movement still has to see it arrive and see it go, so the
   travel goes and the fade stays. Shorter than the server waits, which only means the row sits
   invisible for the remainder. */
@media (prefers-reduced-motion: reduce) {
    .toast {
        animation: toast-fade-in 160ms linear both;
    }

    .toast[data-leaving="true"] {
        animation: toast-fade-out 160ms linear forwards;
    }
}

/* The tone the inline notice never defined. Five places ask for it and have been drawing a
   borderless box since the day the first one did. */
.notice[data-tone="warn"] {
    background: color-mix(in srgb, var(--q-amber) 12%, var(--surface));
    border-color: color-mix(in srgb, var(--q-amber) 38%, transparent);
    color: color-mix(in srgb, var(--q-amber) 58%, var(--text));
}

/* This session's warnings and failures, above the platform's feed in the bell. Scrolls on its own
   rather than sharing the feed's scroller: they are two lists and one of them is capped at fifty, so
   a busy session must not push the feed out of reach. */
.notifications__session {
    max-height: 40dvh;
    overflow-y: auto;
    padding: 0 6px 4px;
}

/* Which of the two lists you are looking at. Said once above each rather than on every row. */
.notification__group {
    margin: 6px 4px 2px;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Where the feed's rows carry an unread dot, these carry their tone: the fact worth knowing about
   one of these is whether it was a warning or a failure. */
.notification__tone {
    flex: none;
    width: 16px;
    height: 16px;
    margin-top: 3px;
}

/* The tone is the fact worth seeing at a glance, so it is the icon's colour and not a word. */
.notifications__session .notification[data-tone="error"] .notification__tone {
    color: var(--q-red);
}

.notifications__session .notification[data-tone="warn"] .notification__tone {
    color: var(--q-amber);
}

/* ==================================================================== field controls, part two
 *
 * The controls added alongside the first set: the ones that collect a value in a new shape, and the
 * ones that collect nothing at all.
 *
 * A presentation control has no label cell — there is nothing to name — so its row spans the form
 * rather than sitting in the value column with an empty label beside it. That is data-full, set from
 * ControlCatalogue rather than guessed per control.
 */

.fieldrow[data-full="true"] {
    grid-template-columns: 1fr;
}

/* Kept out of the flow but not out of the accessibility tree. What the icon modifier leaves behind
 * when it takes a caption's place: the field is still announced and still findable by name. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ------------------------------------------------------------------ choice */

.fieldchecks {
    display: flex;
    flex-direction: column;
    gap: .3rem;
}

.fieldtags {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem;
    align-items: center;
}

.chip--tag {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
}

.chip__x {
    border: 0;
    background: none;
    padding: 0 .1rem;
    font-size: 1rem;
    line-height: 1;
    color: inherit;
    cursor: pointer;
}

.chip__x:hover {
    color: var(--q-red);
}

.input--tag {
    flex: 1 1 8rem;
    min-width: 8rem;
}

/* ------------------------------------------------------------------ numbers */

.fieldunit {
    display: flex;
    align-items: center;
    gap: .4rem;
}

.fieldunit__suffix {
    font-size: .8rem;
    color: var(--text-muted);
    white-space: nowrap;
}

/* Capped rather than filling the cell. The control column is as wide as the pane, and a slider
 * stretched across all of it puts its readout somewhere nobody is looking — and a caption after it
 * off the edge entirely. */
.fieldslider {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: .75rem;
    max-width: 28rem;
}

.fieldslider fluent-slider {
    flex: 1;
    min-width: 6rem;
}

/* Tabular figures, so the readout does not change width as the thumb moves and shift the track
 * under the cursor. */
.fieldslider__out {
    min-width: 4ch;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    font-size: .85rem;
    color: var(--text-soft);
}

/* ------------------------------------------------------------------ timecode */

.fieldspan--tc {
    align-items: flex-end;
}

.fieldspan__rate {
    font-size: .72rem;
    color: var(--text-muted);
    white-space: nowrap;
    padding-bottom: .35rem;
}

/* ------------------------------------------------------------------ colour */

.fieldcolour {
    display: flex;
    align-items: center;
    gap: .5rem;
}

.fieldcolour__well {
    width: 2.4rem;
    height: 2rem;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-field);
    background: none;
    cursor: pointer;
}

.input--hex {
    max-width: 9rem;
    font-family: var(--font-mono);
}

.fieldswatches {
    display: flex;
    flex-wrap: wrap;
    gap: .4rem;
}

.swatch {
    width: 1.6rem;
    height: 1.6rem;
    border-radius: var(--radius-pill);
    border: 1px solid var(--border-strong);
    background: var(--swatch);
    cursor: pointer;
}

.swatch[aria-pressed="true"] {
    box-shadow: var(--focus-ring);
}

/* What is stored but is not on the palette any more. Dashed, so it reads as an exception rather
 * than as one more choice somebody made. */
.swatch--stray {
    border-style: dashed;
    border-color: var(--q-amber);
}

/* ------------------------------------------------------------------ rich text */

/* Not overflow:hidden, however much the rounded corners want it. Quill's link editor is positioned
 * against the container and sits outside it by design; clipping the wrapper cut it in half. The
 * corners are rounded on the toolbar and the editor instead. */
.richtext {
    position: relative;
    border: 1px solid var(--border);
    border-radius: var(--radius-field);
    background: var(--field);
}

.richtext[data-disabled="true"] {
    opacity: .6;
    pointer-events: none;
}

/* Quill's own chrome, pulled onto this application's tokens. Without this the editor arrives with
 * its own borders, its own radius and a white background that ignores the dark theme entirely. */
.richtext .ql-toolbar.ql-snow,
.richtext .ql-container.ql-snow {
    border: 0;
    font-family: var(--font-body);
}

.richtext .ql-toolbar.ql-snow {
    border-bottom: 1px solid var(--border);
    background: var(--surface-sunken);
    border-radius: var(--radius-field) var(--radius-field) 0 0;
}

.richtext .ql-container.ql-snow {
    border-radius: 0 0 var(--radius-field) var(--radius-field);
}

/* The link editor. Themed, because it arrives white-on-white in a dark application, and lifted
 * above the fields under it. */
.richtext .ql-snow .ql-tooltip {
    z-index: 30;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-field);
    background: var(--surface);
    box-shadow: var(--shadow-card);
    color: var(--text);
}

.richtext .ql-snow .ql-tooltip input[type="text"] {
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--field);
    color: var(--text);
    padding: .25rem .4rem;
}

.richtext .ql-snow .ql-tooltip a {
    color: var(--q-violet);
}

.richtext .ql-snow .ql-tooltip::before {
    color: var(--text-muted);
}

/* Quill's own colour and size menus, same treatment. */
.richtext .ql-snow .ql-picker-options {
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-field);
    background: var(--surface);
    box-shadow: var(--shadow-card);
    z-index: 30;
}

.richtext .ql-snow .ql-picker-item {
    color: var(--text-soft);
}

.richtext .ql-editor {
    min-height: 9rem;
    font-size: .9rem;
    line-height: 1.6;
    color: var(--text);
}

.richtext .ql-editor.ql-blank::before {
    color: var(--text-muted);
    font-style: normal;
}

.richtext .ql-snow .ql-stroke {
    stroke: var(--text-soft);
}

.richtext .ql-snow .ql-fill {
    fill: var(--text-soft);
}

.richtext .ql-snow .ql-picker-label {
    color: var(--text-soft);
}

.richtext .ql-snow.ql-toolbar button:hover .ql-stroke,
.richtext .ql-snow.ql-toolbar button.ql-active .ql-stroke {
    stroke: var(--q-violet);
}

.richtext .ql-snow.ql-toolbar button:hover .ql-fill,
.richtext .ql-snow.ql-toolbar button.ql-active .ql-fill {
    fill: var(--q-violet);
}

.richtext .ql-editor img {
    max-width: 100%;
    height: auto;
    border-radius: 6px;
}

/* ------------------------------------------------------------------ presentation: structure */

.fielddisplay__spacer {
    display: block;
}

.fielddisplay__rule {
    border: 0;
    border-top: 1px solid var(--border);
    margin: .75rem 0;
}

/* A named rule: the caption, then the line filling whatever is left. Upper-case and tracked out,
 * so it reads as a divider rather than as a heading competing with the group's own. */
.fielddisplay__rule--named {
    display: flex;
    align-items: center;
    gap: .6rem;
    border: 0;
    margin: 1rem 0 .5rem;
    font-size: .7rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.fielddisplay__rule--named::after {
    content: "";
    flex: 1;
    border-top: 1px solid var(--border);
}

.fielddisplay__heading {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin: 1.1rem 0 .35rem;
    font-family: var(--font-display);
    font-size: 1rem;
    font-weight: 650;
    color: var(--text);
}

.fielddisplay__link {
    font-size: .88rem;
    color: var(--q-violet);
}

/* An address this application will not open — see FieldLinks. Drawn as text rather than dropped, so
 * a misconfigured content type is visible instead of being a line that silently is not there. */
.fielddisplay__deadlink {
    font-size: .88rem;
    color: var(--text-muted);
    text-decoration: line-through;
    text-decoration-color: var(--q-red);
    cursor: help;
}

/* ------------------------------------------------------------------ presentation: infotext
 *
 * Five looks rather than four. Hand-rolled for exactly that reason: an intent vocabulary of four
 * would put Alert and Critical on the same colour, and then two of the five would be the same
 * control with different words in it.
 */

.fieldinfo {
    display: flex;
    gap: .6rem;
    align-items: flex-start;
    padding: .6rem .75rem;
    margin: .35rem 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-field);
    background: var(--surface-sunken);
    color: var(--text);
}

.fieldinfo__body {
    margin: 0;
    font-size: .85rem;
    line-height: 1.5;
}

.fieldinfo[data-severity="info"] {
    border-color: color-mix(in srgb, var(--q-violet) 40%, transparent);
    background: color-mix(in srgb, var(--q-violet) 8%, var(--surface));
    color: var(--text);
}

.fieldinfo[data-severity="warning"] {
    border-color: color-mix(in srgb, var(--q-amber) 55%, transparent);
    background: color-mix(in srgb, var(--q-amber) 12%, var(--surface));
    color: var(--text);
}

.fieldinfo[data-severity="alert"] {
    border-width: 2px;
    border-color: var(--q-amber);
    background: color-mix(in srgb, var(--q-amber) 22%, var(--surface));
    color: var(--text);
}

.fieldinfo[data-severity="critical"] {
    border-color: var(--q-red);
    background: var(--q-red);
    color: #fff;
}

.fieldinfo[data-severity="danger"] {
    border-color: var(--q-red);
    background: var(--q-red);
    color: #fff;
    animation: fieldinfo-pulse 1.1s var(--step-cubic) infinite;
}

/* A pulse, not a flash.
 *
 * Nothing here goes near three transitions per second: that threshold is a photosensitive-seizure
 * risk rather than a matter of taste, and this control exists to be the loudest thing on a form
 * somebody works in all day. The text never moves and never changes contrast — only the surface
 * behind it and the halo around it. */
@keyframes fieldinfo-pulse {
    0%, 100% {
        background: var(--q-red);
        box-shadow: 0 0 0 0 color-mix(in srgb, var(--q-red) 45%, transparent);
    }

    50% {
        background: color-mix(in srgb, var(--q-red) 82%, white);
        box-shadow: 0 0 0 6px color-mix(in srgb, var(--q-red) 0%, transparent);
    }
}

/* Off entirely for anybody who has asked for less motion. Danger keeps the heaviest static
 * treatment of the five and is still unmistakable. */
@media (prefers-reduced-motion: reduce) {
    .fieldinfo[data-severity="danger"] {
        animation: none;
        border-width: 2px;
        outline: 2px solid color-mix(in srgb, var(--q-red) 50%, transparent);
        outline-offset: 2px;
    }
}

/* ------------------------------------------------------------------ presentation: markdown */

.fieldmarkdown {
    font-size: .88rem;
    line-height: 1.6;
    color: var(--text);
}

.fieldmarkdown > :first-child {
    margin-top: 0;
}

.fieldmarkdown > :last-child {
    margin-bottom: 0;
}

.fieldmarkdown h1,
.fieldmarkdown h2,
.fieldmarkdown h3,
.fieldmarkdown h4 {
    font-family: var(--font-display);
    font-size: .95rem;
    margin: .9rem 0 .3rem;
}

.fieldmarkdown code {
    font-family: var(--font-mono);
    font-size: .85em;
    background: var(--surface-sunken);
    padding: .1em .3em;
    border-radius: 4px;
}

.fieldmarkdown pre {
    background: var(--surface-sunken);
    padding: .6rem .75rem;
    border-radius: 6px;
    overflow-x: auto;
}

.fieldmarkdown pre code {
    background: none;
    padding: 0;
}

.fieldmarkdown blockquote {
    margin: .5rem 0;
    padding-left: .75rem;
    border-left: 3px solid var(--border-strong);
    color: var(--text-soft);
}

.fieldmarkdown table {
    border-collapse: collapse;
    font-size: .85rem;
}

.fieldmarkdown th,
.fieldmarkdown td {
    border: 1px solid var(--border);
    padding: .25rem .5rem;
    text-align: left;
}

.fieldmarkdown img {
    max-width: 100%;
    height: auto;
    border-radius: 6px;
}

/* ------------------------------------------------------------------ presentation: media */

.fieldmedia__img,
.fieldmedia__player {
    max-width: 100%;
    height: auto;
    border-radius: var(--radius-field);
    display: block;
}

.fieldmedia__player {
    background: #000;
}

.fieldmedia__audio {
    width: 100%;
    max-width: 28rem;
}

/* What a content type pointing at a deleted asset draws. A gap that says so beats a broken image
 * icon, and beats drawing nothing at all — which would look like the field was never configured. */
.fieldmedia__gap {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 5rem;
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-field);
    color: var(--text-muted);
    font-size: .8rem;
}

/* ------------------------------------------------------------------ presentation: teaching tip */

.teachtip {
    position: relative;
    display: inline-block;
}

.teachtip__button {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .25rem .55rem;
    border: 1px solid color-mix(in srgb, var(--q-violet) 35%, transparent);
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--q-violet) 8%, var(--surface));
    color: var(--q-violet-deep);
    font-size: .78rem;
    font-weight: 600;
    cursor: pointer;
}

.teachtip__button:hover {
    background: color-mix(in srgb, var(--q-violet) 15%, var(--surface));
}

.teachtip__cue {
    max-width: 14rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.teachtip__panel {
    position: absolute;
    z-index: 40;
    top: calc(100% + .4rem);
    left: 0;
    width: min(22rem, 80vw);
    padding: .7rem .8rem;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-field);
    background: var(--surface);
    box-shadow: var(--shadow-card);
}

.teachtip__panel:focus-visible {
    box-shadow: var(--shadow-card), var(--focus-ring);
}

.teachtip__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: .5rem;
    margin-bottom: .4rem;
    font-family: var(--font-display);
    font-size: .88rem;
}

.teachtip__close {
    border: 0;
    background: none;
    font-size: 1.1rem;
    line-height: 1;
    color: var(--text-muted);
    cursor: pointer;
}

.teachtip__img {
    display: block;
    width: 100%;
    height: auto;
    border-radius: 6px;
    margin-bottom: .5rem;
}

.teachtip__body {
    font-size: .82rem;
    line-height: 1.55;
    color: var(--text-soft);
}

.teachtip__body > :first-child { margin-top: 0; }
.teachtip__body > :last-child { margin-bottom: 0; }

/* ------------------------------------------------------------------ presentation: cross-field */

.fieldprogress {
    display: flex;
    align-items: center;
    gap: .6rem;
    max-width: 28rem;
}

.fieldprogress fluent-progress {
    flex: 1;
    min-width: 5rem;
}

.fieldprogress__out {
    font-variant-numeric: tabular-nums;
    font-size: .8rem;
    min-width: 4ch;
    color: var(--text-soft);
}

.fieldprogress__label,
.fieldmirror {
    font-size: .85rem;
    color: var(--text-soft);
}

.fieldcountdown {
    font-size: .85rem;
    font-variant-numeric: tabular-nums;
    color: var(--text-soft);
}

.fieldcountdown[data-past="true"] {
    color: var(--q-red);
    font-weight: 600;
}

/* ------------------------------------------------------------------ the ContentType editor */

.propnote {
    margin: .35rem 0 0;
    font-size: .78rem;
    line-height: 1.45;
    color: var(--text-muted);
}

.propnote--warn {
    color: color-mix(in srgb, var(--q-amber) 70%, var(--text));
}

.cmsvalue__severity[data-severity="critical"],
.cmsvalue__severity[data-severity="danger"] {
    border-color: var(--q-red);
}

.cmsvalue__severity[data-severity="warning"],
.cmsvalue__severity[data-severity="alert"] {
    border-color: var(--q-amber);
}

/* ------------------------------------------------------------------ dark theme
 *
 * Only the pieces that mix against a light surface by name. Everything above that uses
 * color-mix against var(--surface) follows the theme without help.
 */

:root[data-theme="dark"] .fieldmedia__player,
:root[data-theme="dark"] .richtext .ql-editor img {
    background: #000;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .fieldinfo[data-severity="alert"] {
        color: var(--text);
    }
}

/* --------------------------------------------------------------------- the rating
 *
 * Buttons, not radios. A rating means "this many", so the fourth star fills the first four — which
 * is a statement about the group and reads badly as five independent radios.
 */

.fieldrating {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: .1rem;
}

.fieldrating__star {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: .15rem;
    border: 0;
    border-radius: 6px;
    background: none;
    cursor: pointer;
    color: var(--border-strong);
    transition: color .12s var(--step-cubic), transform .12s var(--step-cubic);
}

.fieldrating__star[data-on="true"] {
    color: var(--q-amber);
}

/* Hovering fills what a click would set, so the target is obvious before committing to it. Applied
 * to the hovered star and everything before it — :has is what makes "before it" expressible. */
.fieldrating:hover .fieldrating__star:hover,
.fieldrating__star:hover ~ .fieldrating__star[data-on="true"] {
    color: var(--q-amber);
}

.fieldrating__star:hover {
    transform: scale(1.12);
}

.fieldrating__star:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.fieldrating__star:disabled {
    cursor: default;
    transform: none;
}

.fieldrating__out {
    margin-left: .5rem;
    white-space: nowrap;
    font-size: .8rem;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

/* --------------------------------------------------------------------- the user selector
 *
 * Chosen people above, a box to find more below, and the list only while the box has focus. A tenant
 * of two hundred is not something anybody scrolls, so the search is the way in and the list is
 * capped with a line saying how many are behind it.
 */

.fielduser {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: .4rem;
    max-width: 34rem;
}

.fielduser__chosen {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem;
}

.fielduser__chip {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .2rem .45rem .2rem .25rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    font-size: .82rem;
}

.fielduser__face {
    width: 1.35rem;
    height: 1.35rem;
    font-size: .6rem;
    flex: 0 0 auto;
}

.fielduser__name {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 14rem;
}

/* Stored, but not somebody this control can see — left the tenant, or outside the team the field
 * names. Shown rather than dropped, and marked so it does not read as an ordinary choice. */
.fielduser__name--unknown {
    font-family: var(--font-mono);
    font-size: .74rem;
    color: var(--text-muted);
    cursor: help;
}

.fielduser__list {
    position: absolute;
    z-index: 30;
    top: 100%;
    left: 0;
    right: 0;
    max-height: 16rem;
    overflow-y: auto;
    margin-top: .25rem;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-field);
    background: var(--surface);
    box-shadow: var(--shadow-card);
}

.fielduser__option {
    display: flex;
    align-items: center;
    gap: .5rem;
    width: 100%;
    padding: .35rem .5rem;
    border: 0;
    background: none;
    text-align: left;
    color: var(--text);
    font-size: .84rem;
    cursor: pointer;
}

.fielduser__option:hover,
.fielduser__option:focus-visible {
    background: var(--bg-accent);
    outline: none;
}

.fielduser__option[aria-selected="true"] {
    color: var(--text-muted);
}

.fielduser__who {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: .72rem;
    color: var(--text-muted);
}

.fielduser__note {
    margin: 0;
    padding: .45rem .6rem;
    font-size: .78rem;
    color: var(--text-muted);
}

.fielduser__note--bad {
    color: var(--q-red);
}

/* The rich text editor's font families.
 *
 * Quill emits ql-font-<name> for a whitelisted family and nothing for the default, so each one
 * needs declaring here or the picker changes a class and no pixels. The names in the menu are set
 * through content, because Quill's own labels are the raw keys. */

.richtext .ql-editor .ql-font-serif,
.richtext .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"] {
    font-family: 'Georgia', 'Times New Roman', serif;
}

.richtext .ql-editor .ql-font-mono,
.richtext .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="mono"] {
    font-family: var(--font-mono);
}

.richtext .ql-snow .ql-picker.ql-font {
    width: 6.5rem;
}

.richtext .ql-snow .ql-picker.ql-font .ql-picker-label::before,
.richtext .ql-snow .ql-picker.ql-font .ql-picker-item::before {
    content: 'Body';
}

.richtext .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
.richtext .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
    content: 'Serif';
}

.richtext .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="mono"]::before,
.richtext .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="mono"]::before {
    content: 'Mono';
}

/* ==========================================================================
   The MAM handbook — /help/mam

   Sixteen documents rendered from markdown, so this is the one place in the
   application drawing long-form prose. Everything below is scoped to
   .helpdoc__prose rather than set on the tags themselves: the same tags come
   out of a rich text field, and a manual's reading measure is not what a
   field wants.
   ========================================================================== */
/* No align-items here on purpose. A sticky grid item slides within its own
   grid area, so "start" — which shrinks that area to the nav's own height —
   leaves it nothing to slide in and the contents scroll away with the page.
   Stretched, the area is as tall as the document beside it and the max-height
   on .helpnav is what keeps the box itself a sensible size. */
.helppage__body {
    display: grid;
    grid-template-columns: 232px minmax(0, 1fr);
    gap: 28px;
}

/* The grid item, which carries no styling of its own. It stretches to the row,
   and that is the whole of its job: it is the block the card sticks inside. */
.helpnav {
    min-width: 0;
}

/* The contents, beside the document rather than above it. Sticky, because the
   longest of these is a long scroll and losing the way out of it would mean
   scrolling back up to find one. */
.helpnav__card {
    position: sticky;
    top: 12px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 14px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-card);
    max-height: calc(100dvh - 120px);
    overflow-y: auto;
}

.helpnav__group {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin: 12px 0 4px 6px;
}

.helpnav__group:first-child {
    margin-top: 0;
}

.helpnav__list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.helpnav__item {
    display: grid;
    grid-template-columns: 22px minmax(0, 1fr);
    align-items: baseline;
    gap: 4px;
    padding: 5px 6px;
    border-radius: 8px;
    color: var(--text-soft);
    text-decoration: none;
    font-size: 13px;
    line-height: 1.35;
}

.helpnav__item:hover {
    background: var(--surface-sunken);
    color: var(--text);
}

/* The page being read. aria-current carries it, so what is drawn and what a
   screen reader announces cannot drift apart. */
.helpnav__item[aria-current="page"] {
    background: color-mix(in srgb, var(--q-violet) 14%, transparent);
    color: var(--text);
    font-weight: 700;
}

.helpnav__num {
    font-family: var(--font-mono);
    font-size: 10px;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.helpnav__item[aria-current="page"] .helpnav__num {
    color: var(--q-violet);
}

.helpnav__back {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-top: 14px;
    padding-top: 12px;
    border-top: 1px solid var(--border);
    color: var(--text-muted);
    text-decoration: none;
    font-size: 12px;
}

.helpnav__back:hover {
    color: var(--text);
}

.helpdoc {
    min-width: 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-card);
    padding: 26px 30px 20px;
}

/* ------------------------------------------------------------------ prose */
.helpdoc__prose {
    color: var(--text-soft);
    font-size: 14px;
    line-height: 1.7;
    max-width: 74ch;
}

.helpdoc__prose > :first-child {
    margin-top: 0;
}

.helpdoc__prose h1,
.helpdoc__prose h2,
.helpdoc__prose h3,
.helpdoc__prose h4 {
    font-family: var(--font-display);
    color: var(--text);
    letter-spacing: -.02em;
    text-wrap: balance;
    /* Room above the anchor a cross-reference lands on, so a jumped-to heading
       does not sit against the top of the pane. */
    scroll-margin-top: 16px;
}

.helpdoc__prose h1 {
    font-size: 24px;
    font-weight: 800;
    margin: 0 0 14px;
}

.helpdoc__prose h2 {
    font-size: 17px;
    font-weight: 800;
    margin: 30px 0 10px;
}

.helpdoc__prose h3 {
    font-size: 14px;
    font-weight: 800;
    margin: 22px 0 8px;
}

.helpdoc__prose h4 {
    font-size: 13px;
    font-weight: 700;
    color: var(--text-soft);
    margin: 18px 0 6px;
}

.helpdoc__prose p {
    margin: 0 0 12px;
}

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

.helpdoc__prose a {
    color: var(--q-violet);
    text-decoration: none;
    border-bottom: 1px solid color-mix(in srgb, var(--q-violet) 40%, transparent);
}

.helpdoc__prose a:hover {
    border-bottom-color: var(--q-violet);
}

.helpdoc__prose ul,
.helpdoc__prose ol {
    margin: 0 0 12px;
    padding-left: 20px;
}

.helpdoc__prose li {
    margin: 0 0 4px;
}

.helpdoc__prose li::marker {
    color: var(--text-muted);
}

.helpdoc__prose hr {
    border: 0;
    border-top: 1px solid var(--border);
    margin: 26px 0 18px;
}

.helpdoc__prose code {
    font-family: var(--font-mono);
    font-size: 12px;
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 1px 5px;
    color: var(--text);
    overflow-wrap: break-word;
}

.helpdoc__prose pre {
    margin: 0 0 14px;
    padding: 12px 14px;
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    border-radius: var(--radius-field);
    /* The diagram in the model chapter is wider than the measure, so it gets
       its own scroller and the pane never scrolls sideways. */
    overflow-x: auto;
}

.helpdoc__prose pre code {
    background: none;
    border: 0;
    padding: 0;
    font-size: 11px;
    line-height: 1.6;
    color: var(--text-soft);
    white-space: pre;
}

.helpdoc__prose blockquote {
    margin: 0 0 14px;
    padding: 12px 14px;
    background: color-mix(in srgb, var(--q-violet) 8%, var(--surface));
    border-left: 2px solid var(--q-violet);
    border-radius: 0 var(--radius-field) var(--radius-field) 0;
}

.helpdoc__prose blockquote > :last-child {
    margin-bottom: 0;
}

.helpdoc__prose blockquote p {
    color: var(--text);
}

/* Tables are half of the reference chapters and several are wider than the
   measure. Made a block so the table itself is the scroller — the renderer
   wraps it in nothing this stylesheet could hang one on. */
.helpdoc__prose table {
    display: block;
    width: max-content;
    max-width: 100%;
    overflow-x: auto;
    border-collapse: collapse;
    margin: 0 0 16px;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
}

.helpdoc__prose th,
.helpdoc__prose td {
    text-align: left;
    vertical-align: top;
    padding: 6px 10px;
    border-bottom: 1px solid var(--border);
}

.helpdoc__prose th {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-muted);
    white-space: nowrap;
    background: var(--surface-sunken);
}

.helpdoc__prose td {
    color: var(--text-soft);
}

.helpdoc__prose tbody tr:last-child td {
    border-bottom: 0;
}

/* A screenshot. Ninety-odd of them, framed the way a card is — the pictures
   are of this application, and an unframed one reads as part of the page
   rather than as a picture of one. */
.helpdoc__prose img {
    display: block;
    width: 100%;
    height: auto;
    margin: 14px 0 6px;
    border: 1px solid var(--border);
    border-radius: var(--radius-field);
    background: var(--surface-sunken);
    box-shadow: var(--shadow-card);
}

/* ------------------------------------------------------------------- ends */
.helpdoc__ends {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    margin-top: 26px;
    padding-top: 16px;
    border-top: 1px solid var(--border);
}

.helpdoc__end {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 8px 12px;
    border: 1px solid var(--border);
    border-radius: var(--radius-field);
    text-decoration: none;
    max-width: 46%;
}

.helpdoc__end:hover {
    border-color: var(--border-strong);
    background: var(--surface-sunken);
}

.helpdoc__end--next {
    margin-left: auto;
    text-align: right;
}

.helpdoc__endway {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.helpdoc__endname {
    font-weight: 700;
    color: var(--text);
    font-size: 13px;
}

/* The handbook link and the advanced switch, grouped so the MAM strip keeps
   its two ends rather than spreading three things across it. */
.mamstrip__tools {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
}

/* The handbook link wears the button styling, so it should not also wear the
   underline an anchor gets by default. */
.mamstrip__tools .button {
    text-decoration: none;
    gap: 6px;
}

@media (max-width: 900px) {
    .helppage__body {
        grid-template-columns: minmax(0, 1fr);
    }

    .helpnav__card {
        position: static;
        max-height: none;
    }

    .helpdoc {
        padding: 18px 16px 14px;
    }
}
