/* h.ua — public site stylesheet.
   One file, no framework, no build step, no third-party requests. The only
   JavaScript on the site is progressive enhancement (public/js/app.js), so
   every rule here must produce a complete page on its own.

   Source of truth: the Claude Design project "h.ua Public Site" — its token
   table, type scale and the "Notes for site.css" table. Keep this file in
   step with that document rather than tuning values ad hoc. */

/* ---------------------------------------------------------------- 1. Fonts
   Fira Sans, self-hosted. The Cyrillic and Latin subsets are separate files
   with `unicode-range`, so a Ukrainian page downloads ~7 KB per weight and
   never fetches the Latin Extended subset it does not draw from. */
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/fonts/fira-sans-400-cyrillic-ext.woff2") format("woff2");
    unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/fonts/fira-sans-400-cyrillic.woff2") format("woff2");
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/fonts/fira-sans-400-latin-ext.woff2") format("woff2");
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/fonts/fira-sans-400-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("/fonts/fira-sans-600-cyrillic-ext.woff2") format("woff2");
    unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("/fonts/fira-sans-600-cyrillic.woff2") format("woff2");
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("/fonts/fira-sans-600-latin-ext.woff2") format("woff2");
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("/fonts/fira-sans-600-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/fonts/fira-sans-700-cyrillic-ext.woff2") format("woff2");
    unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/fonts/fira-sans-700-cyrillic.woff2") format("woff2");
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/fonts/fira-sans-700-latin-ext.woff2") format("woff2");
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/fonts/fira-sans-700-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Greek, Georgian, Hebrew and Arabic. Greek is a Fira Sans subset under the
   same family, so the brand stack is unchanged. The other three are Noto Sans
   families of their own (SIL OFL 1.1), listed AFTER Fira Sans in the stack so
   they only fill glyphs Fira lacks; `unicode-range` means a page in another
   script never downloads them. */
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/fonts/fira-sans-400-greek.woff2") format("woff2");
    unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("/fonts/fira-sans-600-greek.woff2") format("woff2");
    unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
