/*
 * Blog UI — the Press Room skin for /blog/, /blog/<slug> and /saved.
 *
 * Hand-authored, loaded only by those three surfaces, and deliberately NOT
 * part of landing.built.css: this is ~1,700 lines of reading chrome that every
 * marketing and tool page would otherwise download to render a page that has
 * none of it. The blog pages load landing.built.css (site chrome, tokens,
 * .interior-* typography) and then this file on top.
 *
 * ## Tokens
 *
 * The design system is webapp/app/styles/press.css (+ paper.css), which
 * landing.built.css now carries for every marketing page. This file keeps its
 * own short names (--stock, --ink, --spot …) because ~2,000 lines read them and
 * the reader palettes redefine them on #main-content, but each one is an ALIAS
 * of a shared token — there is no second copy of a value. Both themes flip in
 * press.css (its dark media blocks, plus <html data-theme> via
 * postcss-theme-attr), so there is no dark token block here. verify-blog.mjs
 * asserts every alias names the shared token it should.
 *
 * Everything below the token block reads tokens. Outside that block, the sepia
 * palette (a genuinely different paper stock) and the print sheet, there are no
 * colour literals in this file, and verify-blog.mjs fails on one.
 *
 * ## Spot
 *
 * The brand blue is spent in four categories only — the primary action, the
 * live selection, progress, and the focus ring / registration marks — and
 * verify-blog.mjs keeps a selector allowlist for every rule that references a
 * --spot* token. A new spot use edits both in one commit.
 *
 * ## Geometry
 *
 * Radius 0 everywhere except .post-avatar (an avatar is round). Elevation is
 * --shadow-lift on floating planes and --shadow-sheet on the bottom sheet; every
 * other edge is a 1px hairline. Hover lays down more ink; nothing lifts.
 *
 * The tokens invert in press.css, so nearly every component needs no dark rule
 * of its own. The few that do (artwork swaps, code plates) key on `.dark` on
 * <html>, which the head boot script sets to the RESOLVED theme.
 *
 * Because it is outside the Tailwind build there is no purge step, which is
 * what makes it safe for the classes that only ever exist in JS-generated DOM
 * (.toc-chip, .copy-toast, .tts-*, .saved-*, .print-*). The invariant that
 * keeps that true: pages/blog-assets/*.js must never emit a Tailwind utility.
 *
 * Breakpoints: 640 / 768 / 1024 / 1280 / 1536px, matching Tailwind's sm–2xl so
 * the two stylesheets never disagree about where a layout changes.
 */

/* =============================================================== tokens = */
/* Aliases of the shared press tokens (left: this file's name). Both themes come
   from press.css; verify-blog.mjs holds each alias to its target. */
html.press-blog {
    --stock: var(--color-paper);
    --sheet: var(--color-paper-raised);
    --sheet-2: var(--color-slate-100);
    --ink: var(--color-ink);
    --ink-2: var(--color-ink-soft);
    --ink-3: var(--color-slate-400);             /* 4.52:1 on stock */
    --rule: var(--color-hairline);
    --rule-soft: var(--paper-hairline-soft);
    --spot: var(--paper-spot);                   /* fills */
    --spot-text: var(--paper-spot-text);         /* 5.23:1 on stock */
    --spot-press: var(--paper-spot-press);       /* more ink, never dimming */
    --spot-ink: var(--paper-spot-ink);           /* what sits ON a spot fill */
    --spot-wash: var(--paper-spot-wash);
    --spot-wash-strong: var(--paper-spot-wash-strong);
    --ok: var(--color-success);
    --hold: var(--color-warning);                /* fill/border only, 3.6:1 */
    --hold-text: var(--color-caution-fg);
    --stop: var(--color-error);
    --stop-wash: var(--color-danger-bg);
    --ok-wash: var(--paper-ok-wash);
    --scrim: var(--paper-scrim);
    --shadow-lift: var(--elevation-2);
    /* --shadow-sheet, --font-mono and --font-read are press.css's own names. */
    --font-ui: var(--font-sans);
    --ease: cubic-bezier(0.2, 0.9, 0.3, 1);
}

/* ============================================================ foundations = */
.blog-icon { width: 1.125rem; height: 1.125rem; flex: none; }

/* Visually hidden, still announced. Owned here rather than borrowed from
   Tailwind's .sr-only: blog-tooltips.js injects this from JS, and Tailwind's
   content globs only cover HTML, so .sr-only survived purge purely because an
   unrelated page happened to use it. That is the invariant in the header
   comment above, and this class is what makes it true instead of lucky. */
.blog-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
}

/* Every measured value — dates, minutes, percentages, counts, the share URL —
   is set in mono so the page reads as an instrument. */
.num,
.blog-row-date,
.blog-row-mins,
.blog-row-num,
.blog-feature-meta,
.post-meta time,
#readingTimeText,
.post-related-date,
.reader-readout b,
.reader-pct,
.reader-tp-status,
.reader-panel-foot b,
.blog-tab--saved b,
.blog-search-key,
.saved-meta,
.reader-url-text {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}

/* Plate labels: 11px, wide-tracked, uppercase — labels, not headings. */
.blog-eyebrow,
.post-eyebrow,
.blog-feature-topper,
.tldr-title,
.reader-panel-label,
.reader-cmd-group,
.post-endcap h2,
.post-related-topper,
.blog-category,
.mega-category-press {
    font-family: var(--font-ui);
    font-size: var(--text-11);                   /* the site's type floor, as .plate-label */
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-3);
}
/* The rule that runs off the end of a label. */
.blog-eyebrow,
.post-eyebrow,
.tldr-title,
.reader-panel-label,
.post-endcap h2 { display: flex; align-items: center; gap: 0.625rem; }
.blog-eyebrow::after,
.post-eyebrow::after,
.tldr-title::after,
.reader-panel-label::after,
.post-endcap h2::after { content: ''; flex: 1; height: 1px; background: var(--rule); }

.blog-category { display: inline-block; letter-spacing: 0.08em; }

/* Focus ring: the spot, 2px, outside the box. Wins over the utilities' ring. */
body.press-blog :focus-visible { outline: 2px solid var(--spot); outline-offset: 2px; }
body.press-blog ::selection { background: var(--spot-wash-strong); color: inherit; }

/* Qualified with the element so these beat the per-context image rules
   (.blog-card-image, .post-related-image …) regardless of source order. A bare
   .img-dark ties with them on specificity and loses, which renders both the
   light and dark artwork stacked. */
body.press-blog img.img-dark { display: none; }
.dark body.press-blog img.img-dark { display: block; }
.dark body.press-blog img.img-light { display: none; }

/* ======================================================== site chrome = */
/*
 * The nav, mega menu, CTA plate and footer are natively Press Room now: their
 * markup comes from site-shell.mjs and their styles from tailwind.landing.css,
 * on the same press tokens this file aliases. Nothing here restyles them.
 *
 * The navbar's BOX still matters to this file's scripts: blog-sticky.js derives
 * --reader-sticky-offset from the inner row's height plus the scrolled py-4
 * padding and 1px border it hard-codes (NAV_SCROLLED_PADDING/BORDER), and
 * .blog-article's clearance is built on that.
 */

/* ============================================================= blog index = */
/*
 * One appearance per post. Every post used to appear up to three times — a
 * featured card, a cluster of three per category, and an archive of everything —
 * on a blog with five posts.
 *
 * The featured post is the card above the toolbar and is deliberately NOT
 * repeated in the list below it. It carries the same data-category/data-search
 * attributes every row does, so a filter or a query that stops matching it hides
 * it, and the top of the page never argues with the list underneath.
 */
/* The interior hero is centred marketing type on forty tool pages; on the
   three blog surfaces it is a left-set masthead. */
body.press-blog .interior-hero { text-align: left; padding-bottom: 2rem; }
.blog-eyebrow { margin: 0 0 1rem; }
body.press-blog .interior-hero h1 {
    font-size: 2.25rem;
    font-weight: 700;
    font-variation-settings: 'wdth' 112;
    letter-spacing: -0.03em;
    line-height: 1.08;
    margin-bottom: 0.75rem;
    color: var(--ink);
    text-wrap: balance;
}
@media (min-width: 768px) { body.press-blog .interior-hero h1 { font-size: 2.75rem; } }
body.press-blog .interior-hero .hero-subtitle { margin: 0; max-width: 56ch; font-size: 0.9375rem; line-height: 1.62; color: var(--ink-2); }

.blog-archive { padding: 0 0 4rem; }

/* -------------------------------------------------------------- feature -- */
/* A sheet plane with registration marks in two corners. Hover lays down more
   ink on the border; nothing lifts. */
.blog-feature {
    position: relative;
    display: grid;
    grid-template-columns: 1.05fr 1fr;
    margin-bottom: 1.5rem;
    border: 1px solid var(--rule);
    background: var(--sheet);
    overflow: hidden;
    text-decoration: none;
    transition: border-color var(--transition-duration-base, 200ms) var(--ease);
}
.blog-feature[hidden] { display: none; }
.blog-feature:hover { border-color: var(--ink); }
/* Crop marks: L-shaped, top-left and bottom-right, in the spot at half
   strength — the registration device. The feature card does not use these: its
   artwork is generated by scripts/blog-cards/template.mjs, which already draws
   the same device into all four corners of the tile. */
.tldr-box::before,
.tldr-box::after {
    content: '';
    position: absolute;
    z-index: 1;
    width: 12px;
    height: 12px;
    border: 1px solid var(--spot);
    opacity: 0.5;
    pointer-events: none;
}
.tldr-box::before { top: 6px; left: 6px; border-right: 0; border-bottom: 0; }
.tldr-box::after { bottom: 6px; right: 6px; border-left: 0; border-top: 0; }

/* The card is as tall as whichever is taller: the body, or the tile at its own
   1200x630 ratio up to a ceiling. The ratio comes from a ::before rather than
   `aspect-ratio`, which a stretched grid item ignores — its height is decided by
   the row, and the row is what we are trying to size. The image is absolutely
   positioned so it contributes no height of its own and covers whatever the row
   settles at; that is also what lets `height: 100%` resolve, since against a row
   sized by the image the percentage would be circular.
   The 20rem ceiling is what stops a full-rail card from being 330px of artwork
   over three lines of text. */
.blog-feature-art {
    position: relative;
    display: block;
    overflow: hidden;
    min-height: 11.5rem;
    background: var(--sheet-2);
    border-right: 1px solid var(--rule);
}
.blog-feature-art::before { content: ''; display: block; padding-top: min(52.5%, 20rem); }
.blog-feature-art img { position: absolute; inset: 0; display: block; width: 100%; height: 100%; object-fit: cover; }
/* A post can ship before its cards are drawn. */
.blog-feature:not(:has(.blog-feature-art)) { grid-template-columns: minmax(0, 1fr); }

.blog-feature-body {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 0.6rem;
    min-width: 0;
    padding: 1.5rem 1.6rem;
}
.blog-feature-topper { display: block; }
.blog-feature-dot { margin: 0 0.4rem; opacity: 0.5; }
.blog-feature-title {
    font-size: 1.5rem;
    font-weight: 700;
    font-variation-settings: 'wdth' 108;
    line-height: 1.15;
    letter-spacing: -0.02em;
    color: var(--ink);
    text-wrap: balance;
}
.blog-feature-text { font-size: 0.875rem; line-height: 1.55; color: var(--ink-2); }
.blog-feature-meta { font-size: 0.6875rem; color: var(--ink-3); }

