/*
    M7a: the token layer and the post page.

    Tokens are derived from design/frontend (the Figma bundle), not copied from it. Two rules
    govern every number below, and both are recorded in design/tokens.md:

      1. The design's root is 16px; ours is 17px. Every rem figure taken from the design is
         converted, never copied. Source pixels are noted in comments where it matters.
      2. Where the design contradicted itself, one value was chosen and the choice is recorded
         in design/tokens.md §10. Nothing was normalised silently.

    Page chrome (header, nav, footer, index pages) is still the structural CSS from M0 and is
    restyled in M7b. It is repointed at the new tokens here so it keeps working, nothing more.
*/

/* Fonts ------------------------------------------------------------------------------------- */

/*
    Self-hosted because the CSP allows font-src 'self' only, and because the design's Google Fonts
    import is a third-party dependency of exactly the kind RendererVersion v3 already removed once.
    Subset to Latin + Cyrillic by scripts/build-fonts.sh; both scripts ship in one file per face
    because a Russian-language page with code samples in it needs both on the same request.
*/

@font-face {
    font-family: "Inter";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/fonts/Inter-Regular.woff2") format("woff2");
}

@font-face {
    font-family: "Inter";
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url("/fonts/Inter-Medium.woff2") format("woff2");
}

@font-face {
    font-family: "JetBrains Mono";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/fonts/JetBrainsMono-Regular.woff2") format("woff2");
}

@font-face {
    font-family: "JetBrains Mono";
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url("/fonts/JetBrainsMono-Medium.woff2") format("woff2");
}

/* Tokens ------------------------------------------------------------------------------------ */

:root {
    /*
       The content column. This is not only a design value: the renderer bakes a matching
       "sizes" attribute into every stored page (PrecomputedImageRenderer.ContentColumnSizes),
       so changing this changes what those pages should say. Doing so needs a RendererVersion
       bump and a full rerender, not just a stylesheet edit.

       The design does not overrule this. Its only running-prose container is 42rem, and that is
       a paragraph inside a hero on a page that never drew an article body. See design/tokens.md §3.1.
    */
    --measure: 68ch;
    --gap: 1.5rem;

    /* Families. The design had no font token at all — see design/tokens.md §7.8. */
    --font-sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --font-mono: "JetBrains Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;

    /*
       Type scale, converted from the design's 16px root to ours at 17px.
       Source: theme.css @layer base — 40 / 32 / 24 / 18 px, and text-sm / text-xs at 14 / 12 px.
    */
    --text-h1: 2.353rem;
    --text-h2: 1.882rem;
    --text-h3: 1.412rem;
    --text-h4: 1.059rem;
    --text-small: 0.824rem;
    --text-tiny: 0.706rem;

    /* Spacing, converted from the design's 4px step. 4 / 8 / 12 / 16 / 24 / 32 / 48 px. */
    --space-1: 0.235rem;
    --space-2: 0.471rem;
    --space-3: 0.706rem;
    --space-4: 0.941rem;
    --space-6: 1.412rem;
    --space-8: 1.882rem;
    --space-12: 2.824rem;

    /*
       One radius scale. The design shipped `rounded` (4px) and `rounded-md` (4px) as two names
       for one value, and used `rounded-lg` (6px) for the same cards — design/tokens.md §7.6.
       Radii are absolute: they should not grow with the reader's text size.
    */
    --radius-sm: 4px;
    --radius: 6px;

    /* Light. Derived, not drawn — the design's light mode was never rendered. See §10.2. */
    --text: #0d0d0d;
    --text-muted: #5f5f5f;
    --background: #f5f5f5;
    --surface: #ffffff;
    --border: #d8d8d8;
    --accent: #006a6a;
    --danger: #a3161b;

    --transition: 300ms;
}

/*
    Dark is the designed theme and is taken from the design as drawn, with one change: the greys
    are one family. The design mixed true neutrals (#0d0d0d) with blue-tinted Tailwind greys
    (#111827, #1f2937, #e5e7eb, #9ca3af), so cards read bluer than the page. Each blue-tinted
    value is replaced by the true neutral of the same lightness. See design/tokens.md §10.1.
*/
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --text: #e7e7e7;
        --text-muted: #a2a2a2;
        --background: #0d0d0d;
        --surface: #181818;
        --border: #282828;
        --accent: #00ffff;
        --danger: #ff6b6b;
    }
}

:root[data-theme="dark"] {
    --text: #e7e7e7;
    --text-muted: #a2a2a2;
    --background: #0d0d0d;
    --surface: #181818;
    --border: #282828;
    --accent: #00ffff;
    --danger: #ff6b6b;
}

@media (prefers-reduced-motion: reduce) {
    :root {
        --transition: 1ms;
    }
}

/* Base -------------------------------------------------------------------------------------- */

html {
    font-family: var(--font-sans);

    /*
       17px up to 1280, 19px from 2560, straight line between. The reading measure is fixed in
       characters, so the column grows with the type rather than the line getting longer: 68ch is
       729px at 17px and 815px at 19px, and a reader on a 4K display gets a column sized for the
       screen they are actually using without anybody reading a 110-character line.

       rem for the fixed parts rather than px, so a reader who has set a larger default still gets
       it. The floor is the old fixed value, so nothing below 1280 changes at all.
    */
    font-size: clamp(1.0625rem, 0.9375rem + 0.15625vw, 1.1875rem);
    line-height: 1.65;
    color: var(--text);
    background: var(--background);
    -webkit-text-size-adjust: 100%;

    /*
       The standard property, which Firefox and Chromium both honour. The design styled
       ::-webkit-scrollbar only, so Firefox got nothing.
    */
    scrollbar-color: var(--border) transparent;
}

/*
    The design set selected text to the accent colour on a 20% wash of the same accent, which is
    close to unreadable. Same idea, but the text keeps its own colour.
*/
::selection {
    background: color-mix(in srgb, var(--accent) 25%, transparent);
    color: var(--text);
}

body {
    display: flex;
    flex-direction: column;
    margin: 0;
    min-height: 100vh;
}