@font-face {
    font-family: "Fira Sans";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/fonts/fira-sans-700-greek.woff2") format("woff2");
    unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
@font-face {
    font-family: "Noto Sans Georgian";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/fonts/noto-sans-georgian-400.woff2") format("woff2");
    unicode-range: U+10A0-10FF, U+1C90-1CBF, U+2D00-2D2F;
}
@font-face {
    font-family: "Noto Sans Georgian";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/fonts/noto-sans-georgian-700.woff2") format("woff2");
    unicode-range: U+10A0-10FF, U+1C90-1CBF, U+2D00-2D2F;
}
@font-face {
    font-family: "Noto Sans Hebrew";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/fonts/noto-sans-hebrew-400.woff2") format("woff2");
    unicode-range: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
@font-face {
    font-family: "Noto Sans Hebrew";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/fonts/noto-sans-hebrew-700.woff2") format("woff2");
    unicode-range: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
@font-face {
    font-family: "Noto Sans Arabic";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/fonts/noto-sans-arabic-400.woff2") format("woff2");
    unicode-range: U+0600-06FF, U+0750-077F, U+0870-08FF, U+FB50-FDFF, U+FE70-FEFF;
}
@font-face {
    font-family: "Noto Sans Arabic";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/fonts/noto-sans-arabic-700.woff2") format("woff2");
    unicode-range: U+0600-06FF, U+0750-077F, U+0870-08FF, U+FB50-FDFF, U+FE70-FEFF;
}

/* --------------------------------------------------------------- 2. Tokens
   Light is the bare `:root` definition. Dark redefines the same properties
   twice: once under `prefers-color-scheme` guarded against an explicit light
   choice, and once under `[data-theme="dark"]` so the JS toggle wins in both
   directions. No colour is ever defined only inside a media query. */
:root {
    color-scheme: light dark;
    --bg: #f8f7f4;
    --fg: #161512;
    --muted: #6b6864;
    --border: #d9d6d0;
    --accent: #8a3324;
    --accent-hover: #6e281c;
    --focus: #161512;
    --input-bg: #ffffff;
    --surface: #f0ede5;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg: #1f1e1b;
        --fg: #ecebe6;
        --muted: #a19d96;
        --border: #3a3833;
        --accent: #d9927f;
        --accent-hover: #e8ab9b;
        --focus: #ecebe6;
        --input-bg: #282725;
        --surface: #29271f;
    }
}

:root[data-theme="dark"] {
    --bg: #1f1e1b;
    --fg: #ecebe6;
    --muted: #a19d96;
    --border: #3a3833;
    --accent: #d9927f;
    --accent-hover: #e8ab9b;
    --focus: #ecebe6;
    --input-bg: #282725;
    --surface: #29271f;
}

/* ----------------------------------------------------------------- 3. Base */
*, *::before, *::after { box-sizing: border-box; }
html { font-size: 100%; }
body {
    margin: 0;
    font-family: "Fira Sans", "Noto Sans Georgian", "Noto Sans Hebrew", "Noto Sans Arabic", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    font-size: 1rem;
    line-height: 1.45;
    color: var(--fg);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
    -webkit-text-size-adjust: 100%;
    text-wrap: pretty;
}

/* CJK families for the Chinese hosts. No webfont: a Chinese face is 5-15 MB,
   which the unicode-range subsetting used for Fira Sans cannot rescue. These
   sit BEHIND "Fira Sans" so Latin text in a Chinese page — names, acronyms,
   numerals — still renders in Fira Sans, and only Han falls through.
   Scoped by :lang() so the CJK families never enter the cascade on the other
   six languages. `:lang(zh)` also matches `zh-tw` per CSS language-range
   rules, so the more specific `zh-TW` rule comes second, to win. */
:lang(zh) body,
body:lang(zh) {
    font-family: "Fira Sans", "Noto Sans Georgian", "Noto Sans Hebrew", "Noto Sans Arabic", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", system-ui, sans-serif;
    line-height: 1.7;
}
:lang(zh-TW) body,
body:lang(zh-TW) {
    font-family: "Fira Sans", "Noto Sans Georgian", "Noto Sans Hebrew", "Noto Sans Arabic", "PingFang TC", "Microsoft JhengHei", "Noto Sans CJK TC", system-ui, sans-serif;
    line-height: 1.7;
}

/* Arabic needs more leading than Latin; Korean breaks between words, not
   syllables; Japanese and Korean get system faces as the Chinese hosts do
   (no webfont: a CJK face is megabytes). Same shape as the :lang(zh) block. */
:lang(ar) body,
body:lang(ar) { line-height: 1.75; }
:lang(ko) body,
body:lang(ko) { word-break: keep-all; }
:lang(ja) body,
body:lang(ja) {
    font-family: "Fira Sans", "Noto Sans Georgian", "Noto Sans Hebrew", "Noto Sans Arabic", "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Yu Gothic", "Noto Sans JP", system-ui, sans-serif;
}
:lang(ko) body,
body:lang(ko) {
    font-family: "Fira Sans", "Noto Sans Georgian", "Noto Sans Hebrew", "Noto Sans Arabic", "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans KR", system-ui, sans-serif;
}

/* Arrow glyphs point along the reading direction; mirror them under RTL. */
[dir="rtl"] .dir-glyph { display: inline-block; transform: scaleX(-1); }

img { max-width: 100%; }

/* The single column. Header, main and footer share it so the wordmark, the
   text and the footer rules all line up on the same two edges. */
header, main, footer { max-width: 40rem; margin: 0 auto; padding: 0 1.25rem; }

a { color: var(--accent); text-underline-offset: 3px; }
a:hover { color: var(--accent-hover); }
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* ------------------------------------------------------------ 4. Skip link */
.skip {
    position: absolute;
    top: -100%;
    inset-inline-start: 0.5rem;
    z-index: 10;
    padding: 0.5rem 0.75rem;
    background: var(--fg);
    color: var(--bg);
    font-size: 0.875rem;
    font-weight: 600;
    text-decoration: none;
}
.skip:focus { top: 0.5rem; }

/* --------------------------------------------------------------- 5. Header
   Sticky so the wordmark stays reachable in a long story. `is-stuck` is added
   by app.js once the page has scrolled; without JS the bar is still sticky, at
   its full height and without the rule under it.

   Stuck, it shrinks to a little over half its height and the wordmark drops a
   third of its size: a masthead earns its space at the top of the page and
   stops earning it the moment it is a bar over somebody's reading.

   The height is set explicitly rather than left to padding so that giving it
   up can be paid back exactly. A sticky element stays in flow, so a header
   that shrinks by 2rem pulls the whole page up by 2rem under a reader who did
   not ask for that — and the jump lands at the exact moment their eye is
   moving. `margin-bottom` returns precisely the 2rem the height gave up, and
   because both sides animate on the same curve the flow is unchanged in every
   frame of the transition, not merely at its ends. With `box-sizing:
   border-box` the hairline is inside the height, so it does not disturb the
   arithmetic either. */
.site-header {
    position: sticky;
    top: 0;
    z-index: 5;
    display: flex;
    align-items: center;
    height: 4.5rem;
    background: var(--bg);
    transition: height 0.18s ease, margin-bottom 0.18s ease;
}
.site-header.is-stuck {
    height: 2.5rem;
    margin-bottom: 2rem;
    border-bottom: 1px solid var(--border);
}
.wordmark {
    font-size: 1.375rem;
    font-weight: 700;
    color: var(--fg);
    text-decoration: none;
    transition: font-size 0.18s ease;
}
.site-header.is-stuck .wordmark { font-size: 1rem; }
.wordmark:hover { color: var(--accent); }

/* ----------------------------------------------------------------- 6. Type */
h1 { margin: 2.5rem 0 1.75rem; font-size: 2.5rem; line-height: 1.1; font-weight: 700; }
@media (max-width: 900px) { h1 { font-size: 2rem; } }
@media (max-width: 480px) { h1 { font-size: 1.75rem; } }

.meta {
    margin: 0;
    font-size: 0.6875rem;
    line-height: 1.1;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}
.meta a { color: var(--muted); text-decoration: none; }
.meta a:hover { color: var(--accent); }

/* The story page's timestamp line carries four things — a label, a date, a
   clock and a timezone — and only the date is worth a reader's eye. The whole
   line stays muted and the date alone is lifted back to body colour, whether
   it is a plain span or the link to that day's archive. */
.meta .date { color: var(--fg); }
.meta a.date:hover { color: var(--accent); }

/* A run-in title with a rule above it: "Yesterday's edition", "Sources". */
.section-title {
    margin: 3.5rem 0 0;
    padding-top: 0.75rem;
    border-top: 1px solid var(--fg);
    font-size: 1.375rem;
    line-height: 1.15;
    font-weight: 400;
    text-transform: uppercase;
    letter-spacing: 0.02em;
}
.section-title a { color: var(--fg); text-decoration: none; }
.section-title a:hover { color: var(--accent); }
.section-title .count { color: var(--muted); }

/* An empty day, an empty topic, no search results: a statement, not an error. */
.note { margin: 0; font-size: 1.1875rem; line-height: 1.45; color: var(--muted); }
.note-tight { margin: 0.875rem 0 0; font-size: 1rem; line-height: 1.45; color: var(--muted); }

/* ------------------------------------------------- 7. Edition story list
   Two columns: the emoji marker, then everything else. `compact` is the
   previous-days and archive-month variant — headline only, tighter padding. */
.stories { list-style: none; margin: 0; padding: 0; }
.stories li {
    display: grid;
    grid-template-columns: 2.75rem 1fr;
    padding: 1.375rem 0;
    border-top: 1px solid var(--border);
}
.stories .n {
    font-size: 1.375rem;
    line-height: 1.15;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}
.stories h2, .stories h3 { margin: 0 0 0.5rem; font-size: 1.375rem; line-height: 1.15; font-weight: 600; }
.stories li:first-child h2 { font-size: 1.625rem; }
.stories h2 a, .stories h3 a { color: var(--fg); text-decoration: none; }
.stories h2 a:hover, .stories h3 a:hover { color: var(--accent); }
.stories p { margin: 0 0 0.5rem; }
.stories .meta { margin: 0; }
.stories.compact li { padding: 0.625rem 0; }
.stories.compact .n, .stories.compact h3 { font-size: 1.125rem; margin: 0; }

/* A previous day's block on the homepage, and a day block in the archive month. */
.day-block { margin: 0; }
.day-block .stories { margin-top: 0.625rem; }

/* Wide-screen edition layout.

   Everything above is the phone layout: one story per row, one column. Past
   the breakpoint the edition pages widen the column they share with the header
   and the footer — the same 71rem the story page uses, so the two page types
   line up on the same two edges — and lay the edition out as a mosaic rather
   than a single tall stack.

   The mosaic is 6 columns wide because that divides by both 2 and 3: the two
   lead stories take three columns each, and everything after them takes two,
   so a full five-story edition falls out as 2 on the first row and 3 on the
   second with no per-item rules. The daily edition is capped at five stories
   (app.daily_edition.max_stories), so that is the shape almost every day has.

   Short editions are the exception, and each gets its own answer rather than
   being left lopsided: three stories become one row of three, and a lone story
   drops back to a single reading column, because one story stretched across a
   third of a 71rem page reads as a layout that broke. */
@media (min-width: 64rem) {
    body.edition-page header,
    body.edition-page main,
    body.edition-page footer { max-width: 71rem; }

    .stories:not(.compact) {
        display: grid;
        grid-template-columns: repeat(6, minmax(0, 1fr));
        column-gap: 2.5rem;
        align-items: start;
    }
    .stories:not(.compact) > li { grid-column: span 2; }
    .stories:not(.compact) > li:nth-child(-n+2) { grid-column: span 3; }

    /* Both stories on the lead row carry the larger headline, not just the
       first: side by side they are peers, and sizing only one of them would
       read as an accident. */
    .stories:not(.compact) > li:nth-child(2) h2 { font-size: 1.625rem; }

    /* A summary in a two-column cell runs about half as wide and so twice as
       tall as it does in the single column, which leaves the row ragged. The
       lead pair keeps its full summary; the rest are clipped to five lines,
       with the whole text a click away on the story itself. */
    .stories:not(.compact) > li:nth-child(n+3) p:not(.meta) {
        display: -webkit-box;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 5;
        line-clamp: 5;
        overflow: hidden;
    }

    .stories:not(.compact):has(> li:nth-child(3):last-child) > li { grid-column: span 2; }
    .stories:not(.compact):has(> li:only-child) { grid-template-columns: minmax(0, 42rem); }

    /* A day of history is headlines only, so it columnises much harder than
       the edition does: three across turns a week of scrolling into a block
       the eye takes in at once. Row-wise fill keeps the ranking order. */
    .stories.compact {
        display: grid;
        grid-template-columns: repeat(3, minmax(0, 1fr));
        column-gap: 2.5rem;
        align-items: start;
    }
    .stories.compact li { grid-template-columns: 2rem 1fr; }
}

/* ----------------------------------------------------------- 8. Story page */
article .standfirst { margin: 0 0 0.5rem; font-size: 1.375rem; line-height: 1.35; }
article p { font-size: 1.1875rem; line-height: 1.45; margin: 0 0 1em; }
article section h2 {
    margin: 2.25rem 0 0.625rem;
    font-size: 0.75rem;
    line-height: 1.1;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--muted);
}
article .body-h2 { font-size: 2rem; line-height: 1.15; font-weight: 700; margin: 2rem 0 0.75rem; }
@media (max-width: 480px) { article .body-h2 { font-size: 1.75rem; } }

/* Sources, grouped by publication date. The favicon is decorative — the
   outlet name carries the meaning — so it takes alt="" and a fixed box that
   holds its place whether or not the file exists. */
.source-group h3 {
    margin: 1.375rem 0 0.375rem;
    font-size: 0.6875rem;
    line-height: 1.1;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}
.sources { list-style: none; margin: 0; padding: 0; }
.sources li {
    display: grid;
    grid-template-columns: 16px 1fr;
    gap: 0.625rem;
    padding: 0.625rem 0;
    border-top: 1px solid var(--border);
    font-size: 1rem;
    line-height: 1.4;
}
.sources .icon { width: 16px; height: 16px; margin-top: 0.25rem; display: block; }
.sources .outlet { font-weight: 600; }
.sources .date { color: var(--muted); font-size: 0.875rem; white-space: nowrap; }

/* Topics, people, organizations at the end of a story: a label column and a
   wrapping list. */
.tags {
    margin-top: 2.5rem;
    padding-top: 1rem;
    border-top: 1px solid var(--border);
    font-size: 0.875rem;
    line-height: 1.45;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}
.tags > div { display: flex; gap: 1rem; }
.tags .label {
    width: 6rem;
    flex: none;
    font-size: 0.6875rem;
    line-height: 1.8;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}
.tags .list { display: flex; flex-wrap: wrap; gap: 0.25rem 0.875rem; }
.tags a { text-decoration: none; }
.tags a:hover { text-decoration: underline; }

/* A person named in the body text, linked to their own page. Subtle on
   purpose: a dotted underline in the body colour, not another red link. A
   paragraph where every name is an accent-coloured link stops reading like
   prose. */
.person-mention {
    color: inherit;
    text-decoration: underline dotted var(--muted);
    text-decoration-thickness: 1px;
    text-underline-offset: 3px;
}
.person-mention:hover { color: var(--accent); text-decoration-color: var(--accent); }

/* The scannable spine of the story, directly under the standfirst: a boxed
   list a reader can take in without reading the prose. It is the one block a
   reader who reads nothing else should still leave with, so it gets a tint
   and not only a rule. The tint is deliberately faint — a few percent off the
   page background, no coloured fill — because the page has exactly one accent
   colour and it belongs to links. The negative inline margin bleeds the panel past the text measure
   so the bullets stay on the body's left edge instead of stepping in; 1.125rem
   is smaller than main's 1.25rem gutter and than the story grid's 3rem
   column gap, so the bleed never reaches the viewport edge or the sidebar. */
.key-points {
    margin: 1.75rem -1.125rem 0;
    padding: 0.875rem 1.125rem 1rem;
    background: var(--surface);
    border-top: 1px solid var(--fg);
    border-bottom: 1px solid var(--border);
}
article .key-points h2 { margin-top: 0; }
.key-points ul { margin: 0; padding: 0 0 0 1.125rem; }
.key-points li { font-size: 1.0625rem; line-height: 1.4; margin-bottom: 0.375rem; }
.key-points li:last-child { margin-bottom: 0; }
.key-points li::marker { color: var(--muted); }

/* What people said. Usually our summary of a statement rather than a
   transcript, so: a rule and an indent, and deliberately no oversized
   quotation mark that would claim these are the exact words. */
.quotes { margin-top: 0.5rem; }
.quote { margin: 0 0 1.5rem; }
.quote figcaption {
    margin: 0 0 0.375rem;
    font-size: 0.8125rem;
    line-height: 1.3;
    color: var(--muted);
}
.quote figcaption i { font-style: normal; }
.quote figcaption b { color: var(--fg); font-weight: 600; }
.quote blockquote {
    margin: 0;
    padding: 0.125rem 0;
    padding-inline-start: 1rem;
    border-inline-start: 3px solid var(--border);
    font-size: 1.0625rem;
    line-height: 1.5;
}
.quotes .section-title + .quote { margin-top: 1rem; }

/* In the sidebar the statements are apparatus, not body copy, so they take the
   sidebar's smaller measure — the same step down the source list and the
   related items already take. The rule down the leading edge stays: it is what says
   "these are words attributed to someone" without claiming they are verbatim. */
.story-aside .quote blockquote {
    font-size: 0.9375rem;
    line-height: 1.45;
    padding-inline-start: 0.75rem;
}
.story-aside .quote { margin-bottom: 1.125rem; }

/* Wide-screen story layout. Everything above is the phone layout and stays
   the shared single column; only past the breakpoint does the story page
   widen the column it shares with the header and the footer (so all three
   still line up on the same two edges) and split the article into a reading
   column that keeps its measure and a narrower column for the apparatus. */
.story-head, .story-body, .story-aside { min-width: 0; }

@media (min-width: 64rem) {
    body.story-page header,
    body.story-page main,
    body.story-page footer { max-width: 71rem; }

    .story {
        display: grid;
        grid-template-columns: minmax(0, 40rem) minmax(14rem, 1fr);
        /* The aside spans both rows and is routinely taller than the head and
           the body together — a long source list outruns the article. With two
           `auto` rows the browser hands that surplus height to BOTH spanned
           rows, so the head's row inflates and `align-items: start` leaves the
           extra as a blank band between the head and the first section. `1fr`
           on the body's row makes it the only track that grows, so the head's
           row stays exactly as tall as the head and the body starts flush
           under it; the surplus becomes trailing space beside the aside, where
           nothing is missing from. */
        grid-template-rows: auto 1fr;
        column-gap: 3rem;
        align-items: start;
    }
    .story-head { grid-column: 1; grid-row: 1; }
    .story-body { grid-column: 1; grid-row: 2; }
    .story-aside { grid-column: 2; grid-row: 1 / span 2; }

    /* One apparatus column until there is genuinely room for two. Splitting at
       the same breakpoint the article splits at would give each half about
       7rem, which is narrower than a source headline wants and would set the
       whole right side in two-word lines. */
    .aside-column > :first-child .section-title,
    .aside-column > .section-title:first-child { margin-top: 2.5rem; }

    /* At the top of the side column the "Sources" rule is a beginning, not a
       break in a long page, so it loses the big gap it needs when stacked. */
    .story-aside .section-title { margin-top: 2.5rem; }
    .story-aside .tags { margin-top: 2rem; }
    .story-aside .tags > div { flex-direction: column; gap: 0.125rem; }
    .story-aside .tags .label { width: auto; line-height: 1.4; }
}

/* Three columns: the article, then the apparatus split in two.
   1120px is where the two apparatus columns each clear ~15rem, which is the
   width at which a source headline stops setting in two-word lines. */
@media (min-width: 70rem) {
    body.story-page header,
    body.story-page main,
    body.story-page footer { max-width: 82rem; }

    .story {
        grid-template-columns: minmax(0, 40rem) minmax(0, 1fr);
        column-gap: 3rem;
    }

    .story-aside {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        column-gap: 2.5rem;
        align-items: start;
    }

    /* Each column starts its own flow, so the first rule in either one is a
       beginning rather than a break part-way down a long page — and whatever
       happens to lead a column starts flush with it, since which block that is
       depends on what the story has. */
    .aside-column > *:first-child { margin-top: 0; }
    .aside-column > *:first-child .section-title,
    .aside-column > .section-title:first-child { margin-top: 0; }
}

/* The line a reader meets when a saved story link no longer resolves. Set as
   a notice rather than an error: nothing has gone wrong, the edition simply
   moved on, and red would tell them otherwise. */
.note.removed {
    margin: 2rem 0 0;
    padding: 0.875rem 1rem;
    border: 1px solid var(--border);
    border-radius: 0.4rem;
    font-size: 1rem;
}

/* The parody comment. Styled to read as an aside and never as a quotation:
   no quote marks, no rule down the left, nothing borrowed from how the story's
   real quotes are set. The note under the heading is the same small uppercase
   caption the rest of the page uses for metadata, because that is what it is —
   a statement about what this text is, not part of the joke. */
/* The parody sits below the statements, so it needs its own top margin — it
   used to lead the column and take its spacing from that. Same 2.5rem as the
   related list, which is the gap between two unrelated sidebar blocks. */
.commentary { margin: 2.5rem 0; }
.commentary .section-title { margin-top: 0; }
.commentary-note {
    margin: 0.375rem 0 0;
    font-size: 0.6875rem;
    line-height: 1.1;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}
.commentary-text {
    margin: 0.625rem 0 0;
    font-size: 1rem;
    line-height: 1.4;
    color: var(--fg);
}

/* Related news in the story aside: date over headline, no summary. The date
   is the small print and the headline is the link, which is the opposite of
   the sources list above it — there the outlet is the small print. Both are
   lists of links under a story; making them look identical would invite a
   reader to think they lead to the same kind of place. */
.related { margin-top: 2.5rem; }
.related ul { list-style: none; margin: 0.625rem 0 0; padding: 0; }
.related li { padding: 0.75rem 0; border-top: 1px solid var(--border); }
.related time {
    display: block;
    margin-bottom: 0.2rem;
    font-size: 0.6875rem;
    line-height: 1.1;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}
.related a { color: var(--fg); text-decoration: none; font-size: 1rem; line-height: 1.25; }
.related a:hover { color: var(--accent); }

/* ---------------------------------------------------------- 9. Topic index */
.topics {
    list-style: none;
    margin: 0;
    padding: 1rem 0 0;
    border-top: 1px solid var(--border);
    columns: 3;
    column-gap: 2rem;
    font-size: 1.125rem;
    line-height: 1.15;
}
.topics li { padding: 0.5rem 0; break-inside: avoid; }
.topics a { color: var(--fg); text-decoration: none; }
.topics a:hover { color: var(--accent); }
@media (max-width: 700px) { .topics { columns: 1; } }

/* -------------------------------------------------------------- 10. Archive */
.years, .months { list-style: none; margin: 0; padding: 0; font-size: 1.375rem; line-height: 1.15; font-weight: 600; }
.years li, .months li {
    padding: 1.125rem 0;
    border-top: 1px solid var(--border);
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
}
.years a, .months a { color: var(--fg); text-decoration: none; }
.years a:hover, .months a:hover { color: var(--accent); }
.months .count {
    font-size: 0.6875rem;
    font-weight: 400;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}

/* --------------------------------------------------------------- 11. Search
   A three-column grid on wide screens, one column narrow. The query spans the
   full width; every control carries a visible label. */
form.search {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1rem;
    padding-bottom: 1.75rem;
    border-bottom: 1px solid var(--border);
}
form.search .wide { grid-column: 1 / -1; }
form.search label { display: flex; flex-direction: column; gap: 0.375rem; }
form.search .field-label {
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}
@media (max-width: 700px) { form.search { grid-template-columns: 1fr; } }

input, select, button {
    font: inherit;
    font-size: 1rem;
    padding: 0.625rem 0.75rem;
    border: 1px solid var(--border);
    background: var(--input-bg);
    color: var(--fg);
    border-radius: 0;
    width: 100%;
    box-sizing: border-box;
}
button {
    width: auto;
    background: var(--fg);
    color: var(--bg);
    border: 0;
    font-weight: 600;
    padding: 0.625rem 1.125rem;
    cursor: pointer;
}

.results { list-style: none; margin: 0.5rem 0 0; padding: 0; }
.results li {
    padding: 1.375rem 0;
    border-top: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}
.results h2 { margin: 0; font-size: 1.375rem; line-height: 1.15; font-weight: 600; }
.results h2 a { color: var(--fg); text-decoration: none; }
.results h2 a:hover { color: var(--accent); }
.results p { margin: 0; }
.result-summary { margin: 1.5rem 0 0; }

/* ----------------------------------------------------------- 12. Pagination
   The unavailable direction is a muted span, never a dead link. */
.pagination {
    display: flex;
    justify-content: space-between;
    margin-top: 2rem;
    padding-top: 0.875rem;
    border-top: 1px solid var(--border);
    font-size: 0.875rem;
}
.pagination a { text-decoration: none; }
.pagination a:hover { text-decoration: underline; }
.pagination > span { color: var(--muted); }

/* --------------------------------------------------------------- 13. Footer */
footer {
    margin-top: 3.5rem;
    padding-top: 1.25rem;
    padding-bottom: 2.5rem;
    border-top: 1px solid var(--border);
    font-size: 0.875rem;
    display: flex;
    flex-direction: column;
    gap: 0.875rem;
}
footer nav { display: flex; flex-wrap: wrap; gap: 0.375rem 1.125rem; }
footer a { text-decoration: none; }
footer a:hover { text-decoration: underline; }
footer [aria-current] { font-weight: 600; color: var(--fg); }

/* The language switcher is a native <details>: all six links are in the HTML
   and it opens and closes with no JavaScript. app.js only adds outside-click
   and Escape closing. */
footer details summary {
    list-style: none;
    cursor: pointer;
    display: inline-flex;
    gap: 0.375rem;
    align-items: center;
    color: var(--accent);
}
footer details summary::-webkit-details-marker { display: none; }
footer details summary:hover { text-decoration: underline; }
footer details ul {
    list-style: none;
    margin: 0.625rem 0 0;
    padding: 0.625rem 0 0;
    border-top: 1px solid var(--border);
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
    gap: 0.25rem 1rem;
    max-width: min(40rem, calc(100vw - 2rem));
}
.switcher-label {
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}

/* The theme toggle only exists once JS has run — a control that cannot work
   without a script must not be painted before the script arrives. */
.theme-toggle {
    display: none;
    align-self: flex-start;
    width: auto;
    padding: 0;
    border: 0;
    background: none;
    color: var(--accent);
    font-size: 0.875rem;
    font-weight: 400;
    cursor: pointer;
}
.theme-toggle:hover { text-decoration: underline; }
.js .theme-toggle { display: inline-flex; gap: 0.375rem; align-items: center; }

/* Google's preferred-sources button, hidden for the same reason as the theme
   toggle and one more: the div is an empty anchor until Google's script fills
   it, so a browser that never runs JavaScript would reserve a gap in the
   footer for a button that is never coming. The × is ours and sits beside it;
   it is deliberately quiet, because the offer it dismisses is quiet too. */
.preferred-source {
    display: none;
    align-items: center;
    gap: 0.5rem;
    align-self: flex-start;
}
.js .preferred-source { display: flex; }
.preferred-source [data-preferred-dismiss] {
    width: auto;
    padding: 0 0.25rem;
    border: 0;
    background: none;
    color: var(--muted);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}
.preferred-source [data-preferred-dismiss]:hover { color: var(--fg); }

/* The About page's permanent copy needs no `.js` gate of its own — it is the
   same empty div, and it simply stays empty in a browser that cannot draw it. */
.preferred-source-settings > [google-add-preferred-source-btn] { margin-top: 0.75rem; }

/* The theme toggle and the language switcher, grouped. Narrow screens stack
   them, which is what the footer did before they were wrapped. */
.footer-controls {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.875rem;
}

/* From 48rem the footer is one row: section links at the left, the two
   settings together at the trailing edge. `margin-inline-start: auto` rather than
   `justify-content: space-between`, so the pair stays at the trailing edge
   whatever else the row happens to contain. */
@media (min-width: 48rem) {
    footer {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: baseline;
        column-gap: 1.5rem;
    }
    footer nav { flex: 1 1 auto; }
    .footer-controls {
        flex-direction: row;
        align-items: baseline;
        gap: 1.5rem;
        margin-inline-start: auto;
    }

    /* The preferred-sources button keeps a line of its own below the row:
       `order` puts it after both, and a full-basis item is guaranteed to wrap.
       It is a call to action, not a setting, and squeezing it between the two
       would read as one. */
    .preferred-source { flex-basis: 100%; order: 1; }

    /* Opening the switcher in a right-hand corner has to open a panel rather
       than grow the box: in flow the ten names would either wrap into a narrow
       column or push the row's own width around. Upward, because the footer is
       the last thing on the page and there is nothing below it to open into. */
    .footer-controls details { position: relative; }
    .footer-controls details ul {
        position: absolute;
        inset-inline-end: 0;
        bottom: 100%;
        z-index: 2;
        width: max-content;
        white-space: nowrap;
        margin: 0 0 0.5rem;
        padding: 0.625rem 0.875rem;
        border: 1px solid var(--border);
        border-radius: 0.25rem;
        background: var(--bg);
    }
}

/* ------------------------------------------------------------ 14. Utilities */
.visually-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 (prefers-reduced-motion: reduce) {
    *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}

/* The reader's dismissal, applied by the inline script in the document head
   before the first paint. `!important` is not needed and not used: nothing else
   sets `display` on this section. */
.commentary-hidden .commentary {
    display: none;
}

.commentary {
    position: relative;
}

/* Hidden until app.js marks the document scripted, on the same rule as the
   theme toggle: a button that only JavaScript can implement must not be
   offered to a browser that will never run it. The About page's switch and the
   whole section around it follow the same rule — a disclaimer that survives
   without script is one thing, a switch that cannot switch is another. */
.commentary-dismiss,
.commentary-switch,
.tracking-switch {
    display: none;
}

.js .commentary-switch,
.js .tracking-switch {
    display: block;
}

/* A switch rather than a button, because a lone button labelled with an action
   («Показувати «Трампампам»») reads just as naturally as a statement of the
   current setting, and readers took it that way. The label here is fixed and
   the control carries the state, so there is nothing left to misread.

   Built on a real checkbox: `appearance: none` only takes away the native
   painting. With this rule dropped it degrades to an ordinary tickbox that
   still works, still labels itself and is still keyboard-reachable. */
.switch {
    display: inline-flex;
    align-items: center;
    gap: 0.625rem;
    cursor: pointer;
}

.switch input {
    appearance: none;
    -webkit-appearance: none;
    position: relative;
    flex: none;
    width: 2.25rem;
    height: 1.25rem;
    margin: 0;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg);
    cursor: pointer;
    transition: background-color 0.15s, border-color 0.15s;
}