@media (max-width: 767px) {
    .blog-feature { grid-template-columns: minmax(0, 1fr); }
    .blog-feature-art { min-height: 0; aspect-ratio: 40 / 17; border-right: 0; border-bottom: 1px solid var(--rule); }
    .blog-feature-body { padding: 1.15rem 1.15rem 1.35rem; }
    .blog-feature-title { font-size: 1.25rem; }
}

/* The toolbar hangs off the navbar's bottom edge, so that edge has to hold
   still. #navbar animates its own padding on scroll — site-shell.mjs swaps py-6
   for py-4 over 300ms — which moved the edge 16px every time the reader started
   scrolling and left the toolbar clipped behind a navbar that was still tall.
   An article already opts out of that for the reader bar (see the rule under
   .reader-bar); the index is the other page with something pinned directly
   beneath the edge, and it opts out at every width because the toolbar is
   top-pinned at every width. Padding only: the background, blur and border the
   navbar gains on scroll are untouched. */
body:has(.blog-toolbar) #navbar { padding-top: 1rem; padding-bottom: 1rem; }

.blog-toolbar {
    position: sticky;
    top: var(--reader-sticky-offset, 5rem);
    z-index: 20;
    display: flex;
    align-items: stretch;
    gap: 0.6rem;
    flex-wrap: wrap;
    padding: 0.75rem 0;
    background: var(--stock);
    /* This rule is also the rule the list starts on — the toolbar is sticky, so
       it has to draw its own edge as rows pass under it, and .blog-archive-grid
       therefore does not draw a second one 4px below it. */
    border-bottom: 1px solid var(--rule);
}

/* A text field: sheet, one rule, and the spot on the border when focused. */
.blog-search {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex: 1 1 14rem;
    min-width: 0;
    min-height: 2.25rem;
    padding: 0 0.75rem;
    border: 1px solid var(--rule);
    background: var(--sheet);
    color: var(--ink-3);
    transition: border-color var(--transition-duration-fast, 150ms) var(--ease);
}
.blog-search:focus-within { border-color: var(--spot); }
.blog-search .blog-icon { flex: none; width: 1rem; height: 1rem; }
.blog-search input {
    flex: 1;
    min-width: 0;
    border: 0;
    background: none;
    outline: none;
    font-family: inherit;
    font-size: 0.875rem;
    color: var(--ink);
}
.blog-search input::placeholder { color: var(--ink-3); }
/* Safari's own clear button, which sits on a background this one does not use. */
.blog-search input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
/* The shortcut this actually answers to. The mock showed a bare "/" here, which
   blog-filter.js deliberately does not bind — a single character key needs to be
   remappable or focus-scoped under WCAG 2.1.4, and "/" is Firefox's Quick Find. */
.blog-search-key {
    flex: none;
    font-size: 0.625rem;
    color: var(--ink-3);
    border: 1px solid var(--rule);
    padding: 0.05rem 0.3rem;
}

/* Stage tabs — plate selectors. One ruled strip; a 2px spot rule marks the
   live plate, the same device the app's mode tabs use. */
.blog-tablist { display: flex; flex: 1 1 auto; border: 1px solid var(--rule); background: var(--sheet); }
.blog-tab {
    position: relative;
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-height: 2.25rem;
    padding: 0 0.8rem;
    border: 0;
    background: none;
    color: var(--ink-3);
    font-family: inherit;
    font-size: 0.625rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--transition-duration-fast, 150ms) var(--ease), color var(--transition-duration-fast, 150ms) var(--ease);
}
.blog-tab + .blog-tab { border-left: 1px solid var(--rule); }
.blog-tab::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 2px;
    background: transparent;
    transition: background var(--transition-duration-fast, 150ms) var(--ease);
}
.blog-tab:hover { color: var(--ink); background: var(--sheet-2); }
.blog-tab[aria-pressed='true'] { color: var(--spot-text); background: var(--stock); }
.blog-tab[aria-pressed='true']::before { background: var(--spot); }
.blog-tab[hidden] { display: none; }
.blog-tab .blog-icon { width: 0.9rem; height: 0.9rem; }
.blog-tab--saved b {
    font-size: 0.5625rem;
    font-weight: 600;
    padding: 0.05rem 0.3rem;
    border: 1px solid var(--rule);
    color: var(--ink-3);
}
.blog-tab--saved[aria-pressed='true'] b { border-color: var(--spot); color: var(--spot-text); }
/* Its own end of the strip: it filters by something the reader did rather than
   by what the article is about, so it does not belong in the category run. */
.blog-tab--saved { margin-left: auto; border-left: 1px solid var(--rule); }

@media (max-width: 639px) {
    /* One strip that scrolls, not a wrapped cloud of pills. A visible thin bar,
       so scrollability is perceivable. */
    .blog-tablist {
        flex: 1 1 100%;
        overflow-x: auto;
        overflow-y: hidden;
        -webkit-overflow-scrolling: touch;
        scroll-snap-type: x proximity;
        overscroll-behavior-x: contain;
        scrollbar-width: thin;
        scrollbar-color: var(--ink-3) var(--stock);
    }
    .blog-tablist::-webkit-scrollbar { height: 3px; }
    .blog-tablist::-webkit-scrollbar-track { background: var(--stock); }
    .blog-tablist::-webkit-scrollbar-thumb { background: var(--ink-3); }
    .blog-tab { scroll-snap-align: start; }
}

/* -------------------------------------------------------------- archive -- */
/* Rules, not cards: the list has a rule on top — the sticky toolbar's own
   bottom border, so the two never stack into a double line — and one under
   every row. */
.blog-archive-grid { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; }
.blog-row { border-bottom: 1px solid var(--rule); }
.blog-row[hidden] { display: none; }

/* Running number, artwork, then title and standfirst, then everything scannable
   pushed to the right edge where the eye can run down one column of dates
   instead of hunting for them at the end of three different line lengths. */
.blog-row-link {
    display: grid;
    grid-template-columns: 2rem 5rem minmax(0, 1fr) auto;
    align-items: center;
    gap: 1rem;
    padding: 0.95rem 0.5rem 0.95rem 0.25rem;
    text-decoration: none;
    transition: background var(--transition-duration-fast, 150ms) var(--ease);
}
.blog-row-link:hover { background: var(--sheet); }
.blog-row-num { font-size: 0.6875rem; color: var(--ink-3); }

/* The listing tiles are authored at 1200x630. The slot keeps that ratio so a
   thumbnail is a smaller tile rather than a crop through the middle of the words
   drawn on it. */
.blog-row-media {
    display: block;
    overflow: hidden;
    background: var(--sheet-2);
    box-shadow: inset 0 0 0 1px var(--rule);
    aspect-ratio: 40 / 21;
}
.blog-row-media img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* A post can ship before its cards are drawn, so the column collapses rather
   than leaving a hole. */
.blog-row-link:not(:has(.blog-row-media)) { grid-template-columns: 2rem minmax(0, 1fr) auto; }

.blog-row-body { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
.blog-row-title {
    font-size: 0.9375rem;
    font-weight: 600;
    line-height: 1.35;
    letter-spacing: -0.012em;
    color: var(--ink);
}
/* One line, clipped. Two wrapped lines of standfirst per row turns a list you
   scan into a page you read. */
.blog-row-text {
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--ink-2);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.blog-row-meta {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    font-size: 0.6875rem;
    color: var(--ink-3);
    white-space: nowrap;
}
.blog-row-date { color: inherit; }
.blog-row-mark { display: inline-flex; color: var(--ink); }
.blog-row-mark[hidden] { display: none; }
.blog-row-mark .blog-icon { width: 0.875rem; height: 0.875rem; }

@media (max-width: 639px) {
    .blog-row-link { grid-template-columns: 3.25rem minmax(0, 1fr); gap: 0.85rem; align-items: start; }
    .blog-row-num,
    .blog-row-text { display: none; }
    /* The meta cluster drops under the title rather than squeezing a third
       column into 320px. Column 2 keeps it aligned with the text above it. */
    .blog-row-meta { grid-column: 2; margin-top: 0.15rem; gap: 0.5rem; }
    .blog-row-link:not(:has(.blog-row-media)) { grid-template-columns: minmax(0, 1fr); }
    .blog-row-link:not(:has(.blog-row-media)) .blog-row-meta { grid-column: 1; }
}

.blog-archive-empty { padding: 2.5rem 0.25rem; color: var(--ink-2); font-size: 0.9375rem; }
.blog-archive-empty button {
    border: 0;
    border-bottom: 1px solid var(--spot-text);
    background: none;
    padding: 0 0 2px;
    color: var(--spot-text);
    font: inherit;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    cursor: pointer;
}

/* ========================================================= post: layout == */
.blog-article { padding-top: var(--nav-clearance); padding-bottom: 4rem; position: relative; }

/* Below 1024px the bar floats just above the bottom safe area, so the article
   needs to end above the island rather than behind it. */
@media (max-width: 1023px) {
    .blog-article { padding-bottom: calc(4rem + 4.25rem + env(safe-area-inset-bottom, 0px)); }
}

.blog-article-body { margin-top: 2rem; }

@media (min-width: 1024px) {
    .blog-article-body {
        display: grid;
        /* The gutters are tracks rather than `gap`, because collapsing the
           contents pane has to close that pane's gutter and leave the share
           rail's alone — which one `gap` cannot express. */
        grid-template-columns:
            var(--reader-pane-width, 264px) var(--reader-gutter, 2.5rem)
            minmax(0, 1fr)
            var(--reader-gutter, 2.5rem) auto;
        grid-template-areas: 'pane pane-gutter main share-gutter share';
        align-items: start;
    }
    .reader-panel--contents { grid-area: pane; }
    .main-content { grid-area: main; }
    .post-share-rail { grid-area: share; }
    /* Collapsing the track rather than hiding the panel: display:none would drop
       the element out of the accessibility tree while its toggle still claims to
       control it. The share track is `auto` — exactly as wide as the tiles — so
       it has nothing to collapse. */
    .reader-pane-collapsed .blog-article-body {
        grid-template-columns: 0 0 minmax(0, 1fr) var(--reader-gutter, 2.5rem) auto;
    }
}
@media (min-width: 1280px) { .blog-article-body { --reader-pane-width: 276px; --reader-gutter: 3rem; } }

/* The lead image opens the article column, directly above the takeaways, rather
   than spanning the rail above the whole body. No max-height crop on the
   artwork: these tiles carry words, and a banner crop cuts straight through
   them. The hairline frame is what integrates the picture with the ruled page. */
.post-lead { margin: 0 0 1.75rem; background: var(--sheet-2); box-shadow: inset 0 0 0 1px var(--rule); }
.post-lead-image { display: block; width: 100%; height: auto; }

.anchor { display: block; height: 0; }

/* ----------------------------------------------------------- share rail -- */
/* Platform marks. This is the one place the stylesheet spends colour it did not
   choose: a share row is read by its brands, and five grey squares are not
   recognisable as Facebook or X. The geometry is still press — squared,
   hairline, no lift — and the marks are the only literals here, which is what
   the literal-ok marker below scopes. They do not change between themes; only
   the X tile does, because a black square disappears into the dark ground. */
.post-share-rail { /* literal-ok */
    --brand-facebook: #1877F2;
    --brand-x: #17161B;
    --brand-link: #0D9488;
    --brand-mail: #EA580C;
    --brand-native: #16A34A;
    --brand-ink: #FFFFFF;
}
html.dark.press-blog .post-share-rail { /* literal-ok */
    --brand-x: #3A3844;
}

.post-share-rail {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.85rem;
    /* Level with the takeaways, and it stays there: the article runs several
       screens and a share control at the top of it is out of reach for all but
       the first one. */
    position: sticky;
    top: calc(var(--reader-sticky-offset, 4.625rem) + 3rem + 1.25rem);
}
/* Reads bottom-to-top up the side of the column, which is the direction a
   vertical label runs when it sits to the right of what it labels. */
.post-share-label {
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    font-size: 0.625rem;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--ink-3);
}
.post-share-tiles { display: flex; flex-direction: column; gap: 0.5rem; }
.share-tile {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    padding: 0;
    border: 1px solid var(--rule);
    background: var(--tile);
    color: var(--brand-ink);
    cursor: pointer;
    transition: filter var(--transition-duration-fast, 150ms) var(--ease), border-color var(--transition-duration-fast, 150ms) var(--ease);
}
/* Hidden until blog-actions.js finds a share sheet to hand off to. */
.share-tile[hidden] { display: none; }
/* More ink, nothing lifts: the mark darkens rather than growing a shadow, which
   also means the hover state needs no second colour per platform. */