.wrap {
    margin: 0 auto;

    /* --page-max is set on <body> by the index-page rule further down; prose pages never set it. */
    max-width: var(--page-max, var(--measure));
    padding: 0 1rem;
    width: 100%;
    box-sizing: border-box;
}

main {
    flex: 1 0 auto;
    padding-block: 2rem 4rem;
}

/* Links */

a {
    color: var(--accent);
    text-underline-offset: 0.15em;
    transition: color var(--transition);
}

a:hover {
    text-decoration-thickness: 2px;
}

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

.skip-link {
    position: absolute;
    left: -9999px;
    background: var(--background);
    padding: 0.5rem 1rem;
    z-index: 10;
}

.skip-link:focus {
    left: 1rem;
    top: 1rem;
}

/* Header and footer */

.site-header {
    border-bottom: 1px solid var(--border);
    padding-block: 1rem;
}

.site-header__inner {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.75rem var(--gap);
}

.site-title {
    font-family: var(--font-mono);
    font-weight: 500;
    text-decoration: none;
    margin-right: auto;
}

.site-nav {
    display: flex;
    gap: 1rem;
    flex-wrap: wrap;
}

.site-footer {
    border-top: 1px solid var(--border);
    color: var(--text-muted);
    font-size: var(--text-small);
    padding-block: 1rem;
}

.theme-toggle {
    display: inline;
}

.theme-toggle__button {
    background: none;
    border: 0;
    color: var(--accent);
    cursor: pointer;
    font: inherit;
    font-size: var(--text-small);
    padding: 0;
    text-decoration: underline;
}

/* Only the button that changes something is shown, so the control is never a no-op. */
:root[data-theme="dark"] .theme-toggle__button--dark,
:root:not([data-theme="dark"]) .theme-toggle__button--light {
    display: none;
}

/* Typography -------------------------------------------------------------------------------- */

/*
    Headings are monospace at weight 500. That is the design's clearest and most consistent
    decision — it puts JetBrains Mono on every heading, wordmark, chip and label, and Inter only
    on running prose. It survives into the article body unchanged.
*/
h1, h2, h3, h4, h5, h6 {
    font-family: var(--font-mono);
    font-weight: 500;
    line-height: 1.25;
    margin-block: 2rem 0.5rem;
    text-wrap: balance;
}

h1 {
    font-size: var(--text-h1);
    letter-spacing: -0.02em;
    margin-top: 0;
}

h2 { font-size: var(--text-h2); }
h3 { font-size: var(--text-h3); }
h4 { font-size: var(--text-h4); }
h5, h6 { font-size: 1rem; }

p, ul, ol, dl, table, pre, blockquote, figure {
    margin-block: 1rem;
}

picture {
    display: block;
}

img {
    max-width: 100%;
    height: auto;
}

hr {
    border: 0;
    border-top: 1px solid var(--border);
    margin-block: var(--space-12);
}

/* The post page ----------------------------------------------------------------------------- */

/*
    Designed here, not translated. The Figma bundle specifies nothing for article bodies — no
    prose container, no code block, no table, quote, list or in-flow image (design/tokens.md §9).
    What follows applies the design's own language — monospace headings and labels, borders rather
    than shadows, a single cyan accent — to a reading surface it never drew.
*/

.post > header {
    border-bottom: 1px solid var(--border);
    margin-bottom: var(--space-8);
    padding-bottom: var(--space-6);
}

.post > header h1 {
    margin-bottom: var(--space-3);
}

/*
    Body text carries full foreground contrast. The nearest thing the design has to body copy is
    a card excerpt in --muted-foreground; inheriting that would set every article in the muted
    colour. Muted is for metadata only.
*/
.post-body {
    color: var(--text);
}

/*
    Nothing in running prose may leave the column, whatever it is. A 400-character percent-encoded
    URL is the case that produced this rule — pasted as text rather than as a link, it is one word
    as far as the browser is concerned, and one word wider than the viewport pushes the whole page
    sideways on a phone.

    `anywhere` rather than `break-word`: only `anywhere` lets the token shrink the element's
    intrinsic minimum width, which is what stops a grid or flex column being sized by it. Code
    blocks are excluded below — they scroll instead, because breaking a line of code changes what
    it says.
*/
.post-body,
.page__body,
.project__body,
.entry__summary,
.callout,
.card__excerpt,
th,
td {
    overflow-wrap: anywhere;
}

.post-body pre,
.page__body pre,
.project__body pre {
    overflow-wrap: normal;
}

.post-body > :first-child {
    margin-top: 0;
}

/* Headings inside the article get more air above than below, so sections read as grouped. */
.post-body h2 {
    border-top: 1px solid var(--border);
    margin-top: var(--space-12);
    padding-top: var(--space-6);
}

.post-body h3,
.post-body h4 {
    margin-top: var(--space-8);
}

.heading-anchor {
    color: var(--accent);
    margin-left: 0.4em;
    opacity: 0;
    text-decoration: none;
    transition: opacity var(--transition);
}

h2:hover .heading-anchor,
h3:hover .heading-anchor,
h4:hover .heading-anchor,
.heading-anchor:focus-visible {
    opacity: 1;
}

/* Metadata: monospace and muted, the design's treatment for every label and timestamp. */
.post-meta,
.entry__meta {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--text-small);
}

/* Lists */

.post-body ul,
.post-body ol {
    padding-left: 1.5rem;
}

.post-body li + li {
    margin-top: var(--space-2);
}

.post-body li::marker {
    color: var(--accent);
}

/*
    The design marks its one list with an accent-coloured ▸ (About.tsx). Borrowed here for
    unordered lists; the glyph is in the subset. Browsers without ::marker content fall back to
    a disc, which is why the colour is set separately above.
*/
.post-body ul > li::marker {
    content: "▸  ";
}

.post-body ol {
    list-style: decimal;
}

/* Blockquote */

.post-body blockquote {
    border-left: 3px solid var(--accent);
    margin-inline: 0;
    padding: var(--space-2) 0 var(--space-2) var(--space-6);
}