/* The knob. An inset rather than a transform so the travel is the same whichever
   way the track is sized; the logical inset mirrors it under dir="rtl". */
.switch input::after {
    content: "";
    position: absolute;
    top: 50%;
    inset-inline-start: 0.1875rem;
    width: 0.875rem;
    height: 0.875rem;
    border-radius: 50%;
    background: var(--muted);
    transform: translateY(-50%);
    transition: left 0.15s, right 0.15s, background-color 0.15s;
}

.switch input:checked {
    border-color: var(--accent);
    background: var(--accent);
}

.switch input:checked::after {
    inset-inline-start: calc(100% - 1.0625rem);
    background: var(--bg);
}

.switch input:focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    .switch input,
    .switch input::after { transition: none; }
}

.js .commentary-switch button:hover,
.js .commentary-switch button:focus-visible {
    border-color: var(--muted);
}

.js .commentary-dismiss {
    position: absolute;
    top: 0;
    inset-inline-end: 0;
    display: block;
    padding: 0 0.25rem;
    border: 0;
    background: none;
    color: var(--muted);
    font-size: 1.25rem;
    line-height: 1;
    cursor: pointer;
    /* A joke's dismiss button should not compete with the joke. */
    opacity: 0.5;
    transition: opacity 0.15s ease;
}