.share-tile:hover { filter: brightness(0.92); border-color: var(--ink); }
.share-tile .blog-icon { width: 1.125rem; height: 1.125rem; }
.share-tile--facebook { --tile: var(--brand-facebook); }
.share-tile--x { --tile: var(--brand-x); }
.share-tile--link { --tile: var(--brand-link); }
.share-tile--mail { --tile: var(--brand-mail); }
.share-tile--native { --tile: var(--brand-native); }

/* No room beside the measure below 1024px, and the reader bar's Share button is
   already within thumb reach there. */
@media (max-width: 1023px) {
    .post-share-rail { display: none; }
}

/* --------------------------------------------------------- post header -- */
/* A masthead across the whole rail: eyebrow, title, byline and rule. The
   contents column, the lead image and the article body begin below it, so the
   table of contents starts level with the artwork rather than the headline. */
.post-header { padding-bottom: 1.4rem; border-bottom: 1px solid var(--rule); }
/* The category as a plate label with its rule. Not a link: category pages do
   not exist, which is why the crumb is inert too. */
.post-eyebrow { margin: 0 0 1rem; }

/* Sized for the rail it spans, and uncapped: the headline runs the full width
   of the page. text-wrap: balance is what keeps a two-line headline from
   breaking with one word on the second line. */
.post-title {
    font-size: 2rem;
    font-weight: 700;
    font-variation-settings: 'wdth' 112;
    letter-spacing: -0.03em;
    line-height: 1.1;
    color: var(--ink);
    text-wrap: balance;
}
@media (min-width: 768px) { .post-title { font-size: 2.75rem; } }
@media (min-width: 1280px) { .post-title { font-size: 3rem; } }

/* One line of provenance, not a control surface. Every action that used to live
   up here is in the reader bar now. */
.post-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem;
    margin: 0.85rem 0 0;
    font-size: 0.75rem;
    color: var(--ink-3);
}
.post-meta-author { display: inline-flex; align-items: center; gap: 0.45rem; font-weight: 600; color: var(--ink); }
/* alt="" and no aria-hidden: the name is right beside it, so the image adds
   nothing to announce, but it is a real avatar rather than decoration and a
   sized box keeps it from shifting the byline as it decodes. An avatar is the
   one genuinely round thing on the page. */
.post-avatar {
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 999px;
    background: var(--sheet);
    border: 1px solid var(--rule);
    object-fit: cover;
}
/* The dots become hairline dividers; the glyph stays in the markup for copy
   and screen readers (it is aria-hidden) but paints at zero size. */
.post-meta-dot { display: inline-block; width: 0; height: 0.9em; border-left: 1px solid var(--rule); font-size: 0; }
.post-meta time { color: inherit; }

/* Revealed by blog-toc.js only while the date it carries is still recent, so it
   cannot outlive the claim it makes. A status stamp: mono, ruled in the OK ink. */
.post-fresh {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.1rem 0.4rem;
    border: 1px solid var(--ok);
    font-family: var(--font-mono);
    font-size: 0.5625rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ok);
}
.post-fresh[hidden] { display: none; }
.post-fresh .blog-icon { width: 0.75rem; height: 0.75rem; }

/* ====================================================== reader chrome === */
/*
 * One bar, one panel per surface. Above 1024px the bar is sticky under the site
 * navbar and the contents panel is a column beside the article; below it, the
 * bar is fixed in the thumb zone and every panel is a bottom sheet. The markup
 * is identical at both widths — only this file differs — so the two breakpoints
 * cannot disagree about what a panel contains.
 *
 * --reader-sticky-offset is published by blog-toc.js from the live height of
 * #navbar; jump calculations add this bar's live height. The navbar is fixed AND
 * animates its own height on scroll (site-shell.mjs swaps py-6 for py-4 over
 * 300ms), so anything that
 * hard-codes a clearance here is wrong for the length of that animation.
 */
.reader-bar {
    position: fixed;
    left: 0.75rem;
    right: 0.75rem;
    bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
    z-index: 40;
    width: auto;
    max-width: 42rem;
    height: 3.5rem;
    margin-inline: auto;
    /* Opaque on purpose. A backdrop-filter here would make this element the
       containing block for every position:fixed descendant, which is exactly
       what the sheets are. */
    background: var(--sheet);
    border: 1px solid var(--rule);
    box-shadow: var(--shadow-lift);
    overflow: visible;
}

/* On compact screens the contents fill a bounded floating island. Desktop
   restores a full-width bar and puts the controls back on the site's rail. */
.reader-bar-inner { display: flex; align-items: stretch; height: 100%; }

/* Reading gets the whole screen; the controls are one flick back. The hysteresis
   in blog-actions.js matters more than the hiding — a bar that vanishes while a
   sheet is open, while a control has focus, or mid-playback has disappeared for
   no reason the reader can see. */
.reader-bar { transition: transform var(--transition-duration-base, 200ms) var(--ease); }
.reader-bar.is-hidden { transform: translateY(calc(100% + 1rem + env(safe-area-inset-bottom, 0px))); }

.reader-bar-context { display: none; }

.reader-bar-actions { display: flex; align-items: stretch; flex: 1; min-width: 0; }

/* The island is one ruled strip of plate-label buttons; the selected one takes
   the 2px spot rule on its top edge. */
.reader-btn {
    position: relative;
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.2rem;
    min-width: 0;
    min-height: 2.75rem;
    padding: 0.25rem;
    border: 0;
    background: none;
    color: var(--ink-3);
    font-family: inherit;
    font-size: 0.5625rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: pointer;
    transition: color var(--transition-duration-fast, 150ms) var(--ease), background var(--transition-duration-fast, 150ms) var(--ease);
}
.reader-btn + .reader-btn,
.reader-transport + .reader-btn { border-left: 1px solid var(--rule); }
.reader-btn::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 2px;
    background: transparent;
    transition: background var(--transition-duration-fast, 150ms) var(--ease);
}
.reader-btn:hover { color: var(--ink); }
.reader-btn[aria-expanded='true'],
.reader-btn[aria-pressed='true'] { color: var(--spot-text); }
.reader-btn[aria-expanded='true']::before,
.reader-btn[aria-pressed='true']::before { background: var(--spot); }
.reader-btn .blog-icon { width: 1rem; height: 1rem; }
.reader-btn-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }

/* Save swaps its own icon off aria-pressed, so blog-bookmarks.js never has to
   touch an icon — it sets one attribute and both the accessible state and the
   glyph follow. */
.bookmark-filled { display: none; }
.reader-btn[aria-pressed='true'] .bookmark-empty { display: none; }
.reader-btn[aria-pressed='true'] .bookmark-filled { display: inline-flex; }

@media (min-width: 1024px) {
    /*
     * The bar sits on the navbar's bottom edge and never leaves it. Two separate
     * movements used to read as a stutter, and both are removed here rather than
     * damped.
     *
     * One: #navbar animates its own padding on scroll — site-shell.mjs swaps
     * py-6 for py-4 over 300ms — so the edge the bar is supposed to hang from
     * moved 16px every time the reader started scrolling. An article is the one
     * page with something pinned directly beneath that edge, so it opts out and
     * the navbar keeps a single height. Padding only: the background, blur and
     * border it gains on scroll are untouched.
     *
     * Two: the bar used to start in the flow of the page, below a breadcrumb
     * row, and travel ~70px before position:sticky caught it. Giving .blog-article
     * a top padding equal to the offset the bar sticks at means it is already
     * stuck in the first painted frame and has nowhere to travel to. It stays in
     * flow, which is also what keeps it on the same rails as the article and the
     * site header without any viewport arithmetic — `left: calc((100vw - 80rem)/2)`
     * would have been off by half a scrollbar, since 100vw includes it and the
     * navbar's own centring does not.
     */
    body:has(.reader-bar) #navbar { padding-top: 1rem; padding-bottom: 1rem; }

    /* position:fixed, not sticky. A sticky element's offset is resolved against
       the scroll position on the main thread, so during a momentum scroll it is
       recomputed a frame behind the compositor and shimmers against the content
       moving past it — its box never moves, which is why measuring it frame by
       frame reports a constant top and the eye still sees it twitch. A fixed
       element is composited once and has nothing to resolve. */
    .reader-bar {
        position: fixed;
        left: 0;
        right: 0;
        top: var(--reader-sticky-offset, 4.625rem);
        bottom: auto;
        z-index: 30;
        width: auto;
        max-width: none;
        height: 3rem;
        margin-inline: 0;
        padding: 0;
        border-top: 0;
        border-right: 0;
        border-left: 0;
        border-bottom: 1px solid var(--rule);
        box-shadow: none;
        background: var(--stock);
    }
    /* Out of flow now, so the article reserves its own clearance: the navbar,
       the bar, and a gap. */
    .blog-article { padding-top: calc(var(--reader-sticky-offset, 4.625rem) + 3rem + 1.25rem); }

    .reader-bar-inner {
        position: relative;
        align-items: center;
        gap: 1rem;
        max-width: 80rem;
        margin: 0 auto;
        /* The same px-6 the navbar's own row uses, on both sides: the crumb then
           starts on the logo's rail and the last button's hover box ends on the
           Open App pill's, which is the pair of edges the eye actually reads. */
        padding: 0 1.5rem;
    }

    .reader-bar-context {
        display: flex;
        align-items: baseline;
        gap: 0.75rem;
        flex: 1;
        min-width: 0;
        font-size: 0.8125rem;
    }
    .reader-bar-crumb {
        color: var(--ink-3);
        white-space: nowrap;
        font-size: 0.625rem;
        font-weight: 600;
        letter-spacing: 0.12em;
        text-transform: uppercase;
    }
    .reader-bar-crumb b { font-weight: 600; color: var(--ink-2); }
    /* The crumb steps back once the bar is carrying the title as well, so the two
       do not read as one run-on line. */
    .reader-bar.is-past-title .reader-bar-crumb,
    .reader-bar.is-past-title .reader-bar-crumb b { color: var(--ink-3); }
    .reader-bar-title {
        font-weight: 600;
        color: var(--ink);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        opacity: 0;
        transition: opacity var(--transition-duration-base, 200ms) var(--ease);
    }
    /* Fades in exactly as the h1 it duplicates leaves the viewport. */
    .reader-bar.is-past-title .reader-bar-title { opacity: 1; }

    .reader-bar-actions { flex: none; gap: 0; align-self: stretch; }
    .reader-btn {
        flex: none;
        flex-direction: row;
        gap: 0.4rem;
        min-height: 0;
        padding: 0 0.7rem;
        font-size: 0.625rem;
        letter-spacing: 0.08em;
    }
    /* Up here the top edge is the navbar; the spot rule sits on the bottom. */
    .reader-btn::before { top: auto; bottom: -1px; }
    .reader-btn + .reader-btn,
    .reader-transport + .reader-btn { border-left: 1px solid var(--rule); }
    .reader-btn:first-child { border-left: 1px solid var(--rule); }
    .reader-btn:last-child { border-right: 1px solid var(--rule); }
    /* Save and Share go icon-only. Visually hidden rather than display:none —
       the span is still the button's accessible name, and blog-bookmarks.js
       rewrites it to "Saved", which a screen reader has to keep hearing. */
    .reader-btn--icon .reader-btn-label {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border-width: 0;
    }
    .reader-btn--icon { padding: 0 0.6rem; }
    .reader-btn:hover { background: var(--sheet); }
    .reader-btn[aria-expanded='true'],
    .reader-btn[aria-pressed='true'] { background: var(--sheet); }
    .reader-btn .blog-icon { width: 0.9375rem; height: 0.9375rem; }
}