/* Quoted text stays at reading contrast; the accent rule is what marks it, not a dimmer grey. */
.post-body blockquote > :first-child { margin-top: 0; }
.post-body blockquote > :last-child { margin-bottom: 0; }

/* Images */

.post-body picture,
.post-body figure {
    margin-block: var(--space-8);
}

.post-body img {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    display: block;
}

.post-body figcaption {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--text-small);
    margin-top: var(--space-3);
    text-align: center;
}

/* Code -------------------------------------------------------------------------------------- */

code {
    font-family: var(--font-mono);
    font-size: 0.9em;
}

/* Inline code is tinted rather than boxed, so it does not fight the line it sits in. */
:not(pre) > code {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0.1em 0.35em;
}

pre {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow-x: auto;
    padding: var(--space-4) var(--space-6);
    position: relative;
}

/*
    The language, top right, from the attribute the renderer already writes. Absent when the fence
    had no language, because the attribute is then absent too — no empty chip on a plain block.

    Deliberately quiet: muted, tiny and lowercased, because it is a note about the block rather
    than part of it, and a code sample should not have to compete with its own label.
*/
pre[data-language]::after {
    color: var(--text-muted);
    content: attr(data-language);
    font-size: var(--text-tiny);
    line-height: 1;
    position: absolute;
    right: var(--space-3);
    text-transform: lowercase;
    top: var(--space-3);
}

/*
    Room for the label, so it never lands on the first line of code. Only when there is a label:
    a block with neither attribute keeps its ordinary padding.
*/
pre[data-language],
pre[data-filename] {
    padding-top: var(--space-8);
}

/*
    Both labels share one row, so a long path is cut with an ellipsis before it reaches the
    language rather than running underneath it.

    Both are positioned against the block, which is also the scroll container, so on a very wide
    sample they travel with the code when it is scrolled. Accepted: the alternative is a wrapper
    element around every code block, which would mean changing stored HTML and re-rendering the
    site to move a label.
*/
pre[data-filename][data-language]::before {
    max-width: calc(100% - var(--space-12) - var(--space-8));
}

/*
    Ligatures off in code. JetBrains Mono turns ->, => and != into single glyphs by default, which
    is a preference, not a default worth imposing on someone reading a code sample.
*/
pre code {
    font-variant-ligatures: none;
}

/*
    The copy button and the wrapper copy.js builds around each block.

    The wrapper exists for one reason: pre is the horizontal scroll container, so anything
    positioned inside it travels with the code and a button on a wide sample scrolls out of reach.
    The labels above accept exactly that and it costs them nothing, because a label that has
    scrolled away has already been read. A button that has scrolled away cannot be pressed.

    None of this is in the stored HTML. The wrapper and the button are built in the browser, so
    there is no rendered markup to change and no re-render — and with scripting off none of these
    rules match anything.
*/
.code-block {
    position: relative;
}

/*
    Room for the button on every block that has one, including a fence with no language, which
    otherwise has no reserved band at the top and would take the button across its first line.
*/
.code-block > pre {
    padding-top: var(--space-8);
}

.code-block__copy {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    cursor: pointer;
    font-family: var(--font-mono);
    font-size: var(--text-tiny);
    line-height: 1;
    padding: var(--space-1) var(--space-2);
    position: absolute;
    right: var(--space-3);
    top: var(--space-3);

    /* Above the block, which is the scroll container and paints its own background. */
    z-index: 1;
}

.code-block__copy:hover {
    border-color: var(--accent);
    color: var(--text);
}

/*
    The language label shares the top row with the button, so it moves left by the button's width
    and a gap. Only inside a wrapper: a block that never got a button keeps the label where it was.
*/
.code-block > pre[data-language]::after {
    right: calc(var(--space-3) + 6rem);
}

/*
    The outcome is announced rather than shown twice: the button's own text changes, and a change
    of text is not announced on its own.
*/
.code-block__status {
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}

/*
    The renderer records the source file name on the block. Shown as a label so a sample that
    names a file says so on the page, not only in the markup.
*/
pre[data-filename]::before {
    color: var(--text-muted);
    content: attr(data-filename);
    font-size: var(--text-tiny);
    left: var(--space-6);
    line-height: 1;
    overflow: hidden;
    position: absolute;
    text-overflow: ellipsis;
    top: var(--space-3);
    white-space: nowrap;
}

/*
    Shiki writes both themes as custom properties, so switching does not re-render anything.
    Only the token colours come from the theme — the container uses our surface, so a code block
    matches the callouts and panels around it. The theme pair itself is untouched: changing it
    would mean a RendererVersion bump.
*/
.shiki,
.shiki span {
    color: var(--shiki-light);
}

:root[data-theme="dark"] .shiki,
:root[data-theme="dark"] .shiki span {
    color: var(--shiki-dark);
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .shiki,
    :root:not([data-theme="light"]) .shiki span {
        color: var(--shiki-dark);
    }
}

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

table {
    border-collapse: collapse;
    display: block;
    overflow-x: auto;
    width: 100%;
}

th, td {
    border: 1px solid var(--border);
    padding: var(--space-2) var(--space-3);
    text-align: left;
}

th {
    background: var(--surface);
    font-family: var(--font-mono);
    font-size: var(--text-small);
    font-weight: 500;
}

/* Content blocks ---------------------------------------------------------------------------- */

.callout {
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: 0 var(--radius) var(--radius) 0;
    padding: var(--space-2) var(--space-6);
}

.callout--warning {
    border-left-color: var(--danger);
}

.youtube-facade__link {
    display: block;
    position: relative;
}

/* Page furniture ---------------------------------------------------------------------------- */

.entry {
    border-bottom: 1px solid var(--border);
    padding-block: 1.25rem;
}

.entry:last-of-type {
    border-bottom: 0;
}

.entry__title {
    font-size: var(--text-h3);
    margin-block: 0 0.25rem;
}

.tag-list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    list-style: none;
    padding: 0;
}