.js .commentary-dismiss:hover,
.js .commentary-dismiss:focus-visible {
    opacity: 1;
}

/* A copy-paste watermark, not decoration.

   The reason it is transparent and 1px rather than `display: none` is the whole
   feature: browsers exclude `display: none` and `visibility: hidden` text from
   the selection and so from the clipboard, which is precisely where this text
   needs to end up. Someone pasting the joke into a group chat carries none of
   this page's context with it — not the heading, not the About link — so the
   pasted line has to say for itself that a machine wrote it.

   It stays in the accessibility tree on purpose: a screen-reader user should
   hear it too. */
.commentary-watermark {
    font-size: 1px;
    line-height: 1;
    color: transparent;
    /* Belt and braces against a future `user-select: none` on an ancestor. */
    user-select: text;
    -webkit-user-select: text;
}

/* Entity reference box: the checkable half of a person/organization page.
   Everything above it is model-written prose; this is Wikidata, verified by
   label and by type before it was stored. Visually quieter than the
   standfirst on purpose — it is a reference, not the lede. */
.entity-facts {
    margin: 1.25rem 0 1.75rem;
    padding: 0.85rem 1rem;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--input-bg);
    font-size: 0.9rem;
}

.entity-facts-title {
    margin: 0 0 0.5rem;
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}