/* -------------------------------------------------------- transport ----- */
/* Takes the slot the Listen button occupies, so the control the reader pressed
   becomes the control they now operate, in the same place. Below 1024px it takes
   the whole row — there is no room for both — and above it, only that one slot,
   so Save and Share stay reachable while something is playing. */
.reader-transport[hidden] { display: none; }
.reader-transport {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 0.25rem;
    min-width: 0;
    padding: 0 0.5rem;
}
body.reader-listening .reader-bar-actions > .reader-btn { display: none; }

.reader-tp-btn {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    border: 0;
    background: none;
    color: var(--ink);
    cursor: pointer;
    transition: background var(--transition-duration-fast, 150ms) var(--ease);
}
.reader-tp-btn:hover { background: var(--sheet-2); }
/* One button, two meanings, no icon swap in JS: the paused state is a class and
   the glyph follows it. */
.reader-tp-btn[data-tts-toggle].is-paused .blog-icon { display: none; }
.reader-tp-btn[data-tts-toggle].is-paused::before {
    content: '';
    width: 0;
    height: 0;
    border-left: 0.7rem solid currentColor;
    border-top: 0.45rem solid transparent;
    border-bottom: 0.45rem solid transparent;
}

/* A 1px rule for a track and a 3px spot fill: progress is a spot use. */
.reader-tp-meter {
    flex: 1;
    min-width: 2rem;
    height: 3px;
    background: linear-gradient(var(--ink-3), var(--ink-3)) center / 100% 1px no-repeat;
    overflow: hidden;
}
.reader-tp-meter i { display: block; height: 100%; width: 0; background: var(--spot); transition: width var(--transition-duration-base, 200ms) linear; }

.reader-tp-status {
    flex: none;
    font-size: 0.6875rem;
    font-weight: 500;
    color: var(--ink-3);
    white-space: nowrap;
}

@media (min-width: 1024px) {
    .reader-transport { flex: none; width: 16rem; padding: 0 0.5rem; border-left: 1px solid var(--rule); }
    .reader-tp-btn { width: 2rem; height: 2rem; }
    /* Only the Listen button steps aside up here. */
    body.reader-listening .reader-bar-actions > .reader-btn { display: inline-flex; }
    body.reader-listening .reader-bar-actions > .reader-btn[data-listen-toggle] { display: none; }
}

/* ----------------------------------------------------------- panels ----- */
/* Shared shell. Presentation is a sheet by default and becomes a popover or a
   column at 1024px, because the small screen is the one with less room to get
   wrong. */
/*
 * A modal panel has to sit above #navbar, which is fixed at z-index 50 — a
 * backdrop that leaves the site navigation undimmed and still clickable is not
 * a backdrop.
 *
 * Raising the panel's own z-index cannot do it: site-shell.mjs gives <main>
 * Tailwind's `isolate z-10`, which makes it a stacking context, so every
 * z-index inside it resolves against that context and can never beat a sibling
 * of it at any value.
 *
 * Lifting <main> above the navbar is the obvious next move and is wrong — it
 * takes the article up with it, so prose scrolls over the top of the nav
 * instead of under it, in the strip the scrim is meant to be dimming. The
 * navbar is hidden instead. It is inert and covered by a backdrop while a modal
 * is open, so there is nothing there to lose, and <main> stays where it is.
 */
body.reader-sheet-open #navbar { visibility: hidden; }

/*
 * The Display and Share sheets are rendered inside .reader-bar, because above
 * 1024px they are popovers anchored to the buttons that open them. The bar is
 * position:fixed, and a fixed element creates a stacking context whatever its
 * z-index — so those two panels' z-index:56 resolved INSIDE the bar and the
 * whole subtree painted at the bar's 40, underneath the scrim at 55. Everything
 * about them looked right and every tap landed on the backdrop instead of the
 * control, which then read as the sheet ignoring you.
 *
 * Lifting the bar above the scrim for the duration is what puts its panels
 * there too. It costs nothing visually: a sheet is anchored to the same bottom
 * edge and is taller than the bar, so the bar is completely covered the whole
 * time it is raised.
 *
 * 56 and not higher, deliberately. The Contents and Commands panels are NOT
 * inside the bar — they sit at 56 in the root stacking context — and the bar is
 * earlier in the DOM, so an equal z-index leaves them painting above it. At 57
 * the bar would win and start swallowing taps through the bottom of those two.
 */
@media (max-width: 1023px) {
    body.reader-sheet-open .reader-bar { z-index: 56; }
}

/* The sheet: stock ground, a rule on its top edge, and the upward shadow whose
   hairline is the load-bearing half. */
.reader-panel {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 56;
    max-height: 72vh;
    display: flex;
    flex-direction: column;
    padding: 0.5rem 0.75rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
    background: var(--stock);
    border-top: 1px solid var(--rule);
    box-shadow: var(--shadow-sheet);
    transform: translateY(101%);
    visibility: hidden;
    transition: transform var(--transition-duration-slow, 300ms) var(--ease), visibility 0s linear var(--transition-duration-slow, 300ms);
}
.reader-panel[data-open='true'] {
    transform: none;
    visibility: visible;
    transition: transform var(--transition-duration-slow, 300ms) var(--ease), visibility 0s;
}
/* The panel scrolls, not the page behind it: the body is pinned while a sheet is
   open, so this is the only thing left that can move. */
.reader-panel { overflow-y: auto; overscroll-behavior: contain; }

/* Named, and closable without knowing that a backdrop is tappable or that the
   handle can be dragged. Both of those work; neither is discoverable from a
   screen reader, and neither exists for a keyboard. */
.reader-panel-bar {
    flex: none;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.1rem 0.35rem 0.6rem;
    border-bottom: 1px solid var(--rule);
    margin-bottom: 0.35rem;
}
.reader-panel-heading {
    margin: 0;
    font-size: 0.8125rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink);
}
/* A 26px ruled square, like the sheet's own close control in the app. */
.reader-panel-close {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border: 1px solid var(--rule);
    background: none;
    color: var(--ink-3);
    cursor: pointer;
    transition: background var(--transition-duration-fast, 150ms) var(--ease), color var(--transition-duration-fast, 150ms) var(--ease);
}
.reader-panel-close:hover { background: var(--sheet-2); color: var(--ink); }

/* The grab handle is the affordance that says "drag me down": a bar, not a pill. */
.reader-panel::before {
    content: '';
    flex: none;
    width: 2.125rem;
    height: 3px;
    margin: 0.25rem auto 0.6rem;
    background: var(--ink-3);
}

.reader-scrim {
    position: fixed;
    inset: 0;
    z-index: 55;
    background: var(--scrim);
    opacity: 0;
    transition: opacity var(--transition-duration-base, 200ms) var(--ease);
}
.reader-scrim[hidden] { display: none; }
.reader-scrim.is-open { opacity: 1; }

.reader-panel-label { margin: 0.75rem 0 0.5rem; padding: 0 0.35rem; }
.reader-divider { border: 0; border-top: 1px solid var(--rule); margin: 0.75rem 0.35rem; }

/* Segmented preference rows — one shared border; the chosen setting inverts to
   ink. Settings are ink; only a live plate takes the spot. */
.reader-seg { display: flex; gap: 0; margin: 0 0.35rem; border: 1px solid var(--rule); background: var(--sheet); }
.reader-seg button {
    flex: 1;
    min-height: 2.5rem;
    padding: 0.4rem 0.25rem;
    border: 0;
    background: none;
    color: var(--ink-2);
    font-family: inherit;
    font-size: 0.625rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background var(--transition-duration-fast, 150ms) var(--ease), color var(--transition-duration-fast, 150ms) var(--ease);
}
.reader-seg button + button { border-left: 1px solid var(--rule); }
.reader-seg button:hover { background: var(--sheet-2); color: var(--ink); }
.reader-seg button[aria-pressed='true'] { background: var(--ink); color: var(--stock); }

.reader-reset {
    width: 100%;
    margin-top: 0.25rem;
    padding: 0.5rem;
    border: 0;
    background: none;
    color: var(--ink-3);
    font-family: inherit;
    font-size: 0.625rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    cursor: pointer;
}
.reader-reset:hover { color: var(--ink); }

