/* =========================================================================
   LazyBlog — post + series
   Home/tag post list, post page, post-figure, post-body code blocks,
   floating TOC, post-footer, tag-page-title, series banner/nav/chip.
   ========================================================================= */

/* ---------- Home / Tag page: post list ---------- */
.post-list { list-style: none; padding: 0; margin: 0; }
.post-item {
    border-bottom: 1px solid var(--border);
    padding: 28px 0;
}
.post-item:last-child { border-bottom: none; }

/* Home variant: posts grouped by day. Date is a section header with a
   dashed horizontal rule extending right; the day's entries hang from
   a dashed vertical rail under it — blueprint/schematic vibe, not a
   container box. Scoped via .post-date-group so /tag and /search keep
   the flat layout. */
.post-date-group { margin-bottom: 0; }
.post-date-header {
    display: flex;
    align-items: center;
    gap: 16px;
    margin: 36px 0 4px;
    font-family: 'Share Tech Mono', monospace;
    font-size: 14px;
    letter-spacing: 4px;
    color: var(--primary-dim);
    text-transform: uppercase;
    font-weight: normal;
}
/* Highlight the date like a text-selection block — solid primary fill,
   bg-coloured text, no border. Mirrors the ::selection treatment in
   base.css so the header reads as "this segment of time is selected". */
.post-date-header-text {
    padding: 2px 8px;
    background: var(--primary);
    color: var(--bg);
}
.post-date-header::after {
    content: '';
    flex: 1;
    border-top: 1px dashed var(--border);
}
.post-date-group:first-child .post-date-header { margin-top: 8px; }
.post-date-group .post-item {
    /* Dashed bottom rule between siblings inside one date group — the
       18+18 padding gap alone read as "one continuous list" against the
       left rail, so a thin divider sits inside the gap to break each
       row apart without crowding (left rail stays the dominant visual).
       Suppressed on :last-child so the next date-group's header acts as
       the next separator instead. */
    border-bottom: 1px dashed var(--border);
    border-left: 1px dashed var(--border);
    padding: 18px 0 18px 28px;
    margin-left: 14px;
    transition: border-left-color 0.22s ease;
}
.post-date-group .post-item:last-child {
    border-bottom: none;
    padding-bottom: 14px;
}
.post-date-group .post-item:hover {
    /* Step from dim --border → full --primary on hover. Earlier
       --primary-dim mid-tone was too close to rest state and disappeared
       on flat themes (p7, p11) where the title's text-shadow lift is
       stripped. Full primary makes the left rail the hover affordance
       across every theme without adding extra ornaments. */
    border-left-color: var(--primary);
}

/* Top meta line — date (anchor) + optional series tag. Bigger letter
   spacing + Share Tech Mono caps reads as a "log timestamp" line and
   pulls the eye before the title. */
.post-meta {
    font-family: 'Share Tech Mono', monospace;
    font-size: 12px;
    color: var(--text-dim);
    letter-spacing: 3.5px;
    text-transform: uppercase;
    margin-bottom: 12px;
    display: flex;
    flex-wrap: wrap;
    gap: 14px;
    align-items: center;
}
.post-date {
    color: var(--primary-dim);
    transition: color 0.22s ease, text-shadow 0.22s ease;
}

.post-title-link { text-decoration: none; display: block; }
.post-title {
    font-family: 'VT323', monospace;
    font-size: clamp(28px, 5.2vw, 34px);
    color: var(--primary);
    text-shadow: 0 0 8px var(--primary-glow-strong);
    letter-spacing: 1px;
    display: inline-block;
    line-height: 1.15;
    transition: text-shadow 0.22s ease, transform 0.22s ease;
    /* Allow long unbreakable tokens (e.g. `STEVEN_SULLIVAN_OFFICE_WANG_CO…`,
       URLs) to wrap mid-string on narrow viewports. Without this they
       overflow the post-item box and the right side of the title gets
       clipped off-screen on mobile. */
    overflow-wrap: anywhere;
    word-break: break-word;
    max-width: 100%;
}
.post-icon { opacity: 0.85; }
/* Bare lock glyph inline with the post title — no brackets, no chip
   border, no second wrapper. Sized to ~0.6× the title for hierarchy
   (title still dominates), inherits color so it tints with the active
   phosphor palette, dim opacity to read as a state marker rather than
   the main mark. */
.post-title-lock {
    font-size: 0.55em;
    margin-right: 0.35em;
    opacity: 0.7;
    vertical-align: 0.18em;
}

/* Summary reads as an editorial preview, no `//` prefix — the dim text
   tone alone signals secondary information. */