/*
    Tags are the design's chip: monospace, accent-tinted fill, accent hairline, small radius.
    colour-mix keeps the tint derived from the accent rather than hard-coded per theme.
*/
.tag-list a {
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: var(--text-small);
    padding: var(--space-1) var(--space-3);
    text-decoration: none;
    transition: background var(--transition), border-color var(--transition);
}

.tag-list a:hover {
    background: color-mix(in srgb, var(--accent) 18%, transparent);
    border-color: var(--accent);
}

.toc,
.series-nav {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-4) var(--space-6);
}

.toc__heading {
    font-size: var(--text-h4);
    margin-block: 0 var(--space-3);
}

.toc ol {
    margin-block: 0.25rem;
    padding-left: 1.25rem;
}

.preview-banner {
    background: color-mix(in srgb, var(--accent) 12%, transparent);
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    font-family: var(--font-mono);
    font-size: var(--text-small);
    padding: var(--space-3) var(--space-4);
}

.pagination {
    display: flex;
    gap: 1rem;
    justify-content: space-between;
    margin-top: 2rem;
}

/* Index pages -------------------------------------------------------------------------------- */

/*
    Two column widths on one layout, decided by what the page contains rather than by threading a
    parameter through MainLayout and fourteen pages. An index holds a grid; an article holds prose.

    :has() is the only way to let a child widen its ancestor without JavaScript. Set on <body> and
    read through a custom property, so the header, the content and the footer all reach the same
    edge — a wide grid under a narrow masthead reads as a mistake.

    If :has() ever fails, --page-max stays unset and every page falls back to the reading measure:
    narrower than intended, never broken, which is the right direction for a fallback to fail in.
*/
body:has(main.wrap .card-grid),
body:has(main.wrap .tag-index) {
    --page-max: 72rem;
}

/* Wide screens ------------------------------------------------------------------------------- */

/*
    Above 100rem the article is a strip in an empty field — 729px of text down the middle of a
    2560px display, with everything else blank. The fix is not a wider measure: --measure is baked
    into the "sizes" attribute of every stored page, so moving it costs a RendererVersion bump and a
    full rerender. What is free is using the margins the measure creates.

    So the table of contents leaves the top of the article and goes into the left margin, where it
    sticks; and figures and code blocks bleed a little way past the prose on both sides. The prose
    column itself is not touched, and neither is the line length.
*/
@media (min-width: 100rem) {
    :root {
        /*
           The bleed stays smaller than the gap. A figure that bled the full gap would touch the
           table of contents, and one that bled further would sit on top of it.
        */
        --toc-width: 14rem;
        --toc-gap: 3.5rem;
        --prose-bleed: 2.5rem;
    }

    /*
        Read on <body> so the header and the footer reach the same edges as the article, the way
        the index pages already do. The third track is empty on purpose: it balances the sidebar,
        so the prose stays in the middle of the display instead of sitting right of centre.
    */
    body:has(.post > .toc) {
        /*
           The 1rem is .wrap's own horizontal padding, counted on both sides. Without it the grid
           is asked for more than the container has and the middle track — the only flexible one —
           gives up the difference, leaving the prose two rem short of the measure.
        */
        --page-max: calc(var(--measure) + 2 * (var(--toc-width) + var(--toc-gap) + 1rem));
    }

    .post:has(> .toc) {
        column-gap: var(--toc-gap);
        display: grid;
        grid-template-columns: var(--toc-width) minmax(0, var(--measure)) var(--toc-width);
        justify-content: center;
    }

    /*
        Everything but the contents stacks in the middle track and is left to flow into rows by
        itself, so a post with no series navigation simply has one row fewer.
    */
    .post:has(> .toc) > * {
        grid-column: 2;
    }

    .post:has(> .toc) > .toc {
        align-self: start;
        grid-column: 1;

        /*
            Three rows, which is one per thing that can be in the middle track. The span is what
            gives the sticky element room to travel: an item occupying only its own row has a
            containing block the height of that row and stops moving immediately.
        */
        grid-row: 1 / 4;
        max-height: calc(100vh - 2 * var(--toc-gap));
        overflow-y: auto;
        position: sticky;
        top: var(--toc-gap);

        /* Margin furniture rather than a card: the border and fill were doing the work of
           separating it from the prose it sat above, and it no longer sits above anything. */
        background: none;
        border: 0;
        border-left: 1px solid var(--border);
        border-radius: 0;
        padding: 0 0 0 var(--space-4);
    }

    .post:has(> .toc) > .toc a {
        color: var(--text-muted);
    }

    .post:has(> .toc) > .toc a:hover {
        color: var(--text);
    }

    /*
        A diagram and a wide line of code both benefit from room the prose does not want. Applied
        whether or not there is a table of contents, because the margins exist either way.
    */
    .post-body > figure,
    .post-body > pre,

    /* copy.js wraps each block, so above this width the wrapper is what the bleed has to reach. */
    .post-body > .code-block {
        margin-inline: calc(-1 * var(--prose-bleed));
    }
}

/*
    Cards. Border and fill do the separating, the way the design does it throughout — 286 borders
    and not one elevation shadow. Hover moves the border to the accent rather than lifting the card,
    because a page of cards that all rise slightly is a page that flickers as the pointer crosses it.
*/
.card-grid {
    display: grid;
    gap: var(--space-6);
    grid-template-columns: 1fr;
    margin-block: var(--space-8);
}

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);

    /* Without this a long unbroken word in a title sets the column's width for the whole row. */
    min-width: 0;
    padding: var(--space-6);
    transition: border-color var(--transition);
}

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

/*
    The cover runs to the card's edges, so the negative margin cancels the card's padding on the
    three sides it touches.

    A fixed ratio rather than the image's own: a grid whose row heights are set by whatever shape
    happened to be uploaded reads as an accident, and object-fit takes the crop from the middle,
    which is where a cover's subject usually is. width and height still come from the library, so
    the box is reserved correctly before the bytes arrive and nothing reflows as covers load.
*/
.card__cover {
    aspect-ratio: 16 / 9;
    border-bottom: 1px solid var(--border);

    /* A pixel inside the card's own radius, so the corners sit within the border rather than
       cutting across it. */
    border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
    display: block;
    margin: calc(-1 * var(--space-6)) calc(-1 * var(--space-6)) 0;
    object-fit: cover;

    /* Undoing the global img rule, which caps every image at the width of its container and would
       otherwise clamp this one back inside the padding the negative margin just escaped. */
    max-width: none;
    width: calc(100% + 2 * var(--space-6));
}