/* Share panel internals. */
.reader-url {
    display: flex;
    align-items: stretch;
    gap: 0;
    margin: 0.35rem 0.35rem 0.6rem;
    padding: 0;
    border: 1px solid var(--rule);
    background: var(--sheet);
}
.reader-url-text {
    flex: 1;
    min-width: 0;
    align-self: center;
    padding: 0 0.7rem;
    font-size: 0.6875rem;
    color: var(--ink-2);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* The primary action of the panel: the spot as a fill. Pressed lays down more
   ink; done goes to the OK ink. */
.reader-copy {
    flex: none;
    min-height: 2.25rem;
    padding: 0.4rem 0.8rem;
    border: 0;
    border-left: 1px solid var(--rule);
    background: var(--spot);
    color: var(--spot-ink);
    font-family: inherit;
    font-size: 0.625rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background var(--transition-duration-base, 200ms) var(--ease);
}
.reader-copy:active { background: var(--spot-press); }
.reader-copy.is-done { background: var(--ok); color: var(--sheet); }

/* Rows: one ruled group, a soft rule between rows, more ink on hover. */
.reader-share-row { display: flex; flex-direction: column; margin: 0 0.35rem; border: 1px solid var(--rule); background: var(--sheet); }
.share-option {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    width: 100%;
    min-height: 2.75rem;
    padding: 0.5rem 0.75rem;
    border: 0;
    background: none;
    color: var(--ink);
    font-family: inherit;
    font-size: 0.8125rem;
    font-weight: 500;
    text-align: left;
    cursor: pointer;
    transition: background var(--transition-duration-fast, 150ms) var(--ease);
}
.share-option + .share-option { border-top: 1px solid var(--rule-soft); }
.share-option .blog-icon { color: var(--ink-2); }
.share-option:hover { background: var(--sheet-2); }
.share-option[hidden] { display: none; }
.reader-divider + .share-option { margin: 0 0.35rem; border: 1px solid var(--rule); background: var(--sheet); width: auto; }

/* Command menu. A sheet like the others below 1024px; a centred dialog above,
   because a bottom sheet on a wide monitor puts the thing you are typing into as
   far from your eyes as the layout allows. */
.reader-cmd-field {
    flex: none;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin: 0 0.35rem 0.5rem;
    padding: 0.6rem 0.8rem;
    border: 1px solid var(--rule);
    background: var(--sheet);
    color: var(--ink-3);
    transition: border-color var(--transition-duration-fast, 150ms) var(--ease);
}
.reader-cmd-field:focus-within { border-color: var(--spot); }
.reader-cmd-field input {
    flex: 1;
    min-width: 0;
    border: 0;
    background: none;
    outline: none;
    font-family: inherit;
    font-size: 1rem;
    color: var(--ink);
}
.reader-cmd-field input::placeholder { color: var(--ink-3); }

.reader-cmd-list { flex: 1; min-height: 0; overflow-y: auto; padding-bottom: 0.25rem; }
.reader-cmd-group { margin: 0.75rem 0 0.25rem; padding: 0 0.6rem; }
.reader-cmd-group[hidden] { display: none; }
.reader-cmd-item {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.7rem;
    width: 100%;
    min-height: 2.75rem;
    padding: 0.5rem 0.6rem 0.5rem 0.75rem;
    border: 0;
    background: none;
    color: var(--ink);
    font-family: inherit;
    font-size: 0.875rem;
    text-align: left;
    cursor: pointer;
}
.reader-cmd-item[hidden] { display: none; }
.reader-cmd-item .blog-icon { flex: none; width: 1rem; height: 1rem; color: var(--ink-2); }
/* The selected command is the live one: a 2px spot rule on its left edge. */
.reader-cmd-item::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: transparent; }
.reader-cmd-item:hover,
.reader-cmd-item.is-selected { background: var(--sheet-2); }
.reader-cmd-item.is-selected::before { background: var(--spot); }
.reader-cmd-empty { margin: 0.5rem 0.6rem 0.75rem; font-size: 0.8125rem; color: var(--ink-2); }
.reader-cmd-empty[hidden] { display: none; }

@media (min-width: 1024px) {
    .reader-panel--command {
        left: 50%;
        right: auto;
        bottom: auto;
        top: 12vh;
        width: min(34rem, calc(100vw - 3rem));
        max-height: 60vh;
        padding: 0.5rem;
        border: 1px solid var(--rule);
        background: var(--sheet);
        box-shadow: var(--shadow-lift);
        transform: translate(-50%, -0.5rem);
    }
    .reader-panel--command[data-open='true'] { transform: translate(-50%, 0); }
    .reader-panel--command::before { display: none; }
    /* It is a dialog at every width, so unlike the popovers it keeps its title
       bar and its close button up here. */
    .reader-panel--command .reader-panel-bar { display: flex; }
    .reader-panel--command .reader-cmd-field { background: var(--stock); }
}

/* Contents panel. */
.reader-panel-head { flex: none; padding: 0.35rem 0.6rem 0.75rem; }
.reader-readout { display: flex; align-items: baseline; gap: 0.35rem; margin: 0 0 0.5rem; }
.reader-readout b { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.02em; color: var(--ink); }
.reader-readout span { font-size: 0.6875rem; color: var(--ink-3); letter-spacing: 0.02em; }
/* Filled by blog-toc.js once it knows how many headings there were. Empty until
   then, and collapsed while it is, so the flex gap does not leave a hole. */
.reader-readout-sections:empty { display: none; }

.reader-pct {
    margin-left: auto;
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0;
    color: var(--ink-3);
}
/* 1px rule track, spot fill. */
.reader-meter { height: 3px; background: linear-gradient(var(--ink-3), var(--ink-3)) center / 100% 1px no-repeat; overflow: hidden; }
.reader-meter i { display: block; height: 100%; width: 0; background: var(--spot); transition: width var(--transition-duration-fast, 150ms) linear; }

.reader-panel-foot {
    flex: none;
    display: flex;
    flex-direction: column;
    gap: 0;
    margin-top: 0.5rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--rule);
}
.reader-panel-foot a {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    min-height: 2.5rem;
    padding: 0.4rem 0.6rem;
    font-size: 0.75rem;
    font-weight: 500;
    color: var(--ink-2);
    text-decoration: none;
    transition: background var(--transition-duration-fast, 150ms) var(--ease), color var(--transition-duration-fast, 150ms) var(--ease);
}
.reader-panel-foot a:hover { background: var(--sheet-2); color: var(--ink); }
.reader-panel-foot .blog-icon { width: 0.95rem; height: 0.95rem; }
.reader-panel-foot b {
    margin-left: auto;
    font-size: 0.5625rem;
    font-weight: 600;
    padding: 0.05rem 0.3rem;
    border: 1px solid var(--rule);
    color: var(--ink-3);
}

@media (min-width: 1024px) {
    /* Popovers: children of the bar, so they travel with it while it is stuck
       and never need repositioning on scroll. */
    .reader-panel--pop {
        position: absolute;
        left: auto;
        /* Offsets on an absolutely positioned child resolve against the
           containing block's PADDING box, so this has to repeat the rail's
           padding to land the popover's edge on the rail rather than 24px past
           it. */
        right: 1.5rem;
        bottom: auto;
        top: calc(100% + 0.5rem);
        width: 19rem;
        max-height: none;
        padding: 0.4rem;
        border: 1px solid var(--rule);
        background: var(--sheet);
        box-shadow: var(--shadow-lift);
        transform: translateY(-0.4rem) scale(0.98);
        opacity: 0;
        transition: opacity var(--transition-duration-fast, 150ms) var(--ease), transform var(--transition-duration-fast, 150ms) var(--ease), visibility 0s linear var(--transition-duration-fast, 150ms);
    }
    .reader-panel--pop[data-open='true'] { transform: none; opacity: 1; transition: opacity var(--transition-duration-fast, 150ms) var(--ease), transform var(--transition-duration-fast, 150ms) var(--ease), visibility 0s; }
    .reader-panel--pop::before { display: none; }
    /* A popover is not a dialog: it is dismissed by Esc, by clicking away, or by
       pressing its trigger again, and a title bar on top of four rows of controls
       is just noise. The sheet keeps both. */
    .reader-panel--pop .reader-panel-bar { display: none; }
    .reader-panel--pop .reader-url,
    .reader-panel--pop .reader-cmd-field { background: var(--stock); }
    .reader-panel--pop .reader-seg,
    .reader-panel--pop .reader-share-row,
    .reader-panel--pop .reader-divider + .share-option { background: var(--stock); }
    /* The contents column is not dismissible — the toolbar button collapses it —
       so it keeps its heading and loses the close button. */
    .reader-panel--contents .reader-panel-close { display: none; }
    .reader-panel--contents .reader-panel-bar { padding: 0 0.6rem 0.5rem; border-bottom: 0; margin-bottom: 0; }
    /* Belt and braces: the class is only ever set below this breakpoint, but a
       resize while it is applied should not leave the bar parked off-screen. */
    .reader-bar.is-hidden { transform: none; }

    /* Contents becomes a column. It is visible by default here — the toggle
       collapses it rather than revealing it — so the sheet transform, the sheet
       shadow and the scrim all have to be unwound. */
    /* A fixed height, not a max-height: the column is a flex box whose list takes
       the slack, and only a resolved height gives it any to take. That is what
       pins "Saved articles / All articles" to the bottom of the column instead of
       letting them ride up under a short table of contents. */
    .reader-panel--contents {
        position: sticky;
        top: calc(var(--reader-sticky-offset, 5rem) + 3.75rem);
        left: auto;
        right: auto;
        bottom: auto;
        z-index: 1;
        height: calc(100dvh - var(--reader-sticky-offset, 5rem) - 5.25rem);
        max-height: none;
        padding: 0;
        border: 0;
        background: none;
        box-shadow: none;
        transform: none;
        visibility: visible;
        overflow: hidden;
        transition: opacity var(--transition-duration-base, 200ms) var(--ease);
    }
    .reader-panel--contents::before { display: none; }
    .reader-pane-collapsed .reader-panel--contents { opacity: 0; pointer-events: none; }
    .reader-panel-head { padding: 0 0.6rem 0.85rem; }

    /* Up here the panel title is a column label rather than a dialog heading:
       a plate label with the rule running off its end. */
    .reader-panel--contents .reader-panel-heading {
        flex: 1;
        display: flex;
        align-items: center;
        gap: 0.625rem;
        font-size: 0.625rem;
        font-weight: 600;
        letter-spacing: 0.14em;
        color: var(--ink-3);
    }
    .reader-panel--contents .reader-panel-heading::after { content: ''; flex: 1; height: 1px; background: var(--rule); }
    .reader-panel--contents .reader-pct { margin-left: 0; }
}

/* ------------------------------------------------------------------ toc -- */
/* Sections are a sequence, so the top-level entries are numbered — in CSS,
   because blog-command.js builds the ⌘K list from each link's textContent and
   a number in the markup would leak into it. */
.toc { flex: 1; min-height: 0; overflow-y: auto; padding: 0.25rem 0; }
.toc ol { list-style: none; margin: 0; padding: 0; }
.toc > ol { counter-reset: toc; }
.toc > ol > li { counter-increment: toc; }
.toc ol ol { margin: 0.1rem 0 0.2rem; padding-left: 1.85rem; }
.toc li { margin: 0; }
.toc a {
    position: relative;
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    padding: 0.5rem 0.6rem 0.5rem 0.85rem;
    font-size: 0.8125rem;
    line-height: 1.4;
    font-weight: 500;
    color: var(--ink-3);
    text-decoration: none;
    transition: color var(--transition-duration-fast, 150ms) var(--ease), background var(--transition-duration-fast, 150ms) var(--ease);
}
.toc > ol > li > a::before {
    content: counter(toc, decimal-leading-zero);
    flex: none;
    font-family: var(--font-mono);
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0;
    color: var(--ink-3);
    font-variant-numeric: tabular-nums;
}
/* The spine: a rule per section that fills in behind you. It answers "how far
   in am I" without a second progress bar competing with the one on the bar. */