.post-summary {
    color: var(--text-dim);
    margin-top: 10px;
    font-size: 0.98em;
    line-height: 1.65;
    transition: color 0.22s ease;
}

/* Bottom row holds the series tag (when present) followed by tag chips.
   Scope strips the chip's border + padding so each entry reads as a
   typographic line of `SERIES · PART N   #tag1   #tag2` — the bordered
   pill style still applies in /post, /search, and plugin nav. */
.post-tags-row {
    margin-top: 16px;
    display: flex;
    flex-wrap: wrap;
    /* row gap = 6px (tight break between series → tags lines);
       column gap = 16px (breathing between sibling chips). */
    gap: 6px 16px;
    align-items: baseline;
}
.post-tags-row .tag-chip,
.post-tags-row .post-series-tag {
    border: 0;
    background: transparent;
    padding: 0;
    transition: color 0.18s ease, text-shadow 0.18s ease;
}
.post-tags-row .tag-chip { color: var(--primary-dim); }
.post-tags-row .post-series-tag {
    color: var(--accent);
    /* Default: stay inline with siblings (saves a row when the only
       other badge is e.g. the LOCKED pill on the homepage). The
       "series gets its own row" treatment for tag / archive pages
       is opted in via the sibling-aware `:has` rule below. */
    flex-basis: auto;
}
/* Force the series chip onto its own row ONLY when tag chips are
   siblings (tag / archive page layouts). On home the row holds at most
   the LOCKED pill + series chip, and we keep them on a single line to
   save vertical space. */
.post-tags-row:has(.tag-chip) .post-series-tag {
    flex-basis: 100%;
}
/* Tag-chip row sits directly under the pill+series row on tag pages.
   Shrink the top margin so the two rows read as one grouped block
   instead of two unrelated bands. */
.post-tags-row--chips {
    margin-top: 6px;
}
.post-tags-row .tag-chip:hover,
.post-tags-row .post-series-tag:hover {
    background: transparent;
    color: var(--primary);
    text-shadow: 0 0 6px var(--primary-glow-strong);
}

/* Hover state — entire item reacts as a unit. Title glow intensifies,
   date lifts from dim to primary, summary text lifts from dim to full.
   No layout shift so list stays calm during scroll-hover. */
.post-item:hover .post-title {
    text-shadow: 0 0 14px var(--primary), 0 0 24px var(--primary-glow-strong);
}
.post-item:hover .post-date {
    color: var(--primary);
    text-shadow: 0 0 8px var(--primary-glow-strong);
}
.post-item:hover .post-summary { color: var(--text); }

/* ---------- Post page ---------- */
.post-page-title {
    margin-bottom: 12px;
}
.post-tags-label {
    margin-top: 40px;
    margin-bottom: 10px;
}
.post-meta-tags {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    align-items: center;
}
/* Post-page tags read as primary CTA-style chips — readers often want to
   jump to "more like this" right after finishing an article. Bigger font,
   brighter accent color, and the bordered pill match the chip language
   used in /search and footer nav. */
.post-meta-tags .tag-chip {
    font-size: 13px;
    padding: 5px 14px;
    color: var(--accent);
    border-color: var(--accent-dim);
    transition: color 0.18s ease, background 0.18s ease, border-color 0.18s ease, text-shadow 0.18s ease;
}
.post-meta-tags .tag-chip:hover {
    background: var(--primary-glow);
    border-color: var(--accent);
    color: var(--primary);
    text-shadow: 0 0 6px var(--primary-glow-strong);
}

.post-body { font-size: 1em; line-height: 1.85; }
/* In-body `# Heading` becomes <h1>, which would otherwise inherit the
   global h1 scale used by the post-page title and compete with it.
   Pin it above h2 but well below the page title so the hierarchy
   reads page-title > body-h1 > h2 > h3. */
.post-body h1 { font-size: clamp(30px, 6vw, 42px); }
.post-body h1, .post-body h2, .post-body h3 { margin-top: 28px; margin-bottom: 14px; }
.post-body p { margin: 14px 0; }
.post-body ul, .post-body ol { margin: 14px 0 14px 24px; }
.post-body li { margin: 8px 0; }

@media (min-width: 768px) {
    .post-body .post-section { padding-left: 22px; margin: 40px 0; }
}

.post-footer {
    margin-top: 56px;
    padding-top: 24px;
    border-top: 1px solid var(--border);
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
}
.post-meta-tags + .post-footer {
    margin-top: 28px;
}
/* On phones the action row (VIEW SOURCE / EDIT / ZEN / BACK TO INDEX,
   for admins) is wider than the viewport so we let it wrap onto two
   lines. Each button still gets compact horizontal padding + tighter
   tracking + a hard `white-space: nowrap` so its own label never breaks
   mid-word. Anonymous visitors see fewer buttons and the row still wraps
   gracefully. */