.card__title {
    font-size: var(--text-h3);
    margin: 0;
    overflow-wrap: anywhere;
}

.card__title a {
    text-decoration: none;
}

.card__title a:hover {
    text-decoration: underline;
}

.card__meta {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--text-small);
    margin: 0;
}

.card__excerpt {
    margin: 0;
}

/* The card's own tag row sits at the bottom whatever the excerpt's length, so rows line up. */
.card .tag-list {
    margin-block: auto 0;
    padding-top: var(--space-2);
}

@media (min-width: 48rem) {
    .card-grid--comfortable {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .card-grid--compact {
        grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
    }
}

/* Compact drops the excerpt in the markup; here it tightens what is left. */
.card-grid--compact {
    gap: var(--space-4);
}

.card-grid--compact .card {
    gap: var(--space-2);
    padding: var(--space-4);
}

.card-grid--compact .card__title {
    font-size: var(--text-h4);
}

/* Projects carry more metadata than posts and less prose, so three across reads better. */
@media (min-width: 64rem) {
    .project-grid {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

/*
    The series page numbers its posts: a series is an argument in order, and the position is part
    of what the reader is being told.
*/
.series-index {
    counter-reset: series;
}

.series-index .card {
    counter-increment: series;
}

.series-index .card__title::before {
    color: var(--accent);
    content: counter(series) ". ";
    font-family: var(--font-mono);
}

/* The tag index: chips at a size you can hit, sorted into one wrapping row. */
.tag-index {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    list-style: none;
    margin-block: var(--space-8);
    padding: 0;
}

.tag-index__count {
    color: var(--text-muted);
    font-size: var(--text-small);
}

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

/* The home page's opening paragraph, set larger than body but below a heading. */
.intro {
    color: var(--text);
    font-size: var(--text-h4);
    max-width: var(--measure);
}

/* Pagination sits at the end of an index and needs to be reachable, not decorative. */
.pagination a {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: var(--text-small);
    padding: var(--space-2) var(--space-4);
    text-decoration: none;
    transition: border-color var(--transition);
}

.pagination a:hover {
    border-color: var(--accent);
}

/* Narrow pages ------------------------------------------------------------------------------- */

/*
    Sign-in, the two-factor step, recovery codes and the error pages. A form of three fields does
    not want a reading column: a narrow card keeps the eye on one thing, which on the 2FA step is
    the entire point of the screen.
*/
.narrow {
    margin-inline: auto;
    max-width: 26rem;
}

.narrow h1 {
    font-size: var(--text-h2);
}

.form-error {
    background: color-mix(in srgb, var(--danger) 10%, transparent);
    border: 1px solid var(--danger);
    border-radius: var(--radius);
    color: var(--text);
    padding: var(--space-3) var(--space-4);
}

/* Forms -------------------------------------------------------------------------------------- */

label {
    display: block;
    font-size: var(--text-small);
    font-weight: 500;
    margin-bottom: var(--space-2);
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="url"],
input[type="number"],
input[type="datetime-local"],
select,
textarea {
    background: var(--background);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-sizing: border-box;
    color: var(--text);
    font: inherit;
    padding: var(--space-2) var(--space-3);
    width: 100%;
}

textarea {
    font-family: var(--font-mono);
    font-size: var(--text-small);
    line-height: 1.55;
    resize: vertical;
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible {
    border-color: var(--accent);
}

button,
.button {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
    font: inherit;
    font-family: var(--font-mono);
    font-size: var(--text-small);
    padding: var(--space-2) var(--space-4);
    transition: border-color var(--transition), background var(--transition);
}

button:hover,
.button:hover {
    border-color: var(--accent);
}

button[type="submit"] {
    background: color-mix(in srgb, var(--accent) 12%, var(--surface));
    border-color: color-mix(in srgb, var(--accent) 35%, transparent);
}

button:disabled {
    cursor: not-allowed;
    opacity: 0.55;
}

button.danger {
    border-color: color-mix(in srgb, var(--danger) 40%, transparent);
    color: var(--danger);
}

button.danger:hover {
    border-color: var(--danger);
}

/* Admin -------------------------------------------------------------------------------------- */

/*
    The same tokens as the public site, deliberately denser. This is a tool used by one person who
    already knows what everything does, so it trades the reading surface's air for rows on screen.
    Not the design's /studio/dashboard sketch, which drew three tabs of the ten pages that exist.
*/

/*
    One container, one left edge. The band across the top spans the window, but what is inside it
    lines up with the content below, so every admin page agrees on where the page begins.
*/
.admin-shell {
    box-sizing: border-box;
    margin-inline: auto;
    max-width: 90rem;
    padding-inline: var(--space-4);
    width: 100%;
}

.admin-nav {
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    font-family: var(--font-mono);
    font-size: var(--text-small);
}

.admin-nav__inner {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    padding-block: var(--space-3);
}

.admin-nav__link {
    border: 1px solid transparent;
    border-bottom-width: 2px;
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    text-decoration: none;
    white-space: nowrap;
}

.admin-nav__link:hover {
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    border-color: color-mix(in srgb, var(--accent) 25%, transparent);
}

/*
    The current section, marked by weight and a bar rather than by colour, so it still reads for a
    reader who cannot tell the accent from the body colour. NavLink adds the class; Match decides
    what "current" means, and that lives in AdminLayout beside the links.
*/
.admin-nav__link.active {
    border-bottom-color: var(--accent);
    color: var(--text);
    font-weight: 500;
}

/*
    Trash, Profile and the link off to the site sit at the far end, away from the seven links used
    daily. Emptying the trash is not something to land on by aiming at "Медиа" and missing.
*/
.admin-nav__link--aside {
    margin-left: auto;
}

.admin-nav__link--quiet,
.admin-nav__link--aside {
    color: var(--text-muted);
}

.admin-nav__link--aside:hover {
    background: color-mix(in srgb, var(--danger) 10%, transparent);
    border-color: color-mix(in srgb, var(--danger) 35%, transparent);
    color: var(--danger);
}

.admin-nav__link--aside.active {
    border-bottom-color: var(--danger);
    color: var(--danger);
}

.admin-main {
    padding-block: var(--space-6) var(--space-12);
}

.admin-main h1 {
    font-size: var(--text-h2);
}

.admin-main h2 {
    font-size: var(--text-h3);
}

/* Admin tables ------------------------------------------------------------------------------- */

.admin-table {
    border-collapse: collapse;
    display: table;
    font-size: var(--text-small);
    width: 100%;
}

.admin-table th,
.admin-table td {
    border: 0;
    border-bottom: 1px solid var(--border);
    padding: var(--space-2) var(--space-3);
    vertical-align: top;
}

.admin-table th {
    background: transparent;
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--text-tiny);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    white-space: nowrap;
}

.admin-table tbody tr:hover {
    background: color-mix(in srgb, var(--accent) 6%, transparent);
}

/* Counts and dates are monospaced so they can be compared down the column. */
.admin-table .counters,
.admin-table .brief-moment {
    font-family: var(--font-mono);
    font-size: var(--text-tiny);
    white-space: nowrap;
}

/*
    A sortable heading is still a heading. It has to be a button so it can be reached and pressed
    from the keyboard, but the shared button treatment made four columns look like controls sitting
    in a row beside two that were plain text — which read as though half the table was interactive
    and the other half was broken.
*/
.admin-main .sort-link,
.admin-main .sort-link:hover {
    background: none;
    border: 0;
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--text-tiny);
    letter-spacing: 0.04em;
    padding: 0;
    text-align: inherit;
    text-transform: uppercase;
}

.admin-main .sort-link:hover {
    color: var(--accent);
}

.filters {
    align-items: end;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-bottom: var(--space-6);
}

.flag,
.brief-list__kind {
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: var(--text-tiny);
    padding: 0 var(--space-2);
    white-space: nowrap;
}

/* Media library ------------------------------------------------------------------------------ */

/*
    The reported symptom: with no styles at all an upload rendered at its own pixel width, so one
    photograph filled the viewport and the library was unusable. Fixed proportions and object-fit
    put the grid in charge of the layout instead of the images.
*/
.media-grid {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 13rem), 1fr));
    list-style: none;
    margin: 0;
    padding: 0;
}