.toc a::after {
    content: '';
    position: absolute;
    left: 0;
    top: 0.3rem;
    bottom: 0.3rem;
    width: 2px;
    background: var(--rule);
    transition: background var(--transition-duration-slow, 300ms) var(--ease);
}
.toc a:hover { color: var(--ink); background: var(--sheet); }
.toc a.is-read::after { background: var(--ink-3); }
.toc a.active { color: var(--spot-text); font-weight: 600; background: var(--sheet); }
.toc a.active::before { color: var(--spot-text); }
.toc a.active::after { background: var(--spot); }
.toc ol ol a { font-size: 0.78125rem; font-weight: 400; padding-top: 0.35rem; padding-bottom: 0.35rem; }

@media (max-width: 1023px) {
    /* Touch targets, and enough of the hierarchy visible at once that the panel
       is worth opening. The horizontal chip rail this replaces showed about two
       and a half h2s and no h3s at all. */
    .toc a { font-size: 0.9375rem; min-height: 2.75rem; display: flex; align-items: center; }
    .toc ol ol a { font-size: 0.875rem; }
}

/* ------------------------------------------------ reading preferences --- */
:root { --reader-size: 1.1875rem; --reader-measure: 52rem; }
[data-reader-size='s'] { --reader-size: 1.0625rem; }
[data-reader-size='l'] { --reader-size: 1.3125rem; }
[data-reader-size='xl'] { --reader-size: 1.5rem; }

/* Centred in its own column rather than pinned to the left of it. The measure is
   narrower than the space beside the panel, and left-aligning leaves the article
   hugging the panel with a third of the screen empty to its right. */
.main-content {
    max-width: var(--reader-measure);
    margin-inline: auto;
    transition: max-width var(--transition-duration-base, 200ms) var(--ease);
}

/*
 * Serif body copy. Scoped through the id, not the class: .interior-prose p and
 * .article-content p are both (0,1,1), so blog.css only wins by source order —
 * which nothing asserts, and which would flip the day someone reorders the two
 * <link> tags. #blog-content.article-content p is (1,2,1) and cannot lose.
 *
 * .interior-prose stays untouched: it is shared by forty tool and marketing
 * pages that are not long-form reading. Every colour, radius and face it sets
 * is overridden below at the id's specificity.
 */
#blog-content.article-content {
    max-width: none;
    font-family: var(--font-read);
    font-size: var(--reader-size);
    line-height: 1.72;
    color: var(--ink);
}
#blog-content.article-content p,
#blog-content.article-content li { font-size: inherit; line-height: 1.72; color: var(--ink); }
#blog-content.article-content strong { font-weight: 600; color: var(--ink); }
#blog-content.article-content h2,
#blog-content.article-content h3,
#blog-content.article-content h4 {
    font-family: var(--font-ui);
    font-weight: 700;
    font-variation-settings: 'wdth' 108;
    letter-spacing: -0.02em;
    color: var(--ink);
}
/* A rule above every section: the print device that also does the table of
   contents' work on the page itself. */
#blog-content.article-content h2 { font-size: 1.625rem; line-height: 1.15; margin-top: 2.75rem; padding-top: 1.25rem; border-top: 1px solid var(--rule); }
#blog-content.article-content h3 { font-size: 1.1875rem; line-height: 1.25; }
#blog-content.article-content h4 { font-size: 1rem; }
#blog-content.article-content a {
    color: var(--spot-text);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.15em;
}
#blog-content.article-content a:hover { color: var(--ink); }
/* Tables and code stay in the UI face — they are scanned, not read. */
#blog-content.article-content table,
#blog-content.article-content code,
#blog-content.article-content pre { font-size: 0.875rem; }
#blog-content.article-content table { font-family: var(--font-ui); font-size: 0.8125rem; }
#blog-content.article-content .table-scroll { border: 1px solid var(--rule); border-radius: 0; }
#blog-content.article-content thead { background: var(--sheet-2); }
#blog-content.article-content th,
#blog-content.article-content td { border-bottom: 1px solid var(--rule-soft); }
#blog-content.article-content th {
    font-size: 0.625rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-2);
}
#blog-content.article-content td { color: var(--ink); font-variant-numeric: tabular-nums; }
#blog-content.article-content caption {
    caption-side: bottom;
    padding: 0.5rem 0;
    text-align: left;
    font-family: var(--font-ui);
    font-size: 0.625rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-3);
}
#blog-content.article-content code {
    font-family: var(--font-mono);
    font-size: 0.875em;
    padding: 0.1rem 0.35rem;
    border: 1px solid var(--rule-soft);
    border-radius: 0;
    background: var(--sheet-2);
    color: var(--ink);
}
/* A code block is an ink plate on stock; on the proofing table it is a sheet. */
#blog-content.article-content pre {
    font-family: var(--font-mono);
    font-size: 0.8125rem;
    border: 0;
    border-radius: 0;
    background: var(--ink);
    color: var(--stock);
}
#blog-content.article-content pre code { padding: 0; border: 0; background: transparent; color: inherit; font-size: 0.8125rem; }
.dark #blog-content.article-content pre { background: var(--sheet); color: var(--ink); border: 1px solid var(--rule); }
#blog-content.article-content hr { border: 0; border-top: 1px solid var(--rule); margin: 2.5rem 0; }
#blog-content.article-content small { color: var(--ink-3); }

/* --------------------------------------------------------- back to top -- */
/* Both nodes are created by blog-top.js, which is why neither appears in any
   template. The sentinel's height IS the trigger distance: a screen and a half,
   so the button never turns up while the top of the page is still visible. */
.blog-top-sentinel {
    position: absolute;
    top: 0;
    left: 0;
    width: 1px;
    height: 150vh;
    pointer-events: none;
}

/* A ruled square on a sheet, floating: the one permitted elevation. */
.blog-top {
    position: fixed;
    right: 1.25rem;
    bottom: 1.25rem;
    /* Above the reader bar, below the scrim and the panels it dims — though it
       is hidden outright while a sheet is open, so that ordering never shows. */
    z-index: 45;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    border: 1px solid var(--rule);
    background: var(--sheet);
    color: var(--ink-2);
    cursor: pointer;
    box-shadow: var(--shadow-lift);
    opacity: 0;
    visibility: hidden;
    transform: translateY(0.5rem);
    transition:
        opacity var(--transition-duration-base, 200ms) var(--ease),
        transform var(--transition-duration-base, 200ms) var(--ease),
        border-color var(--transition-duration-fast, 150ms) var(--ease),
        color var(--transition-duration-fast, 150ms) var(--ease),
        visibility 0s linear var(--transition-duration-base, 200ms);
}
.blog-top:hover { color: var(--ink); border-color: var(--ink); }
.blog-top .blog-icon { width: 1.125rem; height: 1.125rem; }
.blog-top.is-visible {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition:
        opacity var(--transition-duration-base, 200ms) var(--ease),
        transform var(--transition-duration-base, 200ms) var(--ease),
        border-color var(--transition-duration-fast, 150ms) var(--ease),
        color var(--transition-duration-fast, 150ms) var(--ease),
        visibility 0s;
}

/* A sheet dims the page behind it; a button floating over that dimming has not
   been dimmed. */
body.reader-sheet-open .blog-top { opacity: 0; visibility: hidden; pointer-events: none; }

@media (max-width: 1023px) {
    /* Clear of the bar in the thumb zone. */
    body:has(.reader-bar) .blog-top { bottom: calc(4.5rem + env(safe-area-inset-bottom, 0px)); right: 1rem; }
    /* The toast is full-width down here and lands in the same band. It is on
       screen for three seconds after an action the reader just took, and it is
       the one carrying an Undo, so the button is what steps aside. */
    body:has(.copy-toast.show) .blog-top { opacity: 0; pointer-events: none; }
}

/* -------------------------------------------------------- focus pill ---- */
/* Rendered by the template and shown by .focus-boot as well as body.focus-mode,
   so a persisted focus session never paints a page with no visible way out. */
.focus-pill {
    display: none;
    position: fixed;
    right: 1.25rem;
    bottom: 1.25rem;
    z-index: 60;
    align-items: center;
    gap: 0.75rem;
    padding: 0.4rem 0.4rem 0.4rem 0.9rem;
    border: 1px solid var(--rule);
    background: var(--sheet);
    box-shadow: var(--shadow-lift);
    font-size: 0.625rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-2);
}
/* Focus mode is the live plate: a 6px spot square. */
.focus-pill-state::before {
    content: '';
    display: inline-block;
    width: 6px;
    height: 6px;
    margin-right: 0.55rem;
    background: var(--spot);
    vertical-align: middle;
}
.focus-pill-exit {
    min-height: 1.9rem;
    padding: 0.3rem 0.65rem;
    border: 1px solid var(--rule);
    background: none;
    color: var(--ink-2);
    font-family: inherit;
    font-size: 0.625rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    cursor: pointer;
}
.focus-pill-exit:hover { color: var(--ink); border-color: var(--ink); }

/* --------------------------------------------------- article body extras - */
/* A sheet plane with registration marks (see .blog-feature). It sits directly
   under the byline and above the first paragraph, and a block of brand colour
   there reads as an advert for the article rather than as the top of it. The
   points are set in the body serif because they are the article in miniature. */
.tldr-box {
    position: relative;
    border: 1px solid var(--rule);
    background: var(--sheet);
    padding: 1.25rem 1.5rem 1.4rem;
    margin-bottom: 2rem;
}
.tldr-title { margin: 0 0 0.9rem; }
.tldr-points { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.7rem; }
.tldr-points li {
    display: flex;
    gap: 0.75rem;
    font-family: var(--font-read);
    font-size: 1.03125rem;
    line-height: 1.55;
    color: var(--ink);
}
/* A 6px ink square, not a spot dot. */
.tldr-points li::before { content: ''; flex: none; width: 6px; height: 6px; margin-top: 0.6rem; background: var(--ink); }
.tldr-points strong { color: var(--ink); font-weight: 600; }
.tldr-points code { font-family: var(--font-mono); font-size: 0.8125em; padding: 0.05rem 0.3rem; border: 1px solid var(--rule-soft); background: var(--sheet-2); }

/* .article-content also carries .interior-prose, so typography, tables, code
   and their dark variants all come from landing.built.css. The id-scoped rules
   above override the colours; only geometry deltas here. */
.article-content { max-width: none; }
.article-content > :first-child { margin-top: 0; padding-top: 0; border-top: 0; }
/* A hairline frame on artwork. outline, not box-shadow: an <img> is a replaced
   element and an inset shadow paints under its pixels. No <figure> in the
   sanitize allowlist, so there is nothing to wrap. */
.article-content img { max-width: 100%; height: auto; outline: 1px solid var(--rule); outline-offset: -1px; }
/* Anything the browser scrolls to itself — a #hash on load or a native
   find-in-page jump — gets a first-pass clearance here. blog-toc.js then aligns
   an initial hash from the live navbar + bar geometry once layout settles. */