@media (max-width: 600px) {
    .post-footer { flex-wrap: wrap; gap: 6px; }
    .post-footer .view-source-link {
        flex: 0 0 auto;
        min-width: 0;
        padding: 7px 8px;
        font-size: 10px;
        letter-spacing: 1px;
        white-space: nowrap;
    }
}

/* Balance the empty space below post-footer buttons with the 24px
   padding-top inside post-footer — so the buttons sit between two
   equal gaps instead of floating against a big bottom void.
   Scope to .is-post so home/archive/tag listings keep their breathing
   room after pagination / heatmap / back-link. */
body.is-post main {
    padding-bottom: 24px;
}

/* ---------- Tag page (extends post list) ---------- */
.tag-page-title { color: var(--primary); }

/* ---------- Post figures (auto-wrapped images) ----------
   Standalone `![alt](url)` in markdown becomes <figure class="post-figure">
   with the caption pulled from alt. The figure stays inside the post
   reading column so images don't bleed past the article width; the
   caption sits underneath, narrower + centered.
   A theme-color overlay (mix-blend-mode: multiply) tints the image so photos
   feel native to the CRT terminal aesthetic.

   Multiple consecutive image-only lines get grouped into a single
   <figure class="post-figure post-figure-gallery count-N"> and laid out
   as a CSS Grid — 2 columns, 3 columns, or auto-fit for 4+. */
.post-figure {
    width: 100%;
    margin-top: 36px;
    margin-bottom: 36px;
    position: relative;
}
.post-figure-image {
    position: relative;
    width: 100%;
    overflow: hidden;
    border-top: 1px solid var(--primary-dim);
    border-bottom: 1px solid var(--primary-dim);
}
.post-figure-image img {
    display: block;
    width: 100%;
    height: auto;
    /* Heavy desaturation lets the multiply overlay carry a full theme-color
       wash (duotone vibe like open-weather.community). Hover restores
       full colour — see the `@media (hover: hover)` block below. */
    filter: grayscale(0.9) contrast(1.05);
    transition: filter 0.35s ease;
}
/* Direct-link videos (`.webm`/`.mp4`/`.mov`/`.ogv` in markdown) share
   the image cell wrapper but skip the photo-only treatment: no grayscale
   filter (would mute the colour grading of motion content), no opaque
   black box (would defeat .webm with alpha channels — Hermes-style
   transparent orbs and similar), and no multiply tint overlay (would
   veil play/seek controls).
   We key off the `.post-figure-video` class emitted server-side by
   `MarkdownRenderer::postprocessFigures()` instead of `:has(video)`
   so the override works on older browsers that don't yet ship the
   relational pseudo-class. */
.post-figure-image video {
    display: block;
    /* Sit centred inside the article column and cap height at a
       chunk of the viewport so portrait orbs / motion loops stay
       above-the-fold on a laptop. width:auto + max-width:100%
       preserves the intrinsic aspect — wide cinema clips fill the
       column, tall portrait clips shrink to fit the height cap and
       sit narrower in the middle. */
    margin: 0 auto;
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 70vh;
    background: transparent;
}
.post-figure-video .post-figure-image {
    /* Centre the (auto-sized) video element inside the cell. */
    text-align: center;
}
.post-figure-video .post-figure-image {
    /* Drop the phosphor-dim hairlines — transparent videos
       (decorative orbs, motion glyphs) shouldn't sit inside a
       floating rectangle. Captions stay underneath. */
    border-top: none;
    border-bottom: none;
}
.post-figure-video .post-figure-image::after {
    /* Drop the phosphor tint overlay over video so controls
       don't get veiled and alpha channels stay clean. */
    display: none;
}
/* Standalone videos tend to be portrait orbs or decorative loops, so
   shrink the figure to the video's intrinsic width instead of filling
   the whole reading column. */
.post-figure-video {
    width: auto;
    max-width: 100%;
}
/* The theme-color tint overlay — multiply blend so dark areas of the photo
   stay dark and the highlights take on the phosphor color. Opacity is high
   enough to carry a duotone wash when combined with the grayscale filter
   above; hover lifts both back to zero. */
.post-figure-image::after {
    content: '';
    position: absolute;
    inset: 0;
    background: var(--primary);
    mix-blend-mode: multiply;
    opacity: 0.55;
    pointer-events: none;
    transition: opacity 0.35s ease;
}