.media-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    display: flex;
    flex-direction: column;
    min-width: 0;
    overflow: hidden;
}

.media-card img {
    aspect-ratio: 4 / 3;
    background: var(--background);
    border-bottom: 1px solid var(--border);
    display: block;
    height: auto;
    object-fit: cover;
    width: 100%;
}

.media-card__body {
    display: flex;
    flex-direction: column;
    font-size: var(--text-tiny);
    gap: var(--space-1);
    min-width: 0;
    padding: var(--space-3);
}

.media-card__name {
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.media-card__key,
.media-card__meta {
    color: var(--text-muted);
    font-family: var(--font-mono);

    /* A content-addressed key is one 32-character word; without this it sets the column width. */
    overflow-wrap: anywhere;
}

.media-card__usage-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.media-card__usage-trash {
    color: var(--text-muted);
}

.drop-zone {
    border: 1px dashed var(--border);
    border-radius: var(--radius);
    padding: var(--space-8) var(--space-4);
    text-align: center;
    transition: border-color var(--transition), background var(--transition);
}

.drop-zone:hover,
.drop-zone:focus-within {
    background: color-mix(in srgb, var(--accent) 6%, transparent);
    border-color: var(--accent);
}

.drop-zone__hint {
    color: var(--text-muted);
    font-size: var(--text-small);
}

/* The editor --------------------------------------------------------------------------------- */

/*
    Where the time goes, so it gets the room. Two panes side by side on a wide screen and stacked
    below 1024px: a tablet in portrait cannot give both panes a usable measure, and half a column
    of markdown beside half a column of prose is worse than either of them in full.
*/
.editor {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.editor-header {
    align-items: center;
    border-bottom: 1px solid var(--border);
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    padding-bottom: var(--space-3);
}

.editor-header h1 {
    font-size: var(--text-h3);
    margin: 0 auto 0 0;
}

.saved-at {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--text-tiny);
}

.unsaved {
    color: var(--danger);
    font-family: var(--font-mono);
    font-size: var(--text-tiny);
}

.editor-panes {
    display: grid;
    gap: var(--space-6);
    grid-template-columns: 1fr;
}

.editor-source,
.editor-preview {
    min-width: 0;
}

@media (min-width: 64rem) {
    .editor-panes {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    }

    /* Each pane scrolls on its own, so the source does not drag the preview past its heading. */
    .editor-source,
    .editor-preview {
        max-height: calc(100vh - 12rem);
        overflow-y: auto;
    }
}

.editor-preview {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-4) var(--space-6);
}

.editor-preview > h2 {
    color: var(--text-muted);
    font-size: var(--text-tiny);
    letter-spacing: 0.04em;
    margin-top: 0;
    text-transform: uppercase;
}

/*
    The preview reuses the published post's own rules rather than approximating them, which is the
    only way "what it will look like" means anything. Only the code surface differs: the pane is
    already on --surface, so a block takes the page background to stay distinguishable from it.
*/
.editor-preview .post-body pre,
.editor-preview .post-body :not(pre) > code {
    background: var(--background);
}

.editor-source textarea {
    min-height: 24rem;
}

.frontmatter {
    display: grid;
    gap: var(--space-3);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
    margin-bottom: var(--space-4);
}

.field-error {
    color: var(--danger);
    font-size: var(--text-tiny);
}

.field-hint {
    color: var(--text-muted);
    font-size: var(--text-tiny);
}