.article-content h2,
.article-content h3,
[data-toc-section] h2,
.post-keep-working h2,
.post-sources h2,
.post-related h2 { scroll-margin-top: calc(var(--reader-sticky-offset, calc(var(--nav-clearance) - 2rem)) + 3.5rem); }
.article-content blockquote {
    border-left: 2px solid var(--ink);
    padding-left: 1rem;
    margin: 1.5rem 0;
    font-style: italic;
    color: var(--ink-2);
}
.auto-link { font-weight: 500; }

/* A real control, at every input type. The dotted underline used to invite a tap
   that did nothing: the bubble was a CSS ::after revealed on :hover, and
   `@media (hover: none)` hid it outright, so touch — most of the traffic — got
   an affordance with no behaviour behind it. The text stays ink; the rule
   under it is what takes the spot when the term is live. */
.tooltip-term {
    border-bottom: 1px dotted var(--ink-3);
    cursor: help;
    /* An inline span, not a button: buttons do not wrap reliably mid-line, and
       "Open Graph" sitting at a line break in a 320px column would either
       overflow or leave a hole. */
}
.tooltip-term:hover,
.tooltip-term[aria-expanded='true'] { border-bottom-style: solid; border-bottom-color: var(--spot); }

/* position:fixed on <body>, so it contributes nothing to document overflow and
   can be clamped inside the viewport wherever the term happens to fall.
   blog-tooltips.js sets left/top; everything here is presentation. An ink
   plate; a sheet on the proofing table. */
.term-popover {
    position: fixed;
    left: 0;
    top: 0;
    z-index: 70;
    max-width: min(18rem, calc(100vw - 1.5rem));
    padding: 0.55rem 0.75rem;
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    font-weight: 400;
    font-style: normal;
    line-height: 1.5;
    color: var(--stock);
    background: var(--ink);
    box-shadow: var(--shadow-lift);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity var(--transition-duration-fast, 150ms) var(--ease), visibility 0s linear var(--transition-duration-fast, 150ms);
}
.dark .term-popover { color: var(--ink); background: var(--sheet); border: 1px solid var(--rule); }
.term-popover.is-open {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity var(--transition-duration-fast, 150ms) var(--ease), visibility 0s;
}

/* ------------------------------------------------------------------ faq -- */
/* Rules, not cards. The FAQ is the last stretch of the article and boxing every
   question turns eight answers into eight objects to get past. */
.faq-section { margin-top: 3rem; }
.faq-section h2 {
    font-family: var(--font-ui);
    font-size: 1.625rem;
    font-weight: 700;
    font-variation-settings: 'wdth' 108;
    letter-spacing: -0.02em;
    line-height: 1.15;
    color: var(--ink);
    margin: 0 0 0.75rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--rule);
}
.faq-accordion { display: flex; flex-direction: column; }
/* Native <details> deliberately: LogoNuri's shell script binds .faq-btn globally,
   so a button-based accordion here would be double-bound. */
.faq-item { border-bottom: 1px solid var(--rule); }
.faq-item summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    font-family: var(--font-ui);
    font-size: 0.9375rem;
    font-weight: 600;
    color: var(--ink);
    padding: 0.95rem 0.25rem 0.95rem 0;
    cursor: pointer;
    list-style: none;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
    content: '';
    flex: none;
    width: 0.5rem;
    height: 0.5rem;
    border-right: 1px solid var(--ink-3);
    border-bottom: 1px solid var(--ink-3);
    transform: rotate(45deg) translate(-2px, -2px);
    transition: transform var(--transition-duration-fast, 150ms) var(--ease);
}
.faq-item[open] summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.faq-answer {
    padding: 0 2rem 1.1rem 0;
    font-family: var(--font-read);
    font-size: 1.03125rem;
    line-height: 1.65;
    color: var(--ink);
}
.faq-answer p:last-child { margin-bottom: 0; }

/* ------------------------------------------------------------- endcaps -- */
/* Everything after the article proper: the tools it points at, its sources, and
   what to read next. One rule above each and a plate label for a heading, so
   they read as a footer to the piece rather than as three more sections of it. */
.post-endcap { margin-top: 2.75rem; padding-top: 1.4rem; border-top: 1px solid var(--rule); }
.post-endcap h2 { margin: 0 0 1rem; }

/* The tools are the article's primary action; the glyph carries the spot. */
.post-tool-row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.post-tool-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.6rem 0.85rem;
    border: 1px solid var(--rule);
    background: var(--sheet);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink);
    text-decoration: none;
    transition: border-color var(--transition-duration-fast, 150ms) var(--ease), background var(--transition-duration-fast, 150ms) var(--ease);
}
.post-tool-chip:hover { border-color: var(--ink); background: var(--sheet-2); }
.post-tool-chip .blog-icon { width: 0.9375rem; height: 0.9375rem; color: var(--spot-text); }

.post-sources ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.post-sources a {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-size: 0.84375rem;
    color: var(--ink-2);
    text-decoration: underline;
    text-decoration-color: transparent;
    text-underline-offset: 0.2em;
    transition: color var(--transition-duration-fast, 150ms) var(--ease), text-decoration-color var(--transition-duration-fast, 150ms) var(--ease);
}
.post-sources a:hover { color: var(--ink); text-decoration-color: currentColor; }
.post-sources .blog-icon { width: 0.8125rem; height: 0.8125rem; color: var(--ink-3); }

/* -------------------------------------------------------------- related -- */
/* Text only. These are the last two things on the page and they were competing
   with the article's own artwork for the same attention; the category, the title
   and the length are what decides whether either gets clicked. A tiles grid:
   one border, one hairline between cells. */
.post-related-grid { display: grid; gap: 1px; border: 1px solid var(--rule); background: var(--rule); }
@media (min-width: 640px) { .post-related-grid { grid-template-columns: repeat(2, 1fr); } }

.post-related-card {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    padding: 0.9rem 1rem 1rem;
    background: var(--sheet);
    text-decoration: none;
    transition: background var(--transition-duration-fast, 150ms) var(--ease);
}
.post-related-card:hover { background: var(--sheet-2); }
.post-related-title { font-size: 0.9375rem; font-weight: 600; line-height: 1.3; letter-spacing: -0.01em; color: var(--ink); }
.post-related-date { font-size: 0.6875rem; color: var(--ink-3); }

/* ===================================================== focus reading mode = */
/*
 * One hide list, two triggers.
 *
 * `.focus-boot` lands on <html> from the head script before <body> exists, so a
 * persisted focus session never flashes the full chrome; `.focus-mode` lands on
 * <body> once blog-actions.js runs and takes over. The two must hide exactly the
 * same chrome, or the difference is what flashes — which is what used to happen:
 * the boot list omitted the quick actions and the related grid, so both were
 * visible for a frame on every reload of a focused article.
 *
 * :is() is what stops them drifting apart again. landing.built.css already ships
 * 85 of them, so it is not a new browser requirement.
 */
:is(.focus-boot, body.focus-mode) #navbar,
:is(.focus-boot, body.focus-mode) .interior-breadcrumbs,
:is(.focus-boot, body.focus-mode) .reader-bar,
:is(.focus-boot, body.focus-mode) .reader-panel,
:is(.focus-boot, body.focus-mode) .post-related,
:is(.focus-boot, body.focus-mode) .post-share-rail,
:is(.focus-boot, body.focus-mode) .blog-cta-band,
:is(.focus-boot, body.focus-mode) .blog-top,
:is(.focus-boot, body.focus-mode) footer { display: none; }

/* The pill is the whole point of the redesign here: focus mode used to persist
   across every future article with nothing on screen saying so. It is shown
   during boot as well, so the window between first paint and blog-actions.js is
   never a page with no navigation and no way out. */
:is(.focus-boot, body.focus-mode) .focus-pill { display: flex; }

/* Layout, also shared. Collapsing the panel does not reclaim its grid track —
   tracks are declared by grid-template-columns, not by their children — so
   without these the booted article sits in a narrower, off-centre column until
   blog-actions.js lands. */
:is(.focus-boot, body.focus-mode) .blog-article-body { display: block; }
:is(.focus-boot, body.focus-mode) .main-content { max-width: var(--reader-measure); margin: 0 auto; }
/* Focus mode is one column, so the masthead comes in off the rail and onto the
   measure with everything else — a full-width headline over a centred article is
   the two-margin problem the rest of the page is arranged to avoid. */
:is(.focus-boot, body.focus-mode) .post-header { max-width: var(--reader-measure); margin-inline: auto; }
/* The lead image needs no rule of its own: it is inside .main-content, which is
   already on the measure. */
:is(.focus-boot, body.focus-mode) .blog-article { padding-top: 3rem; padding-bottom: 4rem; }


/* ============================================================== feedback = */
/* An ink plate, uppercase, squared. */
.copy-toast {
    position: fixed;
    left: 50%;
    bottom: 1.5rem;
    z-index: 80;
    transform: translate(-50%, 1rem);
    font-size: 0.625rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--stock);
    background: var(--ink);
    padding: 0.7rem 1rem;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--transition-duration-base, 200ms) var(--ease), transform var(--transition-duration-base, 200ms) var(--ease);
}
.copy-toast.show { opacity: 1; transform: translate(-50%, 0); }
.dark .copy-toast { color: var(--ink); background: var(--sheet); border: 1px solid var(--rule); }

/* An actionable toast has to be clickable, which the base rule is not. Only
   raised for the variant that actually carries an action, so a plain
   confirmation still cannot swallow a click meant for the article underneath. */
.copy-toast { display: flex; align-items: center; gap: 0.85rem; }
.copy-toast:has(.copy-toast-action) { pointer-events: auto; padding-right: 0.4rem; }
.copy-toast-action {
    flex: none;
    min-height: 1.9rem;
    padding: 0.3rem 0.6rem;
    border: 1px solid currentColor;
    background: none;
    color: inherit;
    font-family: inherit;
    font-size: 0.625rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
    opacity: 0.75;
    transition: opacity var(--transition-duration-fast, 150ms) var(--ease);
}
.copy-toast-action:hover { opacity: 1; }

/* The bar's Listen button fetches a script and then waits on the browser's
   asynchronous voice list. Silence there reads as a dead button, which is why
   people press it twice. A stepped blink — mechanical, not a fade. */
.reader-btn.is-loading { cursor: progress; }
.reader-btn.is-loading .blog-icon { animation: reader-blink 1.2s steps(1) infinite; }
@keyframes reader-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0.25; } }
@media (prefers-reduced-motion: reduce) {
    .reader-btn.is-loading .blog-icon { animation: none; opacity: 0.5; }
}

/* Below 1024px the bar is fixed to the bottom, so a bottom-anchored toast would
   land on top of it. */
@media (max-width: 1023px) {
    .copy-toast { bottom: calc(4.25rem + env(safe-area-inset-bottom, 0px)); left: 1rem; right: 1rem; transform: translateY(1rem); }
    .copy-toast.show { transform: none; }
}

/* Off-screen scratch field for the execCommand('copy') fallback path. It has to
   be focusable and selectable, so it cannot be display:none or visibility:hidden. */