/* Hover-to-reveal: on pointer-fine devices the wash lifts and the photo
   returns to its original colour. Gated behind `hover: hover` so touch
   devices (where :hover has no reliable "off" state) keep the duotone. */
@media (hover: hover) {
    .post-figure-image:hover img {
        filter: grayscale(0) contrast(1);
    }
    .post-figure-image:hover::after {
        opacity: 0;
    }
}
.post-figure figcaption {
    max-width: 720px;
    margin: 14px auto 0;
    padding: 0 24px;
    text-align: center;
    font-family: 'Share Tech Mono', monospace;
    font-size: 12px;
    color: var(--text-dim);
    letter-spacing: 1.5px;
    line-height: 1.5;
}
/* ---------- Multi-image gallery (post-figure-gallery) ----------
   Triggered when N>=2 consecutive image-only lines are grouped into a
   single figure by MarkdownRenderer. Each (image + caption) pair lives
   in a .post-figure-cell so the grid lays them out as a column inside
   each cell — captions stay UNDER their image, never pushed to the side
   as a second column slot. */
.post-figure-gallery {
    display: grid;
    gap: 8px;
}
.post-figure-cell {
    display: flex;
    flex-direction: column;
}
.post-figure-cell .post-figure-image {
    border-top: none;
    border-bottom: none;
    flex: 0 0 auto;
}
.post-figure-cell figcaption {
    max-width: 100%;
    padding: 0;
    margin-top: 6px;
    font-size: 11px;
    line-height: 1.4;
    flex: 0 0 auto;
}
/* Mỗi count-N giữ đúng N cột bất kể viewport size. Dùng `minmax(0, 1fr)`
   thay vì `1fr` để ngăn nội dung ảnh (intrinsic min-width) ép cột rộng
   hơn 1/N figure width — nếu không sẽ tràn ngang khi N lớn. */
.post-figure-gallery.count-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.post-figure-gallery.count-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.post-figure-gallery.count-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.post-figure-gallery.count-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.post-figure-gallery.count-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
/* count-7+ không có rule riêng → rơi về CSS Grid default (1 cột).
   Nếu cần hỗ trợ count-7+, thêm rule tương tự ở đây. */

/* ---------- Floating TOC (desktop only) ----------
   The post.php template renders the TOC twice:
     1. `.toc-inline` — always in normal flow, sits just below the post meta on
        every viewport size. This is the "stays below summary when not scrolled"
        copy.
     2. `.post-toc-wrap` — a duplicate that's hidden on mobile and only fades in
        on desktop after the reader scrolls past a threshold (toggled by JS).
   The article column width is never affected by the floating TOC.
*/
.post-toc-wrap { display: none; }

@media (min-width: 1024px) {
    .post-toc-wrap {
        display: block;
        position: fixed;
        /* Pushed down to clear the top-left CRT corner vignette which
           was visibly darkening the TOC header. */
        top: 72px;
        left: 36px;
        width: 220px;
        max-height: calc(100vh - 110px);
        overflow-y: auto;
        opacity: 0;
        pointer-events: none;
        transform: translateX(-12px);
        transition: opacity 0.25s ease, transform 0.25s ease;
        z-index: 50;
        margin: 0;
        background: rgba(15, 26, 15, 0.92); /* var(--panel) at 92% so scanline shows through */
        backdrop-filter: blur(2px);
        -webkit-backdrop-filter: blur(2px);
        padding: 14px 14px 14px 16px;
        border: 1px solid var(--border);
        border-left: 2px solid var(--accent-dim);
        border-radius: 3px;
    }
    [data-theme="amber"] .post-toc-wrap {
        background: rgba(26, 18, 8, 0.92);
    }

    .post-toc-wrap.floating-visible {
        opacity: 0.96;
        pointer-events: auto;
        transform: translateX(0);
    }

    .post-toc-wrap .toc {
        margin: 0;
        background: transparent;
        border: none;
        padding: 0;
    }
    .post-toc-wrap .toc-label { margin-bottom: 10px; }
    .post-toc-wrap .toc-list li { margin: 5px 0; font-size: 12px; line-height: 1.45; }

    /* Custom scrollbar for floating TOC */
    .post-toc-wrap::-webkit-scrollbar { width: 4px; }
    .post-toc-wrap::-webkit-scrollbar-thumb { background: var(--primary-dim); border-radius: 2px; }
    .post-toc-wrap::-webkit-scrollbar-track { background: transparent; }
}

/* ---------- Post-body code blocks: dashed HUD frame on grid background ----------
   Cross-hatch grid drawn over the panel bg via repeating-linear-gradients
   in the accent color, plus a dashed accent border around the whole
   block — reads as a targeting/output frame. */