.field-ok {
    color: var(--accent);
    font-size: var(--text-tiny);
}

.stacked-form {
    /* start, not stretch: a submit button has no reason to be as wide as the textarea above it. */
    align-items: start;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    max-width: 34rem;
}

/* The textarea is the exception — it does want the column's full width. */
.stacked-form textarea,
.stacked-form .field {
    align-self: stretch;
}

.inline-form {
    align-items: end;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.tag-chips,
.tag-suggestions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    list-style: none;
    padding: 0;
}

.chip-remove {
    padding: 0 var(--space-2);
}

.publish-controls {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

/* Two versions side by side, neither thrown away unseen. */
.conflict {
    border: 1px solid var(--danger);
    border-radius: var(--radius);
    padding: var(--space-4) var(--space-6);
}

.conflict-versions {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
}

.conflict-body {
    background: var(--background);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: var(--text-tiny);
    max-height: 16rem;
    overflow: auto;
    overflow-wrap: anywhere;
    padding: var(--space-3);
    white-space: pre-wrap;
}

.recovery-codes .codes {
    display: grid;
    font-family: var(--font-mono);
    gap: var(--space-2);
    grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
    list-style: none;
    padding: 0;
}

/*
    Left-aligned, like everything else on the page. Centring made an empty table look like a
    different screen from the form directly beneath it.
*/
.empty {
    color: var(--text-muted);
    padding: var(--space-8) 0;
}

/* Admin controls ----------------------------------------------------------------------------- */

/*
    One treatment for every control, so a text box, a select and a button on the same row are the
    same height. They were browser defaults before, and a default select is a couple of pixels
    taller than a default input on every platform for a different reason on each — which is what
    made filter rows jitter.

    The height comes from one line-height plus one padding, stated once here and inherited by all
    three, rather than from a fixed height that would clip a control the moment the text scales.
*/
.admin-main input:not([type="checkbox"]):not([type="radio"]):not([type="file"]),
.admin-main select,
.admin-main textarea,
.admin-main button,
.admin-main .button {
    background: var(--background);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-sizing: border-box;
    color: var(--text);
    font: inherit;
    font-size: var(--text-small);
    line-height: 1.4;
    padding: var(--space-2) var(--space-3);
}

.admin-main button,
.admin-main .button {
    background: var(--surface);
    cursor: pointer;
    font-family: var(--font-mono);

    /* An <a> carrying .button is still a link, and a link is underlined until told otherwise. */
    text-decoration: none;
    white-space: nowrap;
}

.admin-main button:hover,
.admin-main .button:hover {
    border-color: var(--accent);
}

/*
    Sized to what they hold, not to the container. A tag name is a word and a slug is a word; a
    full-width box for either wastes the row and makes a table of them unreadable.
*/
.admin-main input,
.admin-main select {
    width: auto;
}

.admin-main textarea {
    width: 100%;
}

/*
    A field is a label, a control and whatever the field has to say about itself, stacked. Without
    this the hint is an inline span that starts wherever the control ends and wraps around it, so
    the sentence runs up the right-hand side of the input and continues underneath — which reads as
    a broken layout rather than as a hint.

    align-items keeps a control sized to its content from being stretched to the column's width.
*/
.field {
    align-items: start;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.control--tiny { inline-size: 6rem; }
.control--short { inline-size: 12rem; }
.control--medium { inline-size: 20rem; }
.control--wide { inline-size: 32rem; max-inline-size: 100%; }

/*
    Focus rings are drawn outside the border box, so any ancestor that scrolls clips them. The
    editor's panes scroll, and the outline on the leftmost field was cut in half down its left
    edge. A scroll container needs room inside it for the ring it will one day have to show.
*/
.editor-source,
.editor-preview,
.admin-main .conflict-body,
.admin-table-scroll {
    padding-inline: var(--focus-room);
}

:root {
    /* The 3px outline plus its 2px offset, and a pixel so it never sits flush against the edge. */
    --focus-room: 6px;
}

/*
    The file input is a browser default and looks like one: a grey button whose label the platform
    chooses and the page cannot restyle. Hiding it and labelling it instead gives the same control
    with the same keyboard behaviour — a label is still a click target, and the input still takes
    focus, so the ring appears on the label through :focus-within. No script involved.
*/
.file-field {
    display: inline-flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    align-items: center;
}

.file-field input[type="file"] {
    /*
        Not display:none and not visibility:hidden: either takes the input out of the focus order,
        and the control stops being reachable by keyboard. Clipped to nothing instead, which
        leaves it focusable and leaves the label to show where the focus went.
    */
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}

.file-field__button {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    display: inline-block;
    font-family: var(--font-mono);
    font-size: var(--text-small);
    line-height: 1.4;
    margin: 0;
    padding: var(--space-2) var(--space-3);
}

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

.file-field input[type="file"]:focus-visible + .file-field__button {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
}

/*
    A row of controls that sit on one baseline. `end` rather than `center`, because a field with a
    label above it is taller than a bare button and they should line up along the bottom.
*/
.control-row {
    align-items: end;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
}

.control-row__spacer {
    margin-left: auto;
}

/* The one action a row is for, marked by fill rather than by size or position alone. */
.admin-main button.primary,
.admin-main .button.primary {
    background: color-mix(in srgb, var(--accent) 14%, var(--surface));
    border-color: color-mix(in srgb, var(--accent) 40%, transparent);
    font-weight: 500;
}

.admin-main button.primary:hover,
.admin-main .button.primary:hover {
    border-color: var(--accent);
}

/*
    Fifty tags is the size this has to stay readable at, so the table is dense and the columns are
    sized to their content rather than sharing the width equally. The last column takes what is
    left, which keeps the actions in one place down the page.
*/
/*
    Hugs its content rather than filling the window. Stretched, a table of three short columns puts
    half a screen of nothing between a tag's name and its address, and the eye has to travel it on
    every row — which is exactly what makes fifty rows unreadable.
*/
.admin-table--compact {
    table-layout: auto;
    width: auto;
}

.admin-table--compact td,
.admin-table--compact th {
    padding: var(--space-1) var(--space-3);
}

.admin-table--compact td:last-child {
    white-space: nowrap;
}

.admin-table__actions {
    display: inline-flex;
    gap: var(--space-2);
}

/* A wide table scrolls inside itself rather than widening the page. */
.admin-table-scroll {
    overflow-x: auto;
}

/* Media library ------------------------------------------------------------------------------- */

/*
    Three separate things — upload, search, results — that ran together with no space between them,
    so the grid looked like it was overlapping the row above it. The rule under the uploader says
    where one ends and the next begins.
*/
.uploader {
    border-bottom: 1px solid var(--border);
    margin-bottom: var(--space-6);
    padding-bottom: var(--space-6);
}

.media-search {
    margin-bottom: var(--space-6);
}

/* The thumbnail is the link, so the whole tile is the target rather than a corner of it. */
.media-card__view {
    display: block;
    line-height: 0;
}

.media-card__view:focus-visible {
    outline-offset: -3px;
}

/* The editor -------------------------------------------------------------------------------- */

/*
    The title in the page's own heading position: large, borderless until touched, and reading as
    the heading it will become rather than as the first of eleven form fields.
*/
.editor-title {
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    color: var(--text);
    font-family: var(--font-mono);
    font-size: var(--text-h2);
    font-weight: 500;
    padding: var(--space-2) var(--space-3);
    width: 100%;
}

.editor-title:hover {
    border-color: var(--border);
}

.editor-title:focus-visible {
    background: var(--surface);
    border-color: var(--border);
}

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

/* The writing surface fills its pane; the toolbar and the hint take what they need. */
.body-editor {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-height: 0;
}

.body-editor__label {
    /*
        The textarea is the obvious thing on the screen and the toolbar above it says what it is,
        so the label is for a screen reader rather than for the eye.
    */
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
}

.body-editor__text {
    flex: 1 1 auto;
    min-height: 26rem;
    resize: vertical;
}

/* Where a dragged image will land. */
.body-editor__text.is-over {
    border-color: var(--accent);
    box-shadow: inset 0 0 0 1px var(--accent);
}

.body-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
}

.admin-main .body-toolbar__button {
    font-size: var(--text-small);
    min-width: 2.2rem;
    padding: var(--space-1) var(--space-2);
    text-align: center;
}

/*
    The panel that asks what the pasted image shows. Accent-bordered because it is a question
    waiting on an answer, and the writer has just done something that has not finished yet.
*/
.paste-panel {
    background: var(--surface);
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-4);
}