.copy-fallback { position: fixed; top: 0; left: -9999px; opacity: 0; }

/* ========================================================= listen player = */
/* The word being read is the live plate. */
.tts-active { background: var(--spot-wash-strong); }

/* ====================================================== reader palettes = */
/* Blog-only reading palettes deliberately do not add/remove .dark or touch
   localStorage.theme. The site navbar keeps the site theme; the document, its
   toolbar and its panels use this independent, validated preference.

   Each palette is a paper stock: it re-grounds the same tokens every reading
   component already reads. The variables live on #main-content (and explicitly
   on its body-mounted toast/popover) so the shared navbar, CTA band and footer
   continue to inherit the site theme from <html>. */
html.press-blog[data-reader-palette='light'] #main-content,
html.press-blog[data-reader-palette='light'] .copy-toast,
html.press-blog[data-reader-palette='light'] .term-popover {
    color-scheme: light;
    --stock: #FAF9F6;
    --sheet: #FFFFFF;
    --sheet-2: #F1EEE8;
    --ink: #17161B;
    --ink-2: #464351;
    --ink-3: #69655E;
    --rule: rgba(23, 22, 27, 0.14);
    --rule-soft: rgba(23, 22, 27, 0.08);
    --spot: #2563EB;
    --spot-text: #1D4ED8;
    --spot-press: #1D4ED8;
    --spot-ink: #FFFFFF;
    --spot-wash: rgba(37, 99, 235, 0.06);
    --spot-wash-strong: rgba(37, 99, 235, 0.18);
    --ok: #1E6F52;
    --ok-wash: #E4F0EA;
    --hold: #A8700C;
    --hold-text: #8A5A08;
    --stop: #B3261E;
    --stop-wash: #FBE9E7;
    --scrim: rgba(23, 22, 27, 0.14);
    --shadow-lift: 0 1px 0 var(--rule-soft), 0 6px 18px rgba(23, 22, 27, 0.07);
    --shadow-sheet: 0 -1px 0 var(--rule), 0 -18px 40px rgba(23, 22, 27, 0.10);
}
/* Sepia is a different stock, and the one palette allowed its own literals. The
   ground stays exactly #F6F0E3: smoke-blog asserts rgb(246, 240, 227). */
html.press-blog[data-reader-palette='sepia'] #main-content,
html.press-blog[data-reader-palette='sepia'] .copy-toast,
html.press-blog[data-reader-palette='sepia'] .term-popover { /* literal-ok */
    color-scheme: light;
    --stock: #F6F0E3;
    --sheet: #FCF7EB;
    --sheet-2: #EFE6D2;
    --ink: #352B20;
    --ink-2: #5C4F3F;
    --ink-3: #6E6150;
    --rule: rgba(53, 43, 32, 0.16);
    --rule-soft: rgba(53, 43, 32, 0.09);
    --spot: #8A4B20;
    --spot-text: #7A4119;
    --spot-press: #6A3813;
    --spot-ink: #FCF7EB;
    --spot-wash: rgba(138, 75, 32, 0.08);
    --spot-wash-strong: rgba(138, 75, 32, 0.2);
    --ok: #1E6F52;
    --ok-wash: #E4F0EA;
    --hold: #A8700C;
    --hold-text: #8A5A08;
    --stop: #B3261E;
    --stop-wash: #FBE9E7;
    --scrim: rgba(53, 43, 32, 0.16);
    --shadow-lift: 0 1px 0 var(--rule-soft), 0 6px 18px rgba(53, 43, 32, 0.08);
    --shadow-sheet: 0 -1px 0 var(--rule), 0 -18px 40px rgba(53, 43, 32, 0.12);
}
html.press-blog[data-reader-palette='dark'] #main-content,
html.press-blog[data-reader-palette='dark'] .copy-toast,
html.press-blog[data-reader-palette='dark'] .term-popover {
    color-scheme: dark;
    --stock: #131218;
    --sheet: #1D1B23;
    --sheet-2: #232128;
    --ink: #F2F0F4;
    --ink-2: #BFBCC8;
    --ink-3: #8A8695;
    --rule: rgba(242, 240, 244, 0.16);
    --rule-soft: rgba(242, 240, 244, 0.09);
    --spot: #6FA3FF;
    --spot-text: #6FA3FF;
    --spot-press: #8DB6FF;
    --spot-ink: #0B1220;
    --spot-wash: rgba(111, 163, 255, 0.09);
    --spot-wash-strong: rgba(111, 163, 255, 0.24);
    --ok: #4FD1A0;
    --ok-wash: #16302A;
    --hold: #F0B440;
    --hold-text: #F0B440;
    --stop: #FF7A6E;
    --stop-wash: #331B1B;
    --scrim: rgba(0, 0, 0, 0.5);
    --shadow-lift: 0 1px 0 var(--rule-soft), 0 6px 18px rgba(0, 0, 0, 0.34);
    --shadow-sheet: 0 -1px 0 var(--rule), 0 -18px 40px rgba(0, 0, 0, 0.5);
}

/* The palette re-grounds only the reading surface. Shared site chrome remains
   on the site theme even when the reader explicitly chooses another stock. */
html.press-blog[data-reader-palette] #main-content { background-color: var(--stock); color: var(--ink); }
/* The site theme's few structural dark rules (a sheet where light has an ink
   plate) follow the palette, not .dark, once a palette is set. */
html[data-reader-palette='light'] #blog-content.article-content pre,
html[data-reader-palette='sepia'] #blog-content.article-content pre { background: var(--ink); color: var(--stock); border: 0; }
html[data-reader-palette='dark'] #blog-content.article-content pre { background: var(--sheet); color: var(--ink); border: 1px solid var(--rule); }
html[data-reader-palette='light'] .term-popover,
html[data-reader-palette='sepia'] .term-popover,
html[data-reader-palette='light'] .copy-toast,
html[data-reader-palette='sepia'] .copy-toast { color: var(--stock); background: var(--ink); border: 0; }
html[data-reader-palette='dark'] .term-popover,
html[data-reader-palette='dark'] .copy-toast { color: var(--ink); background: var(--sheet); border: 1px solid var(--rule); }
@media (min-width: 1024px) {
    html[data-reader-palette] .reader-panel--contents { background: transparent; }
}

/* ============================================================ saved page = */
.saved-page { padding-bottom: 3rem; }
.saved-toolbar { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-bottom: 1.5rem; }
.saved-search,
.saved-sort select {
    font-family: inherit;
    font-size: 0.875rem;
    color: var(--ink);
    background: var(--sheet);
    border: 1px solid var(--rule);
    border-radius: 0;
    padding: 0.55rem 0.8rem;
    outline: none;
}
.saved-search::placeholder { color: var(--ink-3); }
.saved-search { flex: 1 1 16rem; }
.saved-sort select { font-size: 0.75rem; font-weight: 600; }
.saved-search:focus-visible,
.saved-sort select:focus-visible { outline: none; border-color: var(--spot); }

/* A tiles grid: one border, one hairline between cells. */
.saved-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 1px; border: 1px solid var(--rule); background: var(--rule); }
.saved-list:empty { border: 0; background: none; }
@media (min-width: 768px) { .saved-list { grid-template-columns: repeat(2, 1fr); } }
.saved-card {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    background: var(--sheet);
    padding: 1.1rem;
}
.saved-card-title { font-size: 0.9375rem; font-weight: 600; color: var(--ink); text-decoration: none; }
.saved-card-title:hover { text-decoration: underline; text-underline-offset: 0.15em; }
.saved-card-desc { font-size: 0.8125rem; line-height: 1.6; color: var(--ink-2); }
.saved-meta { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; margin-top: auto; padding-top: 0.5rem; font-size: 0.6875rem; color: var(--ink-3); }
/* Remove is the stop action: ruled in the stop ink, never filled. */
.saved-remove {
    font-family: inherit;
    font-size: 0.5625rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--stop);
    background: transparent;
    border: 1px solid var(--stop);
    cursor: pointer;
    padding: 0.25rem 0.5rem;
}
.saved-remove:hover { background: var(--stop-wash); }
.saved-empty {
    grid-column: 1 / -1;
    padding: 2.5rem 1rem;
    text-align: center;
    background: var(--sheet);
    font-size: 0.6875rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-3);
}

/* ================================================================ print = */
.print-header,
.print-footer { display: none; }

/* Nothing here is gated by a test, and it rots the moment an element is renamed
   without anyone noticing — `.post-share` sat in this list for months naming a
   class that never existed. When reading chrome is added, add it here too.
   Paper is paper: the greys below are print literals, not tokens. */
@media print { /* literal-ok */
    #navbar,
    .interior-breadcrumbs,
    .reader-bar,
    .reader-panel,
    .reader-scrim,
    .term-popover,
    .focus-pill,
    .blog-cta-band,
    .blog-top,
    .copy-toast,
    .post-related,
    .post-share-rail,
    footer,
    .skip-link { display: none !important; }

    .blog-article { padding-top: 0; padding-bottom: 0; }
    .blog-article-body { display: block; }
    .main-content { max-width: none; }
    .article-content { max-width: none; color: #000; }
    .article-content a { text-decoration: underline; }
    /* Paper has no preference, and the reader's on-screen size choice should not
       decide how many pages come out of the printer. */
    #blog-content.article-content { font-size: 11pt; line-height: 1.5; }
    /* An ink plate does not print; a code block becomes a ruled box. */
    #blog-content.article-content pre { background: none; color: #000; border: 1px solid #999; }

    .print-header,
    .print-footer { display: block; font-size: 10pt; color: #111; }
    .print-header { border-bottom: 1px solid #999; padding-bottom: 0.4rem; margin-bottom: 1rem; }
    .print-footer { border-top: 1px solid #999; padding-top: 0.6rem; margin-top: 1.5rem; display: flex; align-items: center; gap: 1rem; }
    .print-url { word-break: break-all; }
    /* A long URL encodes to a version-9 symbol (49 modules). At 128px that is
       ~0.55mm per module in print, which is around the practical floor for a
       phone camera — do not shrink this. */
    .print-qr { flex: none; width: 128px; height: 128px; }
    .print-qr-label { font-size: 9pt; color: #444; }

    .tldr-box { border: 1px solid #999; background: none; box-shadow: none; }
    .tldr-box::before,
    .tldr-box::after { display: none; }
    .post-header { border-bottom: 1px solid #999; }
    .post-endcap { border-top: 1px solid #999; }
    .post-tool-chip { border: 1px solid #999; }
    .faq-item { border-bottom: 1px solid #999; }
    .faq-answer { display: block !important; }
}

/* ======================================================= reduced motion = */
/* Durations are no longer listed here. Every transition above now reads
   --transition-duration-*, and tokens.css already clamps all three to 50ms
   under this same query, so the blog finally gets the site-wide treatment
   instead of a hand-maintained copy that had to be extended by hand for every
   new selector.

   Nothing on this page moves on hover any more — planes change ink, not
   position — so the only displacement left to neutralise is the entry motion
   of the floating button. (The footer's stepped blink is the shell's, and
   tailwind.landing.css stops it under the same query.) */
@media (prefers-reduced-motion: reduce) {
    .blog-top { transform: none; }
}