/* `.post-code-wrap` is injected by post.js around every <pre>. The HUD
   frame (background + dashed border) lives on the WRAPPER so the corner
   widgets (.code-lang-tag, .copy-btn) can be absolutely positioned
   against a non-scrolling box. The inner <pre> owns only the scroll
   surface — without this split, absolute children inside an
   `overflow:auto` <pre> drift with the horizontal scroll. */
.post-body .post-code-wrap {
    position: relative;
    margin: 20px 0;
    background:
        repeating-linear-gradient(0deg,
            rgba(255, 183, 0, 0.05) 0, rgba(255, 183, 0, 0.05) 1px,
            transparent 1px, transparent 6px),
        repeating-linear-gradient(90deg,
            rgba(255, 183, 0, 0.05) 0, rgba(255, 183, 0, 0.05) 1px,
            transparent 1px, transparent 6px),
        var(--panel);
    border: 1px dashed var(--accent-dim);
    border-radius: 2px;
}
.post-body pre {
    margin: 0;
    padding: 36px 18px 16px;
    background: transparent;
    border: 0;
    font-size: 13px;
    line-height: 1.6;
    overflow-x: auto;
}
.post-body pre code {
    color: var(--accent);
    background: transparent;
    padding: 0;
    text-shadow: 0 0 4px var(--accent-dim);
}

/* ---------- Prism syntax highlighting (phosphor token theme) ----------
   Prism loads grammars on demand via the autoloader (see layout.php).
   Token colors map to existing palette vars so every theme (amber, green,
   crypt, brutalist, p7, p11) recolors syntax highlighting for free.
   Keep buckets coarse — three colors plus dim comment is enough to read
   without competing with the CRT phosphor aesthetic. */
.post-body pre[class*="language-"] code,
.post-body code[class*="language-"] {
    color: var(--accent);
    text-shadow: 0 0 4px var(--accent-dim);
    background: transparent;
}
.post-body .token.comment,
.post-body .token.prolog,
.post-body .token.doctype,
.post-body .token.cdata {
    color: var(--text-dim);
    font-style: italic;
}
.post-body .token.punctuation { color: var(--text); }
.post-body .token.namespace { opacity: 0.7; }
.post-body .token.property,
.post-body .token.tag,
.post-body .token.boolean,
.post-body .token.number,
.post-body .token.constant,
.post-body .token.symbol,
.post-body .token.deleted {
    color: var(--primary);
}
.post-body .token.selector,
.post-body .token.attr-name,
.post-body .token.string,
.post-body .token.char,
.post-body .token.builtin,
.post-body .token.inserted {
    color: var(--accent);
}
.post-body .token.operator,
.post-body .token.entity,
.post-body .token.url,
.post-body .token.variable {
    color: var(--text);
}
.post-body .token.atrule,
.post-body .token.attr-value,
.post-body .token.keyword,
.post-body .token.important {
    color: var(--primary);
    font-weight: 600;
}
.post-body .token.function,
.post-body .token.class-name {
    color: var(--accent);
    font-weight: 600;
}
.post-body .token.regex {
    color: var(--primary-dim);
}
.post-body .token.bold { font-weight: bold; }
.post-body .token.italic { font-style: italic; }

.code-lang-tag {
    position: absolute;
    top: 8px;
    left: 14px;
    font-family: 'Share Tech Mono', monospace;
    font-size: 10px;
    color: var(--accent);
    letter-spacing: 2px;
    text-transform: uppercase;
    pointer-events: none;
}

.copy-btn {
    position: absolute;
    top: 6px;
    right: 8px;
    font-family: 'Share Tech Mono', monospace;
    font-size: 10px;
    background: var(--panel);
    border: 1px solid var(--accent-dim);
    color: var(--accent);
    padding: 4px 10px;
    cursor: pointer;
    letter-spacing: 2px;
    opacity: 0.55;
    transition: opacity 0.18s, background 0.18s, border-color 0.18s;
    border-radius: 2px;
}
.post-body pre:hover .copy-btn { opacity: 1; }
.copy-btn:hover { background: var(--primary-glow); border-color: var(--accent); }
.copy-btn.copied { color: var(--primary); border-color: var(--primary); }

/* ---------- Series link (in post-meta on home feed) ----------
   Series link styled to match the surrounding date meta on the home
   feed — e.g. "2026-06-19 // XV5HP // rtl-sdr-cho-nguoi-moi - P1".
   Inherits the meta's dim color but glows on hover. */