.paste-panel__file {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--text-tiny);
    margin: 0;
    overflow-wrap: anywhere;
}

/*
    Metadata, below the writing and closed. Styled as a panel rather than a bare disclosure so that
    a closed one reads as a place rather than as a stray line of text under the editor.
*/
.editor-meta {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-3) var(--space-4);
}

.editor-meta > summary {
    cursor: pointer;
    font-family: var(--font-mono);
    font-size: var(--text-small);
}

.editor-meta[open] > summary {
    border-bottom: 1px solid var(--border);
    margin-bottom: var(--space-4);
    padding-bottom: var(--space-3);
}

/* The cover, shown rather than described by its key. */
.cover-field__chosen {
    align-items: start;
    display: flex;
    gap: var(--space-3);
}

.cover-field__image {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    height: auto;
    max-width: 12rem;
    object-fit: cover;
}

.cover-field__detail {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-width: 0;
}

.cover-field__key {
    color: var(--text-muted);
    font-size: var(--text-tiny);
    overflow-wrap: anywhere;
}

/* The dashboard ----------------------------------------------------------------------------- */

.counters {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    list-style: none;
    margin: 0 0 var(--space-6);
    padding: 0;
}

.counters a {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    display: flex;
    flex-direction: column;
    font-size: var(--text-small);
    gap: var(--space-1);
    min-width: 8rem;
    padding: var(--space-3) var(--space-4);
    text-decoration: none;
    transition: border-color var(--transition);
}

.counters a:hover {
    border-color: var(--accent);
}

.counters__value {
    font-family: var(--font-mono);
    font-size: var(--text-h3);
    line-height: 1;
}

/*
    Panels of roughly equal weight, laid out so that the state of the site is one glance. auto-fit
    rather than a fixed count: on a narrow screen they stack, on a wide one they fill the row, and
    neither case needs its own breakpoint.
*/
.panel-grid {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
}

.panel {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    min-width: 0;
    padding: var(--space-4);
}

/*
    Something here is waiting on the operator. A left bar rather than a colour change, so it still
    reads for someone who cannot tell the accent from the border.
*/
.panel--attention {
    border-left: 3px solid var(--accent);
}

.admin-main .panel__title {
    font-size: var(--text-small);
    color: var(--text-muted);
    font-family: var(--font-mono);
    letter-spacing: 0.04em;
    margin: 0;
    text-transform: uppercase;
}

.panel__note {
    margin: 0;
}

.panel__command {
    font-size: var(--text-tiny);
    margin: 0;
    padding: var(--space-2) var(--space-3);
}

.panel__facts {
    display: grid;
    gap: var(--space-1) var(--space-3);
    grid-template-columns: auto 1fr;
    margin: 0;
}

.panel__facts dt {
    color: var(--text-muted);
    font-size: var(--text-small);
}

.panel__facts dd {
    margin: 0;
    overflow-wrap: anywhere;
}

.panel .brief-list,
.panel .counters {
    list-style: none;
    margin: 0;
    padding: 0;
}

/*
    The dashboard lists put a title and a moment on one line, and inline elements with a newline
    between them in the markup collapse to nothing on screen — so every row read as
    "Запланированная запись8 сентября". A flex row spaces them and keeps the date from wrapping
    under a long title.
*/
.brief-list li {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-3);
    justify-content: space-between;
    padding-block: var(--space-1);
}

.brief-moment {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--text-tiny);
    white-space: nowrap;
}

/* Inside a panel an empty state is one line among several, not a page of its own. */
.panel .empty {
    padding: 0;
}