.entity-facts dl {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.3rem 0.85rem;
    margin: 0;
}

.entity-facts dt {
    color: var(--muted);
    white-space: nowrap;
}

.entity-facts dd {
    margin: 0;
    /* A long official URL must wrap rather than widen the grid column and
       push the page into a horizontal scroll on a phone. */
    overflow-wrap: anywhere;
}

.entity-facts-source {
    margin: 0.6rem 0 0;
    font-size: 0.75rem;
    color: var(--muted);
}

/* -------------------------------------------------- 15. Visited articles
   A story you have already opened is dimmed. public/js/app.js keeps the list
   of what this browser has read — the page itself is one cached response
   served to everybody and cannot know — and puts `is-visited` on the block.

   Dimmed rather than hidden or greyed out, because a headline you read this
   morning is the thing you use to find your way back to it this evening; and
   restored under the pointer, so a muted block is never harder to read than
   an unread one. With JavaScript blocked the class never lands and every list
   renders exactly as it does today. */
.stories li.is-visited,
.results li.is-visited,
.related li.is-visited,
.sources li.is-visited {
    opacity: 0.4;
}

.stories li.is-visited:hover,
.results li.is-visited:hover,
.related li.is-visited:hover,
.sources li.is-visited:hover {
    opacity: 1;
}

/* ------------------------------------------------------- 16. About page
   A stack of short, independent sections, which is the one shape on this site
   that a single 40rem column serves badly: on a desktop screen it left two
   thirds of the window empty and pushed the analytics switch — the only
   control on the page — below the fold.

   Multi-column rather than a grid, because these sections have wildly
   different heights and a two-column grid would align them into rows and leave
   a hole under every short one. Columns flow instead, and `break-inside` keeps
   a heading from being orphaned from its paragraph. Reading order is
   unchanged: down the first column, then down the second.

   The heading and the standfirst span both columns; the standfirst keeps a
   reading measure of its own, because a single sentence set across 71rem is
   not a standfirst, it is a banner. Below 64rem none of this applies and the
   page is the same single column it has always been. */
@media (min-width: 64rem) {
    body.about-page header,
    body.about-page main,
    body.about-page footer { max-width: 71rem; }

    body.about-page article {
        columns: 2;
        column-gap: 4rem;
    }

    body.about-page article > h1,
    body.about-page article > .standfirst { column-span: all; }

    body.about-page article > .standfirst { max-width: 45rem; }

    body.about-page article > section {
        break-inside: avoid;
        margin-bottom: 2.5rem;
    }
}