.post-date-series {
    color: var(--accent);
    text-decoration: none;
    border-bottom: 1px dashed transparent;
}
.post-date-series:hover {
    color: var(--primary);
    border-bottom-color: var(--primary-dim);
    text-shadow: 0 0 6px var(--primary-glow-strong);
}

/* ---------- Series chip (inline tag on listings) ----------
   Used on home, tag, and archive pages to mark a post that belongs to
   a series. Dashed amber border distinguishes it from tag-chips so the
   "this is part of a series" cue is immediately readable. */
.post-series-tag {
    display: inline-block;
    padding: 1px 8px;
    font-family: 'Share Tech Mono', monospace;
    font-size: 11px;
    letter-spacing: 1px;
    color: var(--accent);
    border: 1px dashed var(--accent-dim);
    border-radius: 2px;
    text-decoration: none;
    text-transform: uppercase;
    white-space: nowrap;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.post-series-tag:hover {
    background: var(--primary-glow);
    border-color: var(--accent);
    color: var(--primary);
}

/* ---------- Series banner + nav (on post page) ----------
   The banner sits between post meta and body: clickable bar that
   announces the post's position in the series and links to the series
   index. The nav sits below the body before the post-footer with
   prev/next titled blocks. */
.series-banner {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 16px 0 28px;
    padding: 12px 16px;
    background: var(--panel);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    text-decoration: none;
    font-family: 'Share Tech Mono', monospace;
    transition: border-color 0.15s, background 0.15s;
}
.series-banner:hover {
    border-color: var(--primary-dim);
    background: var(--primary-glow);
}
.series-banner-label {
    color: var(--accent);
    font-size: 11px;
    letter-spacing: 2px;
    text-transform: uppercase;
    white-space: nowrap;
}
.series-banner-title {
    color: var(--primary);
    font-size: 14px;
    letter-spacing: 1px;
    /* Single-line contract — the label + title are a one-row badge. If
       the series name doesn't fit, truncate with ellipsis rather than
       wrap onto a second line that visually doubles the banner height.
       min-width: 0 is required because the flex parent's default
       min-width: auto would refuse to shrink the title below its
       content width and the truncation would never engage. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
    flex: 1;
}
@media (max-width: 600px) {
    .series-banner { gap: 8px; padding: 10px 12px; }
    .series-banner-label { letter-spacing: 1.5px; }
    .series-banner-title { font-size: 13px; letter-spacing: 0.5px; }
}

.series-nav {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
    margin: 40px 0 24px;
}
.series-nav-link {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 14px 16px;
    background: var(--panel);
    border: 1px solid var(--border);
    text-decoration: none;
    transition: border-color 0.15s, box-shadow 0.15s;
    min-height: 60px;
}
.series-nav-link:hover {
    border-color: var(--primary-dim);
    box-shadow: 0 0 12px var(--primary-glow);
}
.series-nav-empty {
    visibility: hidden;
}
.series-nav-direction {
    color: var(--accent);
    font-family: 'Share Tech Mono', monospace;
    font-size: 10px;
    letter-spacing: 3px;
    text-transform: uppercase;
}
.series-nav-next {
    text-align: right;
}
.series-nav-title {
    color: var(--text);
    font-family: 'VT323', monospace;
    font-size: 18px;
    line-height: 1.2;
}
.series-nav-link:hover .series-nav-title {
    color: var(--primary);
    text-shadow: 0 0 6px var(--primary-glow-strong);
}

@media (max-width: 600px) {
    .series-nav { grid-template-columns: 1fr; }
}

/* ---------- Password unlock panel — HUD warning aesthetic ----------
   Public unlock form for password-protected posts. Lives in post.css
   (universal sheet) — admin.css doesn't load on /posts/*. Colors
   inherit from the active theme (--primary, --primary-glow, --panel,
   --bg) so the HUD reads as the same console as the rest of the site
   (amber on amber theme, green on green, etc.). Only the error pill
   keeps a hard danger-red so the wrong-password state stays obvious
   regardless of theme.
   --------------------------------------------------------------- */

.alert-panel {
    position: relative;
    width: 100%;
    /* Top margin stays small — `main` already pads the page. Bottom
       margin is generous so the panel doesn't bump into the site
       footer's separator rule below it. */
    margin: 4px 0 36px;
    /* Symmetric vertical padding so the top stripe→content gap matches
       the content→bottom stripe gap. Stripes bleed into this padding via
       negative margins below; if top != bottom the layout reads lopsided. */
    padding: 32px 32px;
    background:
        radial-gradient(ellipse at top, var(--primary-glow) 0%, transparent 60%),
        var(--panel);
    /* No border — the hazard stripes top/bottom are the frame. A border
       on top of them double-stacks parallel rules and reads cluttered. */
    box-shadow: inset 0 0 30px var(--primary-glow);
    color: var(--primary);
    text-align: center;
    box-sizing: border-box;
}

/* ── Eyebrow label ──
   Stamped pill at the top of the panel — solid theme color with knockout
   text reads like a console stencil mark sitting on the panel surface. */
.alert-eyebrow {
    display: inline-block;
    font-family: 'Share Tech Mono', monospace;
    font-size: 13px;
    color: var(--bg);
    background: var(--primary);
    letter-spacing: 5px;
    padding: 6px 16px;
    margin: 0 0 18px;
    text-transform: uppercase;
    line-height: 1;
    white-space: nowrap;
    box-shadow: 0 0 12px var(--primary-glow-strong);
    animation: alert-pulse 2.4s ease-in-out infinite;
}
@keyframes alert-pulse {
    0%, 100% { opacity: 0.9; }
    50% { opacity: 0.45; }
}

/* ── Headline + meta ──
   Post title is the headline. Sizing stays modest so a long Vietnamese
   title fits without wrapping into a wall of red on wide screens — the
   panel framing carries the visual weight, not the type itself. */
.alert-headline {
    font-family: 'VT323', 'Share Tech Mono', monospace;
    font-size: clamp(24px, 3.6vw, 38px);
    color: var(--primary);
    text-shadow:
        0 0 10px var(--primary-glow-strong),
        0 0 22px var(--primary-glow);
    letter-spacing: 3px;
    margin: 0 0 8px;
    line-height: 1.15;
    text-transform: uppercase;
    overflow-wrap: break-word;
    max-width: 920px;
    margin-left: auto;
    margin-right: auto;
}
.alert-meta {
    font-family: 'Share Tech Mono', monospace;
    font-size: 11px;
    color: var(--primary);
    opacity: 0.75;
    letter-spacing: 2px;
    padding: 6px 0;
    border-top: 1px solid var(--primary-dim);
    border-bottom: 1px solid var(--primary-dim);
    margin: 6px 0 16px;
}
.alert-meta-sep { margin: 0 6px; opacity: 0.5; }
.alert-summary {
    font-family: 'Share Tech Mono', monospace;
    font-size: 12px;
    color: var(--text);
    opacity: 0.8;
    letter-spacing: 1px;
    margin: 0 0 18px;
    line-height: 1.5;
}

/* ── Form ── */
.alert-form {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    margin: 8px auto 4px;
    /* Shared max-width keeps the input and the [ AUTHENTICATE ][ ABORT ]
       grid below it at the same width — input no longer overshoots the
       button row when the panel is wide. Children stretch via their own
       width:100% (input + alert-actions). 440px so each half (215px after
       the 10px gap) fits the longer `[ AUTHENTICATE ]` chip with its
       3px letter-spacing + side padding without wrapping. */
    width: 100%;
    max-width: 440px;
}
.alert-input-label {
    font-family: 'Share Tech Mono', monospace;
    font-size: 11px;
    color: var(--primary);
    letter-spacing: 3px;
    text-transform: uppercase;
    margin: 0;
    line-height: 1;
}
.alert-input-tick {
    color: var(--primary);
    margin-right: 6px;
    animation: alert-blink 1s step-end infinite;
}
@keyframes alert-blink {
    50% { opacity: 0; }
}
.alert-input {
    width: 100%;
    /* Stretch to the panel's inner width so the input baseline matches
       the [ AUTHENTICATE ] [ ABORT ] grid below it instead of floating
       narrower than the action row. */
    background: var(--bg);
    border: 1px solid var(--primary-dim);
    color: var(--primary);
    font-family: 'Share Tech Mono', monospace;
    /* Chunky bullets so each entered character is clearly visible —
       password fields default to glyphs that are tiny in this monospace
       face. Letter-spacing keeps consecutive bullets from merging into
       a single blob. */
    font-size: 38px;
    line-height: 1;
    padding: 16px 18px;
    border-radius: 2px;
    box-sizing: border-box;
    text-align: center;
    letter-spacing: 14px;
    caret-color: var(--primary);
}
.alert-input::placeholder {
    color: var(--primary-dim);
    opacity: 0.55;
    letter-spacing: 8px;
    /* Placeholder stays smaller than the typed bullets so the field's
       active state reads visually distinct from the resting state. */
    font-size: 22px;
}
.alert-input:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 12px var(--primary-glow-strong);
}

/* Wrong-password state lives INSIDE the input itself: red border,
   red glow, and the error string is the placeholder. Once the user
   types again, the placeholder disappears — which is exactly the
   right semantics ("you're retrying now, the old warning no longer
   applies"). Saves a whole stacked panel block of vertical space. */
.alert-input-error {
    border-color: #ff3333;
    box-shadow: 0 0 12px rgba(255, 51, 51, 0.55);
    color: #ff5555;
    caret-color: #ff5555;
}
.alert-input-error::placeholder {
    color: #ff5555;
    opacity: 0.95;
    /* Compact spacing so even the longer throttle message
       ("Too many attempts. Try again later.") fits on a single line and
       stays centered instead of overflowing left-aligned. */
    font-size: 13px;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    text-align: center;
}

/* Disabled state: the rate-limit throttle has kicked in. Show that
   the field cannot accept input — dimmed, no-cursor, no glow. The
   browser already sets `cursor: not-allowed` on disabled inputs but
   we restate it for clarity. The accompanying button uses the same
   semantic. */
.alert-input:disabled,
.alert-btn:disabled {
    opacity: 0.55;
    cursor: not-allowed;
    pointer-events: none;
}
.alert-input:disabled {
    box-shadow: none;
}

/* Two equal-width columns so the [ AUTHENTICATE ] + [ ABORT ] pair
   spans the same width as the access-key input above them. The parent
   .alert-form caps that width (max-width 380px) so this grid inherits
   the same envelope and the two buttons stay paired with the input. */
.alert-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    width: 100%;
    margin-top: 6px;
}
.alert-btn {
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 36px;
    font-family: 'Share Tech Mono', monospace;
    font-size: 12px;
    font-weight: 400;
    color: var(--primary);
    background: transparent;
    border: 1px solid var(--primary-dim);
    padding: 0 18px;
    margin: 0;
    letter-spacing: 3px;
    text-decoration: none;
    border-radius: 2px;
    cursor: pointer;
    text-transform: uppercase;
    line-height: 1;
    /* Belt-and-braces against wrap — even with the 440px form envelope
       above, sub-pixel rounding on long labels (`AUTHENTICATE` w/ 3px
       tracking) can break to a second line on some renderers. */
    white-space: nowrap;
    -webkit-appearance: none;
    appearance: none;
    transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.alert-btn:hover {
    background: var(--primary-glow);
    border-color: var(--primary);
    box-shadow: 0 0 10px var(--primary-glow-strong);
}
/* `.alert-btn-primary` kept in markup for semantic intent but visually
   identical to `.alert-btn` — the original filled-pill treatment made
   the submit button shout while ABORT whispered, inconsistent with the
   sibling pairing. Hover/focus still inherit the base alert-btn cue. */

/* Mobile: tighten letter-spacing + padding + nowrap so the longer
   `[ AUTHENTICATE ]` label stays on one line inside its half of the
   1fr 1fr grid. Default 3px tracking + 18px side padding overflows the
   ~140-150px slot a phone column hands the button, wrapping the chip
   into two lines while [ ABORT ] sits beside it on one. */
@media (max-width: 600px) {
    .alert-btn {
        letter-spacing: 1px;
        padding: 0 8px;
        white-space: nowrap;
    }
}

/* ── Footer label ──
   Pushed down toward the panel bottom edge with a large top margin so
   it reads as a console footer rather than a caption that belongs to
   the buttons just above it. */
.alert-footer {
    font-family: 'Share Tech Mono', monospace;
    font-size: 10px;
    color: var(--primary-dim);
    letter-spacing: 2px;
    margin: 36px 0 0;
    text-transform: uppercase;
    opacity: 0.8;
}

/* ── Wrong-password feedback: shake + red glow ──
   Panel-wide signal that the last attempt failed. Inset shadow tints
   the panel interior red; outer shadow surrounds it with a red halo
   that's visible past the page background. Animation gives it a one-
   shot shake on first paint after submit. */
.alert-panel-error {
    box-shadow:
        0 0 22px rgba(255, 51, 51, 0.55),
        inset 0 0 40px rgba(255, 51, 51, 0.18);
    animation: alert-shake 0.42s cubic-bezier(.36,.07,.19,.97) both;
}
@keyframes alert-shake {
    10%, 90% { transform: translateX(-2px); }
    20%, 80% { transform: translateX(4px); }
    30%, 50%, 70% { transform: translateX(-7px); }
    40%, 60% { transform: translateX(7px); }
}

@media (max-width: 600px) {
    .alert-panel { padding: 26px 18px; margin: 8px 0 24px; }
    .alert-headline { letter-spacing: 2px; }
    .alert-input { font-size: 28px; letter-spacing: 10px; padding: 12px 14px; }
}
