/* static/css/pages.css — the per-page umbrella (everything else).
 *
 * Fourth of the seven stylesheets split out of the former single
 * ``gallae.css`` (tasks/FILE_SPLIT_TASKS.md Phase 1), and the largest. It is
 * one umbrella of divider-delimited sub-blocks covering the site chrome and
 * every non-reading page: form controls, the ``.btn`` family, the TOTP
 * wizard, site nav + masthead, flash messages, the post compose form, the v1
 * post-detail page, the drafts inbox, profiles, post cards + their responsive
 * rules, the social buttons, search, and the notifications inbox + prefs.
 * Kept as one file on purpose — the sub-blocks are children of a single
 * umbrella and the cascade requires them after ``base.css`` anyway. MUST load
 * after ``base.css`` and before ``post-page.css`` (whose larger-screen rules
 * override some of these at equal specificity).
 */

/* ---------------------------------------------------------------------
 * Phase 8 mobile-responsive review (Task 8.4)
 *
 * Walking every page on a 380px viewport surfaced four families of
 * problems the earlier-phase CSS hadn't addressed:
 *
 *   1. Form controls used the browser default. ``<input type="text">``
 *      sits at ~20ch (~150-180px) on mobile, ``<textarea>`` ditto,
 *      and bare ``<button>`` is small. Result: forms looked like
 *      they were rendered for a desktop with a tiny window — most
 *      of the 380px screen was empty whitespace and the controls
 *      had sub-44pt tap targets that fail Apple's HIG and WCAG
 *      2.5.5 (Target Size).
 *   2. ``_nav.html`` grew across Phases 6/7/8 from "log in / log
 *      out" to a cluster of ~9 links + a logout form. Flexbox
 *      without ``flex-wrap`` pushed the right-hand items off-screen
 *      on narrow viewports.
 *   3. Per-row inline groups (``.post-author-actions``,
 *      ``.post-affordances``, ``.drafts-list-item``,
 *      ``.notification-row``, ``.search-form``) all rendered as
 *      one-line groups with no wrap rule. Fine on desktop; busy and
 *      overflow-prone at 380px.
 *   4. Long URLs / code spans / handles in user-authored content
 *      can blow past the container width with no break, dragging
 *      the page horizontal. ``overflow-wrap: break-word`` on the
 *      content surfaces is the fix.
 *
 * Every rule below is scoped to a class the templates already
 * carry — no markup rename was required to apply the styling.
 * Where a section-specific rule conflicts with a global default
 * (e.g. ``.compose-form textarea`` already had explicit width
 * before this pass), the section-specific rule wins on
 * specificity, so the global defaults below extend coverage to
 * the surfaces that previously had none rather than overriding
 * the polished cases.
 *
 * Two cross-cutting principles:
 *   - Tap targets meet ~44px minimum: ``min-height: 2.5rem`` (40px
 *     at the default 16px root) on interactive elements is a
 *     small concession below the strict guideline; combined with
 *     the surrounding padding the actual hit area is ≥44px in
 *     practice. Values below 2.5rem break the rule deliberately
 *     for inline text-only links inside running prose, where the
 *     tap target is a finger-sized line of body text rather than
 *     a chrome control.
 *   - Group-level wrapping uses ``flex-wrap: wrap`` plus a small
 *     ``gap``: at 380px controls reflow onto multiple rows; on
 *     desktop the unchanged single-row layout is preserved
 *     because the container is wide enough that no wrap fires.
 * ------------------------------------------------------------------ */

/* ----- Form controls (default styling for text inputs, textareas,
 * selects, and buttons). Excluded: ``[type="checkbox"]`` and
 * ``[type="radio"]`` keep their browser-native rendering — the
 * compose-kind toggle (Task 5.7) and the digest opt-in toggle
 * (Task 8.3) both rely on the native control under a styled
 * wrapper, and stretching them to 100% width would break the
 * affordance. ``[type="hidden"]`` is also excluded by the
 * explicit selector list. */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="search"],
input[type="url"],
input[type="number"],
input[type="tel"],
textarea,
select {
    /* ``box-sizing: border-box`` is set globally at the top of the
     * file, so 100% includes padding + border without overflowing
     * the container. */
    width: 100%;
    max-width: 100%;
    font-family: inherit;
    font-size: 1rem;
    padding: 0.5rem;
    /* iOS Safari inflates the page when a focused input has a
     * font-size below 16px (it auto-zooms to "help" the user); we
     * already use 1rem == 16px above, so the auto-zoom does not
     * fire. The explicit value here is a load-bearing comment more
     * than a load-bearing rule. */
}

textarea {
    /* Vertical resize only: horizontal resize past the container
     * would re-introduce the horizontal-scroll case the rule above
     * is meant to prevent. */
    resize: vertical;
    /* Default min-height keeps a fresh textarea visibly tall —
     * Django's default rows=10 (or template-supplied rows=20) sets
     * the *initial* height via the rows attribute; this rule
     * applies when no rows attribute is present. */
    min-height: 6rem;
}

/* ----- Identity edit fields (settings → public profile). The handle /
 * display name / bio inputs drop their border for a cleaner, softer look —
 * a filled box (--surface-raised) carries the affordance instead. Handle and
 * Display Name put their box on the same line as the label (see the inline
 * row below); bio keeps its label above the textarea. */
.identity-form input[type="text"],
.identity-form textarea {
    border: none;
    background: var(--surface-raised);
    border-radius: 0.3rem;
    color: var(--text);
}

.identity-form input[type="text"]:focus-visible,
.identity-form textarea:focus-visible {
    outline: 2px solid var(--brand-cool);
    outline-offset: 1px;
}

/* Inline row: label + input box share the first line; help text and the
 * error list (full-basis flex items) wrap onto their own lines below. */
.identity-field--inline {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.4rem 0.75rem;
}

.identity-field--inline > input[type="text"] {
    flex: 0 1 24rem;
    width: auto;
}

/* The handle row's Save button sits at its natural width to the right of the
 * input, on the same baseline; it shouldn't stretch or wrap unless the row
 * runs out of space (then flex-wrap drops it below). */
.identity-field--inline > .btn {
    flex: 0 0 auto;
}

.identity-field--inline > small,
.identity-field--inline > .errorlist {
    flex-basis: 100%;
    margin: 0;
}

/* Inline preference dropdowns (Color theme, Parallel post sort order): a compact
 * <select> beside its label, in the identity inputs' borderless filled style,
 * instead of the global full-width control on its own line. Sized to its (short)
 * options via ``width: auto``. */
.pref-inline-field > select {
    width: auto;
    flex: 0 0 auto;
    border: none;
    background: var(--surface-raised);
    border-radius: 0.3rem;
    color: var(--text);
}

.pref-inline-field > select:focus-visible {
    outline: 2px solid var(--brand-cool);
    outline-offset: 1px;
}

/* Inline preference number input (Word count goal): same borderless filled
 * treatment as the inline <select> above, overriding the global
 * ``input[type="number"] { width: 100% }`` so it sits beside its label on the
 * same line rather than dropping full-width onto its own row. Sized to a few
 * digits. */
.pref-inline-field > input[type="number"] {
    width: auto;
    flex: 0 0 6rem;
    border: none;
    background: var(--surface-raised);
    border-radius: 0.3rem;
    color: var(--text);
}

.pref-inline-field > input[type="number"]:focus-visible {
    outline: 2px solid var(--brand-cool);
    outline-offset: 1px;
}

button {
    font-family: inherit;
    font-size: 1rem;
    padding: 0.5rem 0.85rem;
    border: 1px solid var(--border);
    border-radius: 3px;
    background: var(--surface-raised);
    color: inherit;
    cursor: pointer;
    /* See cross-cutting principle #1 above re: tap target size. */
    min-height: 2.5rem;
}

button:hover:not(:disabled) {
    background: var(--surface-muted);
}

button:focus-visible {
    /* Match the focus indicator used elsewhere (links, treeitems)
     * so keyboard users see a consistent ring across the site. */
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* ----- Standard button classes: ``.btn`` (neutral base) +
 * ``.btn-primary`` (accent / blue call-to-action) + ``.btn-danger``
 * (destructive / red). The outlined-pill look is the one shared with the
 * profile social-action buttons (Block / Follow / Mute), promoted to
 * reusable classes so new buttons get a consistent style — reach for
 * these on any new button rather than a bare ``<button>``. They work on
 * both ``<button>`` and ``<a class="btn">`` (e.g. a Cancel link styled as
 * a button). django-two-factor-auth's wizard templates already tag their
 * buttons ``btn`` / ``btn-primary`` / ``btn-danger``, so defining these
 * also themes the whole 2FA flow. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.35rem 0.85rem;
    border: 1px solid var(--border);
    border-radius: 0.4rem;
    background: var(--surface-raised);
    /* ``inherit`` (not the default link colour) so ``<a class="btn">``
     * reads neutral; the colour modifiers below set the accent/red. */
    color: inherit;
    font: inherit;
    font-size: 0.85em;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    min-height: 2.5rem;
}

.btn:hover:not(:disabled) {
    background: var(--surface-muted);
}

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

.btn-primary {
    color: var(--accent);
    border-color: var(--accent);
}

.btn-primary:hover:not(:disabled),
.btn-primary:focus-visible {
    background: var(--accent-tint);
}

.btn-danger {
    color: var(--danger-strong);
    border-color: var(--danger-strong);
}

.btn-danger:hover:not(:disabled),
.btn-danger:focus-visible {
    background: var(--error-bg-hover);
}

/* Horizontal row of ``.btn``s (wizard Cancel/Next, login Back/Next), wrapping
 * to stacked rows on narrow viewports. */
.button-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
    margin-top: 1rem;
}

/* ----- TOTP setup page: the QR code and the manual-entry secret with a
 * copy-to-clipboard control. */
.totp-qr {
    display: block;
    /* The QR SVG draws black modules on a transparent ground, so it needs a
     * white backing to stay scannable on the dark theme. White is correct
     * regardless of palette — a scanner needs the contrast. */
    background: #fff;
    padding: 0.5rem;
    border-radius: 4px;
    width: 12rem;
    max-width: 100%;
    height: auto;
}

.totp-secret {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
}

.totp-secret code,
.totp-secret a {
    font-family: monospace;
    letter-spacing: 0.05em;
    word-break: break-all;
}

/* The copy icon-button: compact (overrides the pill sizing of ``.btn``),
 * inherits the neutral button chrome from the base ``button`` rule. */
.copy-secret {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.35rem;
    color: var(--accent);
}

/* ``<small>`` inside form ``<p>`` is the recurring pattern auth /
 * recovery / pseud-form templates use for help_text. Default
 * inline rendering puts the help blurb beside the input on the
 * same line (or wraps awkwardly mid-line on mobile); a block
 * display puts the blurb on its own row underneath the field, in
 * line with how the post-form's ``.form-help`` already renders. */
form p > small,
form p > label {
    display: block;
    /* Help text and field labels read in the sans face, not the body serif. */
    font-family: var(--font-sans);
}

form p {
    /* Tighten the default ``p`` margin so labels read as adjacent
     * to their input, not separated by a paragraph break. */
    margin: 0 0 0.85rem;
}

form p > label {
    font-weight: 600;
    margin-bottom: 0.25rem;
}

/* ----- Site nav (_nav.html). The link cluster grew across phases
 * to a row of links plus a ``<form>`` logout; without wrap, the
 * right-hand items pushed off the screen at 380px. Wrapping plus a
 * generous gap turns the nav into a multi-line band on mobile while
 * preserving the single-line layout on wider viewports. */
.site-nav-inner {
    /* Allow brand + nav cluster to break onto separate rows. */
    flex-wrap: wrap;
    row-gap: 0.5rem;
}

.site-nav nav {
    /* The nav itself wraps internally too, so a long string of
     * links degrades to two or three rows rather than overflowing. */
    flex-wrap: wrap;
    row-gap: 0.4rem;
    /* Allow the inner cluster to stretch to its own row when it
     * needs more width than ``space-between`` left for it. */
    flex: 1 1 auto;
    justify-content: flex-start;
}

/* The authed nav (see _nav.html) splits into the always-visible primary
 * cluster — home, username, post, drafts, inbox — and a collapsible group
 * ([admin], settings, report-bug, logout). On wide screens the group is
 * flattened back inline and the nav reads as one continuous row; under 559px it
 * folds into a right-aligned hamburger dropdown (see the max-width:559px block
 * below), where drafts joins it too. */
.nav-primary {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.75rem;
}

/* Drafts is a responsive pair (see _nav.html): the .nav-drafts-bar copy sits
 * inline on desktop, the .nav-drafts-menu copy lives in the hamburger dropdown
 * on mobile. Default (desktop, where .nav-menu is display:contents) hides the
 * menu copy so drafts shows exactly once; the max-width:559px block below flips
 * them. */
.nav-drafts-menu {
    display: none;
}

/* Wide screens: dissolve the wrapper + menu (display:contents) so their links
 * become direct flex items of the ``nav`` row; the hamburger is hidden, and
 * .nav-settings carries the margin-left:auto that pushes settings + logout to
 * the right edge — the same right-aligned account cluster as before. */
.nav-menu-wrap,
.nav-menu {
    display: contents;
}

.nav-menu .nav-settings {
    margin-left: auto;
}

.nav-menu-toggle {
    display: none;
}

/* Forms inside the nav (logout, pseud-switch) are block-level by
 * default, which would force a new row each. Inline-block puts
 * them in the link flow. The form's button picks up the
 * ``button`` defaults above, so the visual weight matches the
 * surrounding ``<a>`` links. */
.site-nav .logout-form,
.site-nav .inline-form {
    display: inline-block;
    margin: 0;
}

/* The logout control must stay a POST form (Django 5+ dropped GET logout),
 * but visually it should read as a plain text link like the other nav
 * items, not a bordered button. Strip the button chrome and adopt the
 * link colour. */
.site-nav .logout-form button {
    padding: 0;
    border: none;
    border-radius: 0;
    background: none;
    min-height: 0;
    font: inherit;
    letter-spacing: inherit;
    color: inherit;
    cursor: pointer;
}

.site-nav .logout-form button:hover:not(:disabled) {
    background: none;
    text-decoration: none;
    color: var(--brand-red);
}

/* "report a bug" nav item — an action, but styled as a plain nav text link:
 * strip the global button chrome (like the logout button) and hover to the
 * neutral --brand-cool the other nav links use (not the logout warning red).
 * It opens the shared feedback panel via the feedback:open event; see
 * _nav.html / _feedback_widget.html. Hidden below 880px (the mobile slide-out
 * rail carries the same action) — see the max-width:879px rule. */
.site-nav .nav-report-bug {
    padding: 0;
    border: none;
    background: none;
    min-height: 0;
    font: inherit;
    letter-spacing: inherit;
    color: inherit;
    cursor: pointer;
}

/* ``background: none`` is load-bearing: the base rule above resets the button
 * chrome at rest, but the global ``button:hover:not(:disabled)`` is a *hover*
 * rule and outranks it once hovered, repainting the --surface-muted box. Reset
 * it here (as .logout-form button:hover does) or the item grows a background
 * on hover while its neighbours only change colour. */
.site-nav .nav-report-bug:hover:not(:disabled) {
    background: none;
    color: var(--brand-cool);
}

/* Narrow phones: fold the collapsible group (drafts / admin / settings /
 * report-bug / logout) into a right-aligned hamburger dropdown, leaving home /
 * username / post / inbox on the row. The dropdown mirrors the tag-autocomplete
 * idiom — a floating --surface-raised column with a border + soft shadow. */
@media (max-width: 559px) {
    .nav-menu-wrap {
        /* Real box now (not display:contents): the right-aligned hamburger,
         * and the positioning context the dropdown anchors to. */
        display: flex;
        position: relative;
        margin-left: auto;
    }

    /* Drafts moves into the dropdown here: hide the inline bar copy, reveal the
     * menu copy. (Inverse of the desktop default above.) */
    .nav-drafts-bar {
        display: none;
    }

    .nav-drafts-menu {
        display: block;
    }

    .nav-menu-toggle {
        display: inline-flex;
        align-items: center;
        padding: 0;
        border: none;
        background: none;
        color: inherit;
        font-size: 1.2rem;
        line-height: 0;
        cursor: pointer;
        /* Reset the global ``button { min-height: 2.5rem }`` tap-target floor:
         * left in place it makes this icon-only button ~2.5x the height of its
         * 1.2rem glyph, which grew the whole sticky header at <=559px. The icon
         * is the tap target here, so the floor isn't needed. */
        min-height: 0;
        /* Nudge the glyph down a hair so it optically centers with the text
         * links in the row (it otherwise rides high against their baseline).
         * transform, not margin, so the row height is untouched. */
        transform: translateY(2px);
    }

    /* ``background: none`` + ``:not(:disabled)`` are both load-bearing — see the
     * .nav-report-bug:hover note above. The bare ``.nav-menu-toggle:hover`` is
     * (0,2,0), which *loses* to the global ``button:hover:not(:disabled)``
     * (0,2,1); the extra pseudo-class buys the specificity to reset the box. */
    .nav-menu-toggle:hover:not(:disabled) {
        background: none;
        color: var(--brand-red);
    }

    .nav-menu {
        display: none;
        position: absolute;
        top: calc(100% + 0.75rem);
        right: 0;
        z-index: 100;
        flex-direction: column;
        gap: 0.85rem;
        min-width: 9rem;
        padding: 0.85rem 1rem;
        background: var(--surface-raised);
        border: 1px solid var(--border);
        box-shadow: 0 2px 4px var(--shadow-sm);
    }

    .nav-menu.is-open {
        display: flex;
    }

    /* Reset the desktop right-push: in the vertical column an auto left margin
     * would shove settings to the bottom instead of leaving it in sequence. */
    .nav-menu .nav-settings {
        margin-left: 0;
    }
}

/* ----- Django messages framework (rendered in base.html when
 * the request has queued ``messages.success`` / ``messages.error``
 * / etc.). Until this pass the framework's output rendered as a
 * bare ``<ul>`` with default bullets and no visual separation
 * from surrounding content — the user couldn't tell at a glance
 * that the strip belonged to a notice rather than the page body. */
.messages {
    list-style: none;
    padding: 0;
    margin: 0 0 1rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.message {
    padding: 0.5rem 0.75rem;
    border-left: 3px solid var(--border);
    background: var(--surface-muted);
    color: var(--text-comment);
}

.message-success {
    background: var(--success-bg);
    border-left-color: var(--success-border);
    color: var(--success-text);
}

.message-error {
    background: var(--error-bg);
    border-left-color: var(--danger);
    color: var(--error-text);
}

.message-info,
.message-warning {
    background: var(--accent-tint);
    border-left-color: var(--accent);
    color: var(--accent-strong);
}

/* ----- Post compose / edit form (Task 7.2 + 7.3, _post_form.html).
 * Each field is wrapped in a ``.form-field`` div with label,
 * control, optional help blurb, optional error list. Stacking
 * these vertically with consistent gaps keeps the form scannable
 * at 380px without any field jumping into another's row. */
.form-field {
    margin: 0 0 1rem;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.form-field > label {
    font-weight: 600;
    font-family: var(--font-sans);
}

.form-help {
    color: var(--text-muted);
    font-size: 0.9em;
    /* Explanatory field help reads in the sans face, not the body serif. */
    font-family: var(--font-sans);
}

.post-form-author {
    color: var(--text-muted);
    margin: 0 0 0.75rem;
}

.post-form-actions {
    margin-top: 1rem;
    /* Wrap the action row so the Save+Cancel pair (EH.5) won't
     * shove the cancel button off-screen on narrow viewports. */
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

/* The edit form's Cancel control is an ``<a>`` styled as a button; it
 * now uses the shared ``.btn btn-danger`` classes (a canceling action,
 * so red), which retired the old bespoke ``.post-form-cancel`` rule. */

/* The Publish button is the primary action on the compose form and on a
 * draft's edit form; Save draft / Save changes stays the neutral default
 * beside it. Accent FILL + white text gives Publish visual primacy — a
 * deliberately heavier treatment than the outlined ``.btn btn-primary``
 * used for primary submits elsewhere, kept scoped to the post form. The
 * accent ``#1a4f8b`` matches the focus-ring colour used across the site.
 * Class specificity (0,1,0 / 0,1,1 for the hover) beats the base
 * ``button`` rules (0,0,1) regardless of source order, so these win. */
.post-form-publish {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--on-accent);
}

.post-form-publish:hover:not(:disabled) {
    background: var(--accent-hover);
}

/* Markdown preview panel — the post/riff editor's "Preview" button. Framed
 * like a reading surface so it reads as the finished card. The inner
 * ``.post-body`` reuses the reading-body typography: most prose rules
 * (blockquote / pre / code / links / headings) are global element selectors
 * and already apply here; the reading paragraph gap, however, is scoped to
 * ``.container--post-detail`` (which the compose form is not inside), so we
 * re-add just that one rule below. */
.markdown-preview {
    margin-top: 1rem;
    padding: 1rem 1.25rem;
    background: var(--surface);
    border: 1px solid var(--border-subtle);
    border-radius: 4px;
}

.markdown-preview .post-body p {
    margin-bottom: 1.5em;
}

.markdown-preview-empty {
    margin: 0;
    color: var(--text-muted);
    font-style: italic;
}

.post-compose-blurb,
.post-edit-blurb,
.drafts-inbox-blurb {
    color: var(--text-muted);
}

/* ----- Post detail page (post_detail.html, Task 7.x).
 *
 * ``.post-author-actions`` carries the Edit link and up to four
 * tiny ``<form>``s (Publish / Hide / Unhide / Delete). Each
 * ``<form>`` is block-level by default, so without ``flex-wrap``
 * either the buttons stack with one per row (acceptable but
 * verbose) or — if a future refactor changes the layout — the
 * buttons run off-screen.
 *
 * Same pattern for ``.post-affordances`` (the Riff / Comment
 * action links): default block forms wrap awkwardly; flex-wrap
 * gives a clean row that breaks tidily on narrow viewports. */
.post-author-actions,
.post-affordances {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
    margin: 1rem 0;
    /* The action rows (edit / publish / hide / delete; riff / comment) read in
     * the mono "label" voice — their links and buttons inherit this. */
    font-family: var(--font-mono);
}

.post-author-actions .post-action-form {
    /* Strip the default ``<form>`` block margin so each form
     * behaves as a cell in the wrapping row, not a paragraph. */
    margin: 0;
}

/* The Edit affordance is a navigation link (it loads the edit
 * page rather than POSTing a transition), so it stays an
 * ``<a>`` rather than a ``<form><button>``. But visually it
 * sits alongside Publish / Hide / Unhide / Delete in the same
 * action row, and rendering it as a bare blue underlined link
 * next to four outlined buttons reads as inconsistent. This
 * rule mirrors the global ``button`` rule's box / typography
 * so the link presents as a peer of the surrounding buttons
 * — same border, padding, focus ring, hover affordance. */
.post-author-actions .post-action-link {
    display: inline-flex;
    align-items: center;
    font-family: inherit;
    font-size: 1rem;
    padding: 0.5rem 0.85rem;
    border: 1px solid var(--border);
    border-radius: 3px;
    background: var(--surface-raised);
    color: inherit;
    text-decoration: none;
    min-height: 2.5rem;
    box-sizing: border-box;
}

.post-author-actions .post-action-link:hover {
    background: var(--surface-muted);
}

/* The Delete form's button gets a destructive accent (the muted
 * #c0392b ``--danger``) — the "this is the cautious one" cue without
 * going full-red. Left on its own rule rather than the shared
 * ``.btn btn-danger`` because it sits among the other bespoke
 * post-author-action controls. */
.post-action-form-delete button {
    border-color: var(--danger);
    color: var(--danger);
}

/* Affordance buttons (thread-view UI refresh).
 *
 * Pre-refresh: rectangular outlined "Riff" / "Comment" / "Like"
 * buttons living in their own row above the compose-area. They
 * read as primary action targets but ate vertical space and
 * competed with the body text.
 *
 * Post-refresh: small, unobtrusive icon buttons (pen / speech /
 * heart) tucked into the bottom-right of every post / riff card.
 * The mockup specifies "small and unobtrusive icons ... not
 * buttons with text". Border + background are gone; tap target
 * stays generous via padding so the icons remain easy to click /
 * tap. The colored hover / focus state preserves the "this is
 * interactive" cue without the visual weight of a permanent
 * outline.
 *
 * The minimum size keeps WCAG-recommended 44×44 tap-target hygiene
 * on touch viewports without forcing visible chrome — padding
 * stretches the click area beyond the glyph itself. */
.affordance {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2rem;
    min-height: 2rem;
    padding: 0.25rem 0.4rem;
    text-decoration: none;
    color: var(--text-muted);
    border: 0;
    background: transparent;
    cursor: pointer;
    /* Action affordances (like/subscribe counts etc.) read in the mono
     * "label" voice. */
    font-family: var(--font-mono);
}

.affordance:hover,
.affordance:focus-visible {
    color: var(--accent);
    background: var(--accent-tint);
}

.affordance-glyph {
    font-size: 1.15em;
    line-height: 1;
}

/* The Publish affordance on a draft post card is a text button, not an
 * icon: publishing is a deliberate, named state change, so it reads as
 * the primary action in the row. Filled to stand out from the quiet
 * icon affordances around it, and matched to ``.post-form-publish`` on
 * the compose/edit form so "Publish" looks the same wherever it appears. */
.affordance-publish {
    min-width: auto;
    padding: 0.2rem 0.6rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--on-accent);
    background: var(--accent);
    border-radius: 4px;
}

.affordance-publish:hover,
.affordance-publish:focus-visible {
    color: var(--on-accent);
    background: var(--accent-hover);
}

/* SVG icons inside affordance glyphs: display inline and sink
 * slightly to optically align with the existing Unicode text
 * glyphs (✒, ♡) that sit on the text baseline. */
.affordance-glyph svg {
    display: inline-block;
    vertical-align: -0.15em;
}

/* ``post-state-badge`` is inline metadata about the post; styling
 * it lightly keeps it visible without competing with the title.
 * The legacy ``.post-byline`` rule was a header-level paragraph
 * margin; the byline now lives inside ``.post-footer`` (see the
 * post-footer block further down). */

.post-state-badge,
.contribution-state-badge,
.pseud-profile-post-state {
    color: var(--text-subtle);
    font-size: 0.9em;
}

/* Tag list (Task 8.1): pills that wrap onto multiple rows on narrow
 * viewports. Each pill links to the tag's listing page
 * (``/tags/<name>/``, ``works:tag-detail``); the pill chrome lives on the
 * ``<li>`` and the ``<a>`` just inherits its colour so the whole chip
 * reads as one tappable target rather than a default underlined link. */
.post-tags {
    list-style: none;
    padding: 0;
    margin: 1rem 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}

.post-tag {
    background: var(--accent-tint);
    border-radius: 999px;
    padding: 0.15rem 0.65rem;
    color: var(--accent);
    font-size: 0.9em;
}

.post-tag-link {
    color: inherit;
    text-decoration: none;
}

.post-tag-link:hover,
.post-tag-link:focus {
    text-decoration: underline;
}

/* User-authored content surfaces. Long URLs, long handles, and
 * long inline ``<code>`` spans can blow past the container width
 * if their parent doesn't break them — that's the leading cause
 * of "horizontal scroll on a single page" on a 380px viewport
 * for a content-heavy site. ``overflow-wrap: break-word`` lets
 * the browser break inside a long token rather than letting it
 * push the line off-screen. We scope to the content surfaces
 * (post body, contribution content, pseud bio) rather than
 * applying globally so we don't break ``<code>`` inside the
 * keynav-help-modal mid-shortcut. */
.post-body,
.contribution-content,
.pseud-profile-bio {
    overflow-wrap: break-word;
    word-wrap: break-word;
}

/* ``<pre>`` inside any of the above already scrolls horizontally
 * (set on line 126 of this file). Belt-and-braces: clamp ``<img>``
 * widths so an oversized image (Markdown-embedded) can't blow up
 * the layout either. */
.post-body img,
.contribution-content img,
.pseud-profile-bio img {
    max-width: 100%;
    height: auto;
}

/* ----- Drafts inbox (Task 7.5, drafts_inbox.html). Each row
 * holds title (linked) + author + updated time + edit link. On
 * desktop the inline group is fine; at 380px the title alone can
 * fill the row, so we make the title take a full row and wrap
 * the metadata underneath. */
.drafts-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.drafts-list-item {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 0.75rem;
    align-items: baseline;
    padding: 0.75rem;
    border: 1px solid var(--border-subtle);
    border-radius: 4px;
    background: var(--surface-raised);
}

.drafts-list-title {
    /* ``flex: 1 1 100%`` puts the title on its own row, above the body
     * preview and the timestamp / Edit metadata. */
    flex: 1 1 100%;
    font-weight: 600;
}

.drafts-list-updated {
    color: var(--text-muted);
    font-size: 0.9em;
    /* Timestamp reads in the mono face, like dates elsewhere. */
    font-family: var(--font-mono);
}

.drafts-list-edit {
    /* Push the edit link to the right end of the metadata row on
     * desktop; on mobile the auto-margin collapses harmlessly. */
    margin-left: auto;
}

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

/* ----- Pseud profile (Task 7.6, pseud_profile.html). Stacks a
 * heading, optional bio, and a list of posts. Each post row is
 * a title + state badge + date — same horizontal-overflow risk
 * as the drafts list, treated the same way. */
.pseud-profile-header {
    margin-bottom: 1rem;
    /* A slim accent in the profile owner's identity colour, so a profile
       echoes the same colour that borders their posts and riffs. The
       template sets --card-author-color from display_color (always a hex);
       the fallback keeps the rule sane if it is ever unset. */
    border-left: 4px solid var(--card-author-color, var(--rv-active-border-color));
    padding-left: 0.75rem;
}

.pseud-profile-handle {
    /* The handle is the profile heading — the sole shown public identity. */
    margin: 0;
}

.pseud-profile-bio {
    margin: 1rem 0;
}

.pseud-profile-posts {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 1.3rem;
}

.pseud-profile-posts-empty {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 0.85rem;
}

/* ---- Post cards (home / profile / tag listings) -----------------------
 *
 * The ``.post-cards`` modifier (added by _post_list.html for every surface
 * EXCEPT the notifications inbox's preview-only rows) turns each <li> into a
 * card: an author-colour accent bar down the left, the head (serif title +
 * mono "by <handle>") on top, then a "lower" flexbox holding the body excerpt
 * and a mono meta block (tree stats + dates). The lower block is side by side
 * on desktop and stacked (meta last) on mobile. Inbox preview rows never get
 * ``.post-cards`` and so keep the bare base styling above. */
.post-cards {
    /* Cards separate via their own top borders, not list gap. */
    gap: 0;
}

.post-cards .pseud-profile-post {
    padding: 1.1rem 1rem 1.1rem 0.9rem;
    border-top: 1px solid var(--border-subtle);
    /* Author-colour spine (falls back to a neutral hairline if unset). */
    border-left: 4px solid var(--card-author-color, var(--border));
    /* Mobile-first: the main column (head + excerpt) then meta, stacked. The
     * ≥720 layer turns this into a two-column row (main | meta), so the meta
     * column rises to the top of the card beside the title. */
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.post-cards .pseud-profile-post:last-child {
    border-bottom: 1px solid var(--border-subtle);
}

/* The card box itself is NOT a link — only the title is — so it deliberately
 * does not light up on hover; a whole-card highlight read as the entire box
 * being clickable. The underlined title (below) carries the link affordance. */

/* Head: title + author, on its own row above the lower block. */
.post-cards .pseud-profile-post-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.25rem 0.6rem;
    margin-bottom: 0.5rem;
}

.post-cards .pseud-profile-post-title {
    font-family: var(--font-serif);
    font-weight: 700;
    font-size: 1.3rem;
    line-height: 1.2;
    color: var(--text);
    /* Underlined so the title reads unmistakably as the card's link — the card
     * box itself is not clickable. */
    text-decoration: underline;
}

/* Hover lifts the title to the brand/title accent (--brand-cool), matching every
 * link's hover. */
.post-cards .pseud-profile-post-title:hover {
    color: var(--brand-cool);
}

.post-cards .pseud-profile-post-state {
    font-family: var(--font-mono);
    font-size: 0.75rem;
}

/* "by <handle>" — mono label; the handle itself is author-coloured by the
 * shared .author-badge-handle rule (tinted from the <li>'s --card-author-color). */
.post-cards .pseud-profile-post-author {
    font-family: var(--font-mono);
    font-size: 0.8rem;
    color: var(--text-muted);
}

/* Main column: the title/author head and the body excerpt, stacked. On
 * desktop it's the left flex item beside the meta column (and takes the room
 * the meta leaves — see the ≥720 layer); on mobile it's just the first stacked
 * block. min-width:0 lets it shrink so the excerpt text can wrap/clamp rather
 * than forcing the row wider. */
.post-cards .pseud-profile-post-main {
    min-width: 0;
}

/* Mono meta: tree stats + the dates wrapper. Mobile: stats and dates share one
 * line (a · separator between them), left-aligned at the foot of the card and
 * wrapping when the card is too narrow; the min-width layer turns this back into
 * a right-aligned column. */
.post-cards .pseud-profile-post-meta {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.25rem 0.5rem;
    font-family: var(--font-mono);
    font-size: 0.8rem;
    color: var(--text-muted);
}

/* The · between the stats and the dates, sized to match the dots inside the
 * stats line. Mobile-only — the ≥720 column layout hides it. */
.post-cards .pseud-profile-post-meta-sep {
    color: var(--text-muted);
}

.post-cards .pseud-profile-post-stats {
    color: var(--text);
    white-space: nowrap;
}

/* Dates: started + updated share ONE line on mobile (row); the min-width
 * layer stacks them on separate lines for desktop. */
.post-cards .pseud-profile-post-dates {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    gap: 0.25rem 0.9rem;
}

.post-cards .pseud-profile-post-date {
    white-space: nowrap;
    font-family: var(--font-mono);
}

/* Tag chips — last in the meta column, on their own full-width row below the
 * stats/dates: on mobile (meta is a wrapping row) flex-basis:100% breaks them
 * onto their own line; the ≥720 layer stretches them into a right-aligned row
 * at the foot of the meta column. */
.post-cards .pseud-profile-post-tags {
    flex-basis: 100%;
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
}

/* Neutral chip: the muted meta text colour on the page background with a
 * --border outline — deliberately quieter than the accent-tinted .post-tag
 * pills on the reading page, since these sit inside the card's meta column
 * rather than as a headline row. */
.post-cards .pseud-profile-post-tag {
    display: inline-block;
    max-width: 100%;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg);
    color: var(--text-muted);
    padding: 0.05rem 0.5rem;
    text-decoration: none;
    /* A single long tag breaks within the fixed-width column rather than
     * overflowing the card. */
    overflow-wrap: anywhere;
}

.post-cards .pseud-profile-post-tag:hover,
.post-cards .pseud-profile-post-tag:focus {
    text-decoration: underline;
}

/* ---- Post-card responsive layers -------------------------------------
 *
 * ≥720px: the lower block goes side by side (excerpt | meta); the meta becomes
 * a right-aligned column and the two dates stack on separate lines. The feed
 * stays a single column of cards at every width. */
@media (min-width: 720px) {
    .post-cards .pseud-profile-post {
        padding: 1.4rem 1.25rem 1.4rem 1.15rem;
        /* Two-column row: main (left) | meta (right). flex-start aligns the
         * meta column to the top of the card, beside the title. */
        flex-direction: row;
        align-items: flex-start;
        gap: 1.75rem;
    }

    /* The main column takes the room the fixed-width meta column leaves. */
    .post-cards .pseud-profile-post-main {
        flex: 1;
    }

    .post-cards .pseud-profile-post-meta {
        /* Fixed-width right column on desktop so its content — the stats, dates,
         * and especially the tag chips — wraps WITHIN the column instead of
         * stretching the card to fit them on one line. min-width:0 is required:
         * a flex item's default min-width:auto would let it grow past the 11rem
         * basis to fit a long unbreakable tag, so the column must be told it may
         * shrink to its basis (content then wraps/overflows inside it). */
        flex: 0 0 11rem;
        min-width: 0;
        /* Back to a right-aligned column for desktop: stats on top, dates
         * stacked beneath — the mobile · separator is dropped. */
        flex-direction: column;
        align-items: flex-end;
        text-align: right;
    }

    .post-cards .pseud-profile-post-meta-sep {
        display: none;
    }

    .post-cards .pseud-profile-post-tags {
        /* In the meta column (a flex column with items right-aligned) take the
         * full width and right-align the chips, so the tag row sits at the top
         * of the column flush with the title. flex-basis:auto keeps the row's
         * height to its content (basis:100% would mean full *height* here). */
        flex-basis: auto;
        align-self: stretch;
        justify-content: flex-end;
    }

    .post-cards .pseud-profile-post-dates {
        flex-direction: column;
        align-items: flex-end;
        gap: 0.25rem;
    }
}

/* Per-entry body preview (Phase P task P.5). A short, muted,
 * plain-text excerpt under each post/riff title so a column of
 * similar "Riff on '<title>'" rows is distinguishable.
 *
 * ``flex-basis: 100%`` is load-bearing: the parent
 * ``.pseud-profile-post`` is a wrapping flex row (title + state +
 * right-aligned date), so a full-basis flex item breaks cleanly
 * onto its own line below that row rather than trying to sit
 * inline after the ``margin-left: auto`` date. The body sits flush
 * with the title's left edge; the muted color + smaller size keep
 * it unobtrusive (the title and date stay the primary row). */
.pseud-profile-post-preview {
    flex-basis: 100%;
    margin: 0.25rem 0 0;
    color: var(--text-muted-2);
    font-size: 1em;
    /* Truncate by visual rows, not characters. Each preview block
     * (paragraph / list item) is its own <p> child (see
     * _post_list.html), and ``line-clamp`` cuts the whole set at N
     * rendered rows with an auto ellipsis — it counts the children's
     * line boxes, so a long single paragraph shows its first few
     * wrapped rows while several short paragraphs / a list show as
     * separate rows. ``max-height`` is a fallback that bounds the
     * height on the rare engine that doesn't clamp block children.
     *
     * This base rule is the DEFAULT (5 rows) — it covers the drafts
     * inbox, which hand-rolls this class outside _post_list.html. The
     * feed (home / profile / tag) and the notifications inbox override
     * the row count below; they're distinguished by the ``.post-cards``
     * class the feed's <ul> carries and the notifications' does not. */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 5;
    line-clamp: 5;
    overflow: hidden;
    max-height: 9.5em;
}

/* Feed surfaces (home page, @username profile, tag detail — the
 * ``.post-cards`` list) show a taller 7-row excerpt. */
.post-cards .pseud-profile-post-preview {
    -webkit-line-clamp: 7;
    line-clamp: 7;
    max-height: 13.5em;
}

/* Notifications inbox (the preview-only list, which omits ``.post-cards``)
 * shows a shorter 3-row excerpt so a stack of rows stays scannable. */
.pseud-profile-posts:not(.post-cards) .pseud-profile-post-preview {
    -webkit-line-clamp: 3;
    line-clamp: 3;
    max-height: 5.7em;
}

/* The blocks: line spacing stays tight *within* a paragraph
 * (line-height), and the gap *between* paragraphs comes from the
 * margin — so wrapped lines of one paragraph don't get the extra
 * inter-paragraph air. */
.pseud-profile-post-preview p {
    margin: 0 0 0.45em;
    line-height: 1.4;
}

.pseud-profile-post-preview p:last-child {
    margin-bottom: 0;
}

/* Divider between the home-page welcome text and the post list. */
/* ---- Homepage masthead + sort toolbar --------------------------------
 *
 * The homepage opts <main> into the wide page frame (matching the nav's
 * inner width) via this container modifier, set by home.html's
 * ``container_modifier`` block. The @username profile does the same via
 * .container--profile (below) so its post cards render at the same width as
 * the home feed. Other pages keep the 42rem reading cap. */
.container--home,
.container--profile {
    max-width: var(--page-max);
    padding: 1rem;
}

/* Masthead: brand block (eyebrow + wordmark) and the intro blurb. Stacked on
 * mobile (column); side by side at desktop (row, see the min-width layer
 * below) with ``space-between`` so the gap opens up on wide views. NB: a
 * wrapping row would push the wide blurb onto its own line (wrap beats
 * shrink), so we switch direction rather than wrap. */
.home-hero {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    margin: 0.5rem 0rem;
    padding-bottom: 1.75rem;
}

@media (min-width: 720px) {
    .home-hero {
        flex-direction: row;
        justify-content: space-between;
        /* Top-align the columns so the blurb starts level with the eyebrow,
         * rather than bottom-aligning the blurb to the wordmark's baseline. */
        align-items: flex-start;
        gap: 3rem;
    }

    /* The brand block stays content-width (the wordmark sets it, so it can
     * never overflow into the blurb); the blurb is capped narrower than before
     * so the masthead reads roughly 2:3 (brand:blurb) instead of ~1:2. It still
     * shrinks below its intrinsic width (min-width:0), right-aligned toward the
     * container edge. Dial the ratio with this basis — smaller = the title
     * takes a bigger share. */
    .home-hero-blurb {
        flex: 0 1 30rem;
        min-width: 0;
        text-align: right;
        margin: 0;
    }

    /* Floor for the small-desktop wordmark so the masthead isn't tiny at the
     * narrow end of desktop; the eyebrow, pinned to GALLAE's width, scales up
     * with it. Qualified with .home-hero-brand so it out-specifies the base
     * .home-wordmark rule — that rule is unqualified (same specificity) but
     * sits LATER in the source, so an unqualified override here would lose on
     * source order and silently never apply. */
    .home-hero-brand .home-wordmark {
        font-size: clamp(5rem, 7vw, 6rem);
    }
}

/* The brand block holds its intrinsic width (the wordmark never wraps), so it
 * can't overflow the blurb; the blurb absorbs the row's shrinking instead (see
 * the min-width:720px block above). A single-column grid so the wordmark
 * defines the column width and the eyebrow can be pinned to match it (see
 * .home-eyebrow) — the two share one width at every viewport. */
.home-hero-brand {
    flex: 0 0 auto;
    display: grid;
    grid-template-columns: auto;
    /* Centre both layers — the eyebrow and the wordmark — and centre the track
     * itself, so they stack concentrically. On desktop the block is content-
     * width (track == container), so this is a no-op there; it only bites on
     * mobile, where the block stretches full-width and the stack would
     * otherwise sit hard left. */
    justify-items: center;
    justify-content: center;
}

/* Mono eyebrow over the wordmark — the redesign's "label" voice, in the link
 * blue. It's an inline SVG (see home.html) stretched to the GALLAE width:
 * ``width: 0`` keeps it from widening the grid column (the wordmark defines
 * that), then ``min-width`` pins it to the column width; the viewBox aspect
 * ratio sets the height, so the kicker scales with the wordmark. We inset it to
 * 96% (2% per side, centred) so it lines up with GALLAE's visible glyphs rather
 * than the slightly wider glyph bounding box (the font's side bearings); this
 * also makes the kicker ~10% shorter, since the SVG scales uniformly. Font/fill
 * live here and inherit to the <text>; the line is sized by the SVG's
 * textLength, not font metrics. */
.home-eyebrow {
    display: block;
    width: 0;
    min-width: 96%;
    justify-self: center;
    height: auto;
    margin: 0 0 0.4rem;
    font-family: var(--font-mono);
    font-weight: 500;
    letter-spacing: 0.14em;
    fill: var(--brand-red);
}
.home-eyebrow text {
    /* User units (viewBox space, height 40), not screen px — the SVG scales it.
     * Sized so the natural line is close to the 1000-unit width, keeping the
     * spacing-only stretch (lengthAdjust) modest rather than spreading wide. */
    font-size: 40px;
}

/* GALLAE in Aboreto, in the brand/title accent. The wordmark
 * slide-flips between this and a recoloured Korean SVG — see the -flip / -en /
 * -ko layers below.
 *
 * This base size governs the MOBILE masthead (the ≥720px block overrides it
 * with its own clamp). Below 720px the hero stacks into a column, so the brand
 * block stretches to the full content width and the eyebrow — pinned to 96% of
 * that width — goes wide while a 7vw GALLAE stayed narrow, decoupling the two.
 * The aggressive 24vw term grows GALLAE to roughly fill that same width so it
 * tracks the eyebrow again; the 7rem ceiling lets it keep matching the eyebrow
 * up into the small-tablet range (deliberately above the 6rem desktop max). */
.home-wordmark {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 400;
    font-size: clamp(2.75rem, 24vw, 7rem);
    line-height: 1;
    letter-spacing: 0.02em;
    color: var(--brand-cool);
    white-space: nowrap;
}

/* Clip box: width = the GALLAE text (the -en layer, in normal flow, defines
 * it), overflow hidden so each layer is cut off at the word's right edge as it
 * slides past. */
.home-wordmark-flip {
    position: relative;
    display: inline-block;
    overflow: hidden;
    vertical-align: bottom;
    cursor: pointer;
}

/* English layer: real text (sets the clip width). Centred by default; when the
 * wordmark is flipped it slides out past the word's right edge (clipped). */
.home-wordmark-en {
    display: inline-block;
    transition: transform 0.6s ease;
    will-change: transform;
}
.home-wordmark.is-flipped .home-wordmark-en {
    transform: translateX(130%);
}

/* Korean layer: the SVG recoloured to the wordmark colour via a CSS mask
 * (background = currentColor = --brand-cool, so it tracks the theme). The ggq glyph
 * art fills its viewBox (400 × 84.038 ≈ 4.76:1) and is proportioned to match the
 * GALLAE English text, so the layer spans the full clip width (= the GALLAE word
 * width) and locks its height to the SVG's own aspect ratio; ``bottom`` seats it
 * on the GALLAE baseline. Parked off-screen to the left by default (clipped);
 * slides in to centre when the wordmark is flipped. */
.home-wordmark-ko {
    position: absolute;
    left: 0;
    bottom: 0.14em;
    width: 100%;
    aspect-ratio: 400 / 84.038;
    height: auto;
    background-color: currentColor;
    -webkit-mask: url("../img/ggq.a86d4eb33804.svg") no-repeat left center;
    -webkit-mask-size: contain;
    mask: url("../img/ggq.a86d4eb33804.svg") no-repeat left center;
    mask-size: contain;
    transform: translateX(-130%);
    transition: transform 0.6s ease;
    will-change: transform;
}
.home-wordmark.is-flipped .home-wordmark-ko {
    transform: translateX(0);
}

/* Reduced-motion: keep the click-to-flip working, but swap instantly (no slide). */
@media (prefers-reduced-motion: reduce) {
    .home-wordmark-en,
    .home-wordmark-ko {
        transition: none;
    }
}

/* Mobile: a normal full-width block under the wordmark. The desktop layer
 * below sizes + right-aligns it as the row's right column. (Keeping the
 * ``flex``/``text-align`` here would misfire in the mobile column flex — a
 * 50rem flex-basis becomes a height and blows a huge gap.) */
.home-hero-blurb {
    font-family: var(--font-serif);
    font-size: 0.9rem;
    line-height: 1.5;
    color: var(--text);
    /* Inset the blurb from the viewport edges when stacked under 720px; the
     * desktop block resets this since the blurb is flex-sized there. */
    margin: 0 1rem;
}

.home-hero-blurb p {
    margin: 0 0 0.6em;
}

.home-hero-blurb p:last-child {
    margin-bottom: 0;
}

/* The blurb's "we" (about link) reads as an aside — italic + underlined. */
.home-hero-blurb a {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* Hover lifts the blurb link to the brand/title accent (--brand-cool), matching
 * every link's hover. */
.home-hero-blurb a:hover {
    color: var(--brand-cool);
}

/* Search + sort toolbar. Border scheme: the bar (.home-controls) carries only a
 * top + bottom rule; the Search submit (.home-search-submit) is bracketed by a
 * left + right rule; nothing else has a border — the search form, the input,
 * and the sort <select> are all borderless. All rules are 2px --border.
 * Everything is mono; cells light up --bg-nav on hover / focus. */
.home-controls {
    display: flex;
    align-items: stretch;
    /* Wrap rather than overflow when the cells can't fit (narrow mobile): the
     * sort form drops to a second row inside the top/bottom frame. */
    flex-wrap: wrap;
    border-top: 2px solid var(--border);
    border-bottom: 2px solid var(--border);
    background: transparent;
    font-family: var(--font-mono);
}

/* The search control (input + Search button) fills the row so the input can
 * stretch. Borderless itself — the only vertical rules in the bar bracket the
 * Search submit (see .home-search-submit). */
.home-search {
    display: flex;
    flex: 1;
    min-width: 0;
}

/* Borderless input — the search control's frame is the only border; lights up on focus. */
.home-search-input {
    flex: 1;
    width: 100%;
    min-width: 0;
    border: none;
    background: transparent;
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 0.9rem;
    padding: 0.85rem 1.25rem;
    transition: background-color 0.12s ease;
}

.home-search-input::placeholder {
    color: var(--text-muted);
}

.home-search-input:hover,
.home-search-input:focus {
    background: var(--bg-nav);
}

.home-search-input:focus-visible {
    outline: 2px solid var(--brand-cool);
    outline-offset: -2px;
}

/* Middle cell: the "SEARCH" submit. A deliberate toolbar segment, not a
 * ``.btn`` pill (which wouldn't match the bar) — kin to the nav's link-styled
 * logout button. Bracketed by a left + right rule — the bar's only vertical
 * borders. */
.home-search-submit {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex: 0 0 auto;
    padding: 0 1.5rem;
    border: none;
    border-left: 2px solid var(--border);
    border-right: 2px solid var(--border);
    /* Square corners — override the global button's 3px radius so the bracket
     * sits flush in the bar. */
    border-radius: 0;
    background: transparent;
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 0.85rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background-color 0.12s ease;
}

.home-search-submit:hover,
.home-search-submit:focus-visible {
    background: var(--bg-nav);
}

.home-search-submit:focus-visible {
    outline: 2px solid var(--brand-cool);
    outline-offset: -2px;
}

/* Rightmost cell: "SORT:" label + the value <select>. Borderless — the search
 * control's right border sits to its left; lights up on hover / when the select
 * is focused. */
.home-sort-form {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex: 0 0 auto;
    padding: 0 1.4rem;
    transition: background-color 0.12s ease;
}

/* Light up to ``--surface-muted`` on hover/focus, not ``--bg-nav``: the sort
 * cell is a borderless <form>, so it can't borrow the visible lift the adjacent
 * Search <button> gets from the global ``button:hover`` rule (which lands on
 * ``--surface-muted``). ``--bg-nav`` here is nearly indistinguishable from the
 * page surface in the dark theme, so the hover read as barely-there next to the
 * button; ``--surface-muted`` matches the button's lit colour. */
.home-sort-form:hover,
.home-sort-form:focus-within {
    background: var(--surface-muted);
}

/* Keyboard focus ring on the whole cell: the real <select> is opacity 0, so its
 * own outline can't show — surface it on the cell instead, but only for
 * keyboard focus (``:focus-visible``), not a mouse click. */
.home-sort-form:has(.home-sort:focus-visible) {
    outline: 2px solid var(--brand-cool);
    outline-offset: -2px;
}

.home-sort-label {
    display: flex;
    align-items: center;
    font-size: 0.85rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
    white-space: nowrap;
}

/* The icon glyphs only appear on narrow phones (see the max-width: 559px block,
 * which swaps the "Search" / "Sort:" text for them); hidden on wider screens. */
.home-search-submit svg,
.home-sort-label svg {
    display: none;
}

/* The visible sort value: plain mono text, no native chrome or caret (matches
 * the spec — no dropdown chevron). The real <select> is invisible and overlaid;
 * this span is what the reader sees. */
.home-sort-value {
    font-family: var(--font-mono);
    font-size: 0.85rem;
    color: var(--text);
}

/* The native <select>, overlaid transparently across the whole cell so a click
 * anywhere in the box (the "Sort:" label, the value, the padding) opens the
 * dropdown — not just the value text. ``opacity: 0`` (not visibility/display)
 * keeps it clickable and focusable. ``data-autosubmit`` reorders the feed on
 * change. The visible value lives in ``.home-sort-value`` above. */
.home-sort {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    border: none;
    background: transparent;
    opacity: 0;
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
}

/* Narrow phones: rather than wrap the sort form onto a second row, keep the
 * search + sort cells on one line by collapsing their text labels to icons —
 * "Search" becomes a magnifying glass, "Sort:" an ascending-sort glyph. The
 * tighter padding buys the bit of width the swap needs to stay single-row. */
@media (max-width: 559px) {
    .home-search-submit-text,
    .home-sort-label-text {
        display: none;
    }

    .home-search-submit svg,
    .home-sort-label svg {
        display: block;
        font-size: 1.05rem;
    }

    /* Tighten every cell's horizontal padding so the icon-collapsed row has
     * room to stay single-line on the narrowest phones. */
    .home-search-input {
        padding-left: 0.5rem;
        padding-right: 0.5rem;
    }

    .home-search-submit {
        padding: 0 0.5rem;
    }

    .home-sort-form {
        padding: 0 0.5rem;
    }
}

/* Home-page post-list pagination nav (prev / page-count / next).
 * Sits below the post list; arrows are links when the page exists,
 * muted spans when at a boundary. */
.home-pagination {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-top: 1.5rem;
    font-size: 0.95em;
}

.home-pagination-prev,
.home-pagination-next {
    text-decoration: none;
    font-size: 1.2em;
    line-height: 1;
}

.home-pagination-disabled {
    color: var(--text-faintest);
    cursor: default;
}

.home-pagination-info {
    color: var(--text-muted);
}

/* Post-card metadata (.pseud-profile-post-stats / -dates / -author) is
 * styled in the ``.post-cards`` block above — tree stats + dates live in the
 * right-aligned mono meta column, the "by <handle>" author line sits in the
 * card head. The handle itself renders through _author_badge.html, so its
 * author-colour + hover styling lives on .author-badge-handle (it tints from
 * the <li>'s --card-author-color). */

/* Owner-only rename / delete footer at the bottom of the
 * profile page. Muted + small so it doesn't compete with the
 * post list above — these are rarely-used management actions,
 * not primary affordances. */
.pseud-profile-edit-actions {
    margin-top: 5rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--border-faint);
    font-size: 0.9em;
    color: var(--text-link-muted);
}

.pseud-profile-edit-actions a {
    color: var(--text-link-muted);
}

/* Profile tabs (Phase G — thread-view UI refresh; labels
 * refreshed in Phase P task P.1).
 *
 * Two chip-like tabs above the post list: "Posts" and
 * "Posts & Riffs". The ``text-transform: uppercase`` below
 * renders the chip labels as "POSTS" / "POSTS & RIFFS" even
 * though the template source is mixed-case. The active tab
 * gets a darker background + border to read as the current
 * selection; the inactive tab is subtle so the tab pair
 * doesn't compete with the bio or the post titles below.
 *
 * Tabs are anchors (each one navigates via querystring), not
 * buttons — they're not opening a panel, they're switching
 * URLs. The ``aria-current="page"`` attribute on the active
 * anchor is what carries the "this is the selected tab"
 * semantic for assistive tech. */
.profile-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    /* Bottom margin is larger than the top (Phase P task P.2): the
       tab pair should read as its own group above the list, not
       flush against the first entry. The empty-state ``<p>`` picks
       up the same gap so the page still breathes when there are
       no posts. */
    margin: 1rem 0 1.75rem;
}

.profile-tab {
    display: inline-block;
    padding: 0.35rem 0.85rem;
    border: 1px solid var(--border);
    border-radius: 0.4rem;
    background: var(--surface-raised);
    color: var(--accent);
    font-size: 0.85em;
    letter-spacing: 0.04em;
    text-decoration: none;
    text-transform: uppercase;
}

.profile-tab:hover,
.profile-tab:focus-visible {
    background: var(--accent-tint);
}

.profile-tab-active,
.profile-tab[aria-current="page"] {
    background: var(--accent-strong);
    color: var(--on-accent);
    border-color: var(--accent-strong);
}

/* ----- Profile social-action buttons (Block / Mute / Follow, and the
 * Unblock / Unmute / Unfollow toggles). They borrow the outlined pill
 * look of the profile tabs above so the profile surface reads as one
 * consistent set of controls rather than raw browser buttons.
 *
 * ``.social-actions`` is the HTMX swap anchor for the whole area and is
 * reused by the blocked / unavailable banner branches too — those are
 * not button rows, so the override below puts them back to block flow
 * (paragraph stacked above its button). The muted banner sits *inside*
 * the button row, so it takes a full-width line of its own above the
 * buttons. */
.social-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin: 1rem 0;
}

.social-blocked-banner,
.social-unavailable-banner {
    display: block;
}

.social-actions .social-muted-banner {
    flex-basis: 100%;
}

.social-action-form {
    margin: 0;
}

.social-action-form button {
    display: inline-block;
    padding: 0.35rem 0.85rem;
    border: 1px solid var(--border);
    border-radius: 0.4rem;
    background: var(--surface-raised);
    color: var(--accent);
    font: inherit;
    font-size: 0.85em;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    cursor: pointer;
}

.social-action-form button:hover,
.social-action-form button:focus-visible {
    background: var(--accent-tint);
}

/* Block is the one destructive control in the row — a red accent sets
   it apart from the neutral Mute / Follow buttons while keeping the
   same outlined shape. */
.social-action-block button {
    color: var(--danger-strong);
    border-color: var(--danger-strong);
}

.social-action-block button:hover,
.social-action-block button:focus-visible {
    background: var(--error-bg-hover);
}

/* ----- Search page (Task 8.2, search.html). The form is an
 * input + submit button; on mobile we want them to break onto
 * separate rows when the input alone fills the width.
 *
 * Result lists use the same row-with-title-on-its-own-line
 * pattern as the drafts inbox so the byline / kind-badge /
 * context don't get squeezed. */
.search-form {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: stretch;
    margin: 0.75rem 0 1.5rem;
}

.search-form > label {
    flex: 1 1 100%;
    font-weight: 600;
    font-family: var(--font-sans);
}

.search-form input[type="search"] {
    /* ``min-width: 0`` prevents the input from refusing to shrink
     * below its intrinsic content width — the standard flex
     * gotcha that otherwise re-introduces overflow on narrow
     * viewports. */
    flex: 1 1 60%;
    min-width: 0;
}

.search-form button {
    flex: 0 0 auto;
}

/* Syntax hint under the search box. ``flex: 1 1 100%`` drops it onto its
 * own full-width row beneath the input + button; the muted colour and
 * smaller size keep it unobtrusive — a hint, not a heading. */
.search-form-help {
    flex: 1 1 100%;
    margin: 0;
    color: var(--text-muted);
    font-size: 0.85em;
    /* Explanatory search-syntax hint reads in the sans face, not the body serif. */
    font-family: var(--font-sans);
}

.search-results {
    margin: 1.5rem 0;
}

.search-results-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.search-result {
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--border-faint);
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 0.5rem;
    align-items: baseline;
}

.search-result-title {
    /* Title gets its own row so the byline lands underneath
     * rather than being pushed off-screen by a long title. */
    flex: 1 1 100%;
    font-weight: 600;
}

.search-result-byline,
.search-result-kind {
    color: var(--text-muted);
    font-size: 0.9em;
}

/* ts_headline match snippet (Phase P task P.6). ``flex-basis: 100%``
 * drops it onto its own full-width row below the title/byline —
 * same pattern ``.search-result-title`` uses, since the parent
 * ``.search-result`` is a wrapping flex row. The matched terms are
 * wrapped in ``<mark>`` by the view; the soft amber below replaces
 * the browser's harsh default yellow so it reads as a deliberate
 * highlight rather than a rendering artifact. */
.search-result-snippet {
    flex-basis: 100%;
    margin: 0.25rem 0 0;
    color: var(--text-muted);
    font-size: 0.9em;
    line-height: 1.4;
}

.search-result-snippet mark {
    background: var(--highlight-search);
    /* Pin the text color: the UA default for <mark> is black, which
     * is unreadable on the dark-amber highlight in dark mode. */
    color: var(--text);
    padding: 0 0.1em;
    border-radius: 0.15em;
}

.search-results-cap-note,
.search-results-empty {
    color: var(--text-muted);
    font-size: 0.95em;
}

/* ----- Notifications inbox (Task 8.3, notifications/inbox.html
 * + _inbox_row.html). Each row is a content block + (a tiny
 * mark-read form OR a "read" marker). Without flex layout the
 * form drops to its own row by default; with flex the row reads
 * as one unit on desktop and wraps cleanly on mobile. */
.inbox-summary {
    color: var(--text-muted);
    margin: 0 0 1rem;
}

.notification-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.notification-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: flex-start;
    padding: 0.75rem;
    border: 1px solid var(--border-subtle);
    border-radius: 4px;
    background: var(--surface-raised);
}

.notification-content {
    /* Take the lion's share of the row; ``min-width: 0`` so a
     * long title can wrap rather than push the mark-read button
     * off the row. */
    flex: 1 1 60%;
    min-width: 0;
}

/* Title + read/unread toggle share one flex row: the title takes the available
 * width and wraps; the small button holds its size at the top-right. */
.notification-title-row {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
}

.notification-title-row .notification-title {
    flex: 1 1 auto;
    min-width: 0;
}

/* The toggle form is the flex item (it wraps the button). ``flex: 0 0 auto`` so
 * it never shrinks — the title wraps instead — and a small negative margin pulls
 * it up and toward the card's right edge, reclaiming room for a roomier button
 * (it otherwise reads cramped). */
.notification-mark-form {
    flex: 0 0 auto;
    margin: -0.3rem -0.3rem 0 0;
}

/* The read/unread toggle is a compact .btn in the mono "label" voice: shrunk
 * from the default control height, with a touch more padding than the title row
 * would otherwise allow. ``white-space: nowrap`` keeps "mark unread" on one line
 * (the title wraps instead). */
.notification-mark {
    min-height: 0;
    padding: 0.25rem 0.6rem;
    font-family: var(--font-mono);
    font-size: 0.7em;
    white-space: nowrap;
    /* Half-strength fill at rest (text + border stay full opacity — hence
     * color-mix on the background rather than ``opacity``); the .btn:hover rule
     * restores a solid fill on hover. */
    background: color-mix(in srgb, var(--surface-raised) 50%, transparent);
}

/* The headline reads as plain body text by default (theme text colour, normal
 * weight); the two things that stand out — the story/card title link and the
 * actor usernames — carry their own emphasis below. */
.notification-title {
    margin: 0 0 0.25rem;
    font-weight: normal;
    color: var(--text);
}

/* The post/story-card title is the link: black (theme text), bold, underlined,
 * flaring to the cool link/title accent (--brand-cool) on hover, matching link
 * hover everywhere else. */
.notification-title a {
    color: var(--text);
    font-weight: bold;
    text-decoration: underline;
}

.notification-title a:hover {
    color: var(--brand-cool);
}

/* Actor usernames in a notification headline reuse the canonical byline
 * treatment (see .author-badge-handle): bold, in the author's own identity
 * colour (--card-author-color, set inline per name), flaring to the theme text
 * colour on hover so an oddly light/dark custom colour stays legible. Direct
 * rules on the span beat the enclosing link's colour, so a name keeps its own
 * colour even when the whole title is a link. */
.notification-username {
    color: var(--card-author-color, currentColor);
    font-weight: bold;
}

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

.notification-description {
    margin: 0 0 0.25rem;
}

/* Follow-channel provenance line ("· because you follow <name>"), shown only
 * on rows the reader gets because they follow the actor. Muted, sits just
 * under the headline; the username chip inside keeps its own identity colour. */
.notification-follow-hint {
    margin: 0 0 0.25rem;
    color: var(--text-muted);
}

.notification-meta {
    margin: 0;
    color: var(--text-muted);
    /* The row's timestamp reads in the mono face, like dates elsewhere. */
    font-family: var(--font-mono);
}

/* Collapsed like-group overflow ("…and N other users"). The <details>
 * has to flow inside the headline sentence, so override the browser's
 * block default to inline; the <summary> reads as a link (underlined,
 * accent-coloured, pointer cursor) since that's what the spec calls the
 * "N other users" affordance. When open, the full liker list drops below
 * as a normal bulleted block. The default disclosure triangle is hidden
 * so the summary looks like inline prose, not a widget. */
.like-overflow {
    display: inline;
}

.like-overflow > summary {
    display: inline;
    cursor: pointer;
    color: var(--accent);
    text-decoration: underline;
    list-style: none;
}

/* Chrome/Safari render the triangle via this pseudo-element; Firefox via
 * ``list-style`` above. Hide both so the summary is bare text. */
.like-overflow > summary::-webkit-details-marker {
    display: none;
}

.like-overflow-list {
    margin: 0.25rem 0 0;
    padding-left: 1.25rem;
    font-weight: 400;
    color: var(--text-muted);
}

/* Read rows carry no fill — they match the page background (``--surface``, what
 * <body> paints), so only the row's thin border (on ``.notification-row``)
 * distinguishes them. A super row that has been read reverts here too, because
 * the super tint below is scoped to UNREAD rows and no longer matches once the
 * row is read. */
.notification-read {
    background: var(--surface);
}

/* Unread rows get a solid warm tint (per-theme; see the --notif-* tokens) so
 * they stand out in the unified feed. */
.notification-unread {
    background: var(--notif-unread-bg);
}

/* Super-grade rows: the louder tier — a gradient fill (see --notif-super-bg).
 * Scoped to UNREAD super rows so a super row reverts to the read fill once read;
 * the compound selector also out-specifies ``.notification-unread`` so an unread
 * super row shows the gradient. */
.notification-unread.notification-super {
    background: var(--notif-super-bg);
}

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

/* Inbox controls bar: "Mark all as read" on the left, the "messages
 * per page" dropdown on the right. ``space-between`` splits them; on
 * a narrow screen they wrap to two rows. */
.inbox-controls {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin: 0 0 1rem;
}

/* Match the per-row read/unread toggle's mono "label" voice (.notification-mark)
 * — just the font, not its compact sizing. */
.inbox-mark-all {
    font-family: var(--font-mono);
}

/* The per-page dropdown is an inline label + ``<select>`` that
 * overrides the global ``select { width: 100% }`` and block-label
 * rules so the control sizes to its option text. (It used to mirror
 * a ``.post-detail-sort`` control on the post page, since removed
 * when riff sort became a user-level setting.) */
.inbox-per-page {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0;
}

.inbox-per-page label {
    display: inline-block;
    margin: 0;
    font-size: 0.9rem;
    color: var(--text-muted);
}

.inbox-per-page select {
    width: auto;
    max-width: 100%;
    font-size: 0.9rem;
    padding: 0.35rem 0.5rem;
}

/* Notifications settings: each <select> sits INLINE inside its sentence
 * rather than the usual label-above-widget row, so it overrides the global
 * ``select { width: 100% }`` and sizes to its option text, sitting on the
 * text baseline. The list is numbered to mirror the spec's six settings. */
.inline-settings {
    list-style: decimal;
    padding-left: 1.5rem;
    max-width: 60ch;
}

.inline-settings li {
    margin: 0.75rem 0;
    line-height: 1.3;
}

.inline-settings select {
    width: auto;
    max-width: 100%;
    display: inline;
    vertical-align: baseline;
    padding: 0;
    border: none;
    border-radius: 5px;
    background: var(--surface-raised);
}

/* Pagination nav: centered Previous / "Page X of Y" / Next. Links
 * inherit the site's default anchor styling; the status text and the
 * disabled (no prev / no next) ends are muted. */
.inbox-pagination {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    margin: 1rem 0 0;
    font-size: 0.9rem;
}

.inbox-pagination-status {
    color: var(--text-muted);
}

.inbox-pagination-disabled {
    color: var(--text-faint);
}

/* ----- Notification preferences settings (Task 8.3,
 * accounts/notification_preferences.html). The form renders each
 * field as ``<label>{{ field }} {{ field.label }}</label>`` —
 * the checkbox sits inline with its label, which is the right
 * UX for a binary toggle; we just need to keep the global
 * form ``p > label`` block-display rule from breaking the
 * inline checkbox+label pairing.
 *
 * The override below: when a label *contains* a form control
 * (the checkbox + label-text pattern from the preferences page),
 * keep it inline. Identifying these via ``:has(input)`` lets
 * the rule self-target without coupling to the preferences
 * page's class names. */
form p > label:has(input) {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    font-weight: normal;
    margin-bottom: 0;
}

/* Danger-zone heading on the preferences page: the destructive
 * deactivate-account link sits under it, so the heading itself is
 * red to telegraph "the controls below are irreversible". */
.danger-zone-heading {
    color: var(--danger);
}

.form-saved {
    color: var(--success-text);
    background: var(--success-bg);
    border-left: 3px solid var(--success-border);
    padding: 0.5rem 0.75rem;
    margin: 0 0 1rem;
}

/* ----- Contribution tree (Task 5.4 + 5.7, _contribution.html).
 * Each level adds ``margin-left + padding-left + border-left``
 * for the visible-parentage cue. On desktop the cumulative
 * indent is fine; on mobile a level-6 contribution can be
 * pushed entirely off the right edge.
 *
 * The mobile rule below halves the indent so deep trees stay
 * readable; the existing 1rem padding kicks back in at the
 * existing 720px breakpoint. */
.contribution-children {
    /* Override the 1rem in the Task 5.4 block above for narrow
     * viewports. The existing border-left + margin-left on the
     * same selector still apply (both still tiny). */
    padding-left: 0.5rem;
}

/* Footer-row layout for posts and riffs (thread-view UI refresh).
 *
 * Pre-refresh: the byline lived in a header at the top of the
 * card; the affordances lived in their own row below the body.
 * Post-refresh: a single flex row at the bottom of the card with
 * byline + timestamp + state badge on the left and the icon
 * strip (pen / speech / heart + count) on the right.
 *
 * ``flex-wrap: wrap`` keeps narrow viewports readable — if the
 * byline grows past the card width the affordances fall to a new
 * line below, never out-of-frame. The space-between justification
 * is what produces the left / right split when both children fit
 * in one row. */
.contribution-footer,
.post-footer {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 0.75rem;
    justify-content: space-between;
    align-items: center;
    margin-top: 0.75rem;
}

/* Contribution identity band: author left, date + permalink right.
 * flex-wrap lets the date fall below the author on very narrow cards
 * rather than overflowing. */
.contribution-identity {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.25rem 0.5rem;
}

.contribution-byline,
.post-byline {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 0.5rem;
    align-items: baseline;
    color: var(--text-muted);
    font-size: 0.9em;
    margin: 0.5rem 0;
}

.contribution-time,
.post-time {
    /* margin-left: auto pushes the date (and everything after it —
     * the state badge, edit-history controls, permalink) to the
     * right, keeping the author name left-aligned. */
    margin-left: auto;
    color: var(--text-subtle);
    font-size: 0.9em;
    font-variant-numeric: tabular-nums;
    font-family: var(--font-mono);
}

.contribution-kind-badge {
    color: var(--text-muted);
    font-size: 0.9em;
}

.contribution-affordances,
.post-affordances {
    display: flex;
    flex-wrap: nowrap;
    gap: 0.25rem;
    align-items: center;
    justify-content: flex-start;
    margin: 0.5rem 0 0;
}

/* Spacer between the left (riff + comment) and right (edit/trash/
 * like) icon groups. flex: 1 absorbs all remaining space so the
 * two groups sit at opposite ends of the row. */
.affordance-spacer {
    flex: 1;
}

/* Delete form in the card action row — strip block margin so the
 * <form> element sits flush in the flex row like a link. */
.card-delete-form {
    margin: 0;
}

/* Like affordance (thread-view UI refresh).
 *
 * The form is rendered as a flex row so the heart glyph and the
 * adjacent count read as a unit (per the mockup: "put the count
 * to the right of the like action"). The button itself shares
 * the ``.affordance`` shape (transparent, no border) with the
 * pen / speech siblings; the only state-driving sibling rule is
 * the filled-color variant on ``.affordance-like-active`` below.
 *
 * The anonymous static branch reuses the same shape — same flex
 * row, same glyph + count — so the surface reads consistently
 * regardless of auth state. */
.affordance-like {
    display: inline-flex;
    align-items: center;
    gap: 0.2rem;
    margin: 0;
}

.affordance-like-button {
    /* Inherit ``.affordance`` (icon-button) styling. The pre-
     * refresh outlined-pill look is gone; the heart glyph carries
     * the meaning. */
    font-family: inherit;
}

.affordance-like-active .affordance-like-button {
    /* Filled-heart state. Red-ish accent so a glance at the
     * affordance row tells the viewer "I've already liked this"
     * without depending on the glyph swap alone. */
    color: var(--danger);
}

.affordance-like-active .affordance-like-button:hover,
.affordance-like-active .affordance-like-button:focus-visible {
    color: var(--danger);
    background: var(--error-bg-hover);
}

.affordance-like-static {
    color: var(--text-subtle);
}

/* Subscribe affordance (Task U.9).
 *
 * The action-row subscribe icon (🔔 unsubscribed / 🔕 subscribed)
 * sits alongside the existing Riff / Comment / Like affordances
 * in the post footer. Same wrapper shape as ``.affordance-like``:
 * the ``<form>`` is the wrapper (so the icon and its inevitable
 * future siblings like a count badge sit in one flex row), and
 * the inner ``<button>`` is the actual ``.affordance``-styled
 * surface. We split into wrapper + button because the wrapper
 * also carries the HTMX swap-target (``hx-target="this"``), which
 * forces it to be a single self-replacing element regardless of
 * how the inner state lays out.
 *
 * The wrapper's flex row is single-child today — no count or
 * badge — but the shape keeps the door open for a future
 * "subscribers count" annotation without restructuring the
 * markup. */
.affordance-subscribe {
    display: inline-flex;
    align-items: center;
    margin: 0;
}

.affordance-subscribe-button {
    /* Inherit ``.affordance`` (icon-button) styling. No
     * surface-state-specific color today: 🔔 and 🔕 carry the
     * subscribed-or-not signal via the glyph swap alone, mirroring
     * the existing pen / speech affordances that don't toggle
     * color either. The like affordance is the outlier; its
     * filled-heart variant earns the color because the static
     * (anonymous) state shares the outline glyph and color is
     * the only signal distinguishing "liked" from "not liked". */
    font-family: inherit;
}

/* ----- Comments meta panel (thread-view UI refresh,
 * _comments_panel.html).
 *
 * Comments live in a flat collapsible panel mounted next to each
 * post / riff card, not in the riff tree. The panel reads
 * "META AREA" with a collapse chevron, then a list of "@author:
 * excerpt" rows newest-first, then a "Add a comment" affordance.
 *
 * Layout: at wide viewports the panel sits to the right of the
 * body card; at narrow viewports it stacks below. We use
 * ``flex: 1 1`` on the body and a fixed-ish width on the panel
 * so the body absorbs viewport changes and the panel keeps a
 * predictable shape.
 *
 * The panel inherits visual chrome from the surrounding card
 * (soft background, small padding) so it reads as part of the
 * card rather than a free-floating sidebar. */
.meta-panel {
    /* A beige a touch darker than the page (--surface) so the comment box
     * reads as its own surface. (At v2 desktop the inner panel is transparent
     * and the fixed mount carries this same var — see the desktop block.) */
    background: var(--surface-comment-box);
    border-radius: 4px;
    padding: 0.5rem 0.75rem;
    margin: 0.5rem 0;
    font-size: 0.9em;
}

.meta-panel-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.5rem;
    margin: 0 0 0.4rem;
}

.meta-panel-title {
    margin: 0;
    font-size: 0.85em;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.meta-panel-toggle {
    background: transparent;
    border: 0;
    cursor: pointer;
    padding: 0.15rem 0.3rem;
    color: var(--text-muted);
    line-height: 1;
}

.meta-panel-comments {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.meta-panel-comment {
    padding: 0.25rem 0;
}

.meta-panel-comment-text {
    /* flow-root contains the floated author badge below, so the row's height
     * wraps the message and the meta row sits cleanly beneath it. */
    display: flow-root;
    margin: 0;
    line-height: 1.35;
    word-wrap: break-word;
    /* Chat messages read in the sans face, not the body serif (the floated
     * author handle re-asserts mono via .author-badge-handle). */
    font-family: var(--font-sans);
}

/* The rendered comment body. Chat-style: the full message shows (no
 * truncation), wrapping as needed. ``body_html`` wraps prose in <p>; the
 * paragraphs stay BLOCK so the writer's line breaks (nl2br → <br>) and blank-
 * line paragraph breaks are preserved. The author handle (floated, below)
 * lets the message's first line sit beside the username. ``overflow-wrap:
 * anywhere`` keeps an unbroken long token (e.g. a pasted URL) from forcing
 * the narrow panel wider. */
.meta-panel-comment-excerpt {
    margin: 0;
    overflow-wrap: anywhere;
}

.meta-panel-comment-excerpt p {
    margin: 0;
}

.meta-panel-comment-excerpt p + p {
    margin-top: 0.5rem;
}

/* @-mention links read as a subtle accent-tinted chip, distinct from ordinary
 * in-body links (which are --link coral). Scoped to the comment body excerpt so
 * it never touches the author badge — a sibling /@handle link outside this
 * element — and only comments linkify mentions in the first place. --accent /
 * --accent-tint are theme-aware, so the chip works in light and dark. The
 * renderer marks these links with a relative /@handle href (see
 * works.services.render), which the attribute selector keys off. */
.meta-panel-comment-excerpt a[href^="/@"] {
    color: var(--accent);
    font-weight: 600;
    text-decoration: none;
    background: var(--accent-tint);
    padding: 0 0.2em;
    border-radius: 3px;
}

.meta-panel-comment-excerpt a[href^="/@"]:hover {
    background: var(--accent-tint-hover);
    text-decoration: underline;
}

/* Float the author handle so the message's first line sits beside it and
 * longer / multi-paragraph messages wrap underneath — "username: message…" on
 * one line WITHOUT collapsing the message's own line / paragraph breaks.
 * gap:0 overrides .author-badge's default flex gap so the ":" (a generated
 * ::after flex item) hugs the handle as "username:" rather than "username :". */
.meta-panel-comment-text > .author-badge {
    float: left;
    margin-right: 0.35rem;
    gap: 0;
}

/* Chat-style "username:" separator. Lives on the badge wrapper (not the
 * handle link) so the colon stays out of the link's clickable / hover-
 * underline area and renders in the body text color, hugging the handle. */
.meta-panel-comment-text > .author-badge::after {
    content: ":";
    color: var(--text);
    font-weight: 600;
}

/* Comment author handles use the shared username component (.author-badge-handle:
 * author-coloured, bold, hover-flare) like every other byline — the
 * commenter's colour is set via --card-author-color on .meta-panel-comment-text.
 * meta-panel-comment-author is retained as a per-comment styling hook (and is
 * asserted by tests) but carries no styling of its own right now. */

.meta-panel-comment-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    /* Center, not baseline: the row mixes masked-SVG icon boxes (edit / delete /
     * heart) with the mono timestamp text, and icons have no text baseline to
     * share — centring keeps them optically level with the time. */
    align-items: center;
    margin: 0.15rem 0 0;
    font-size: 0.8rem;
    color: var(--text-subtle);
}

/* In the dense comments row the heart hugs its glyph — drop the
 * ``.affordance`` padding AND its 2rem reserved min-box (both read as space
 * around the heart) so it sits tight against the count and row. Scoped to the
 * comments panel; the metacol hearts keep their roomier tap target. */
.meta-panel-comment-meta .affordance-like-button {
    padding: 0;
    min-width: 0;
    min-height: 0;
}

/* Heart-as-SVG: in the comment row the like glyph uses the SAME masked icons
 * as the reading rail (heart.svg outline / hearted.svg filled) instead of the
 * ♥/♡ text character the post-footer surfaces use. The glyph <span> becomes a
 * currentColor box clipped to the SVG; ``font-size: 0`` hides the ♥/♡ character
 * it still carries (kept in the markup so _like_button.html stays one template)
 * without touching currentColor — the mask is the visible mark, and it inherits
 * the button's colour so the liked state still reads red via
 * ``.affordance-like-active``. Sized to match the edit/delete icons. */
.meta-panel-comment-meta .affordance-glyph {
    display: inline-block;
    width: 0.95rem;
    height: 0.95rem;
    font-size: 0;
    background-color: currentColor;
    -webkit-mask: var(--comment-heart-icon) center / contain no-repeat;
    mask: var(--comment-heart-icon) center / contain no-repeat;
}
/* Not-liked (authed) and the anonymous static render both carry .affordance-like
 * → outline heart; the liked form adds .affordance-like-active → filled hearted
 * (this rule is later, so it wins for a liked heart). */
.meta-panel-comment-meta .affordance-like .affordance-glyph {
    --comment-heart-icon: url("../icons/heart.e4fc1484c8e0.svg");
}
.meta-panel-comment-meta .affordance-like-active .affordance-glyph {
    --comment-heart-icon: url("../icons/hearted.b15d8e75bac1.svg");
}
/* ----- "Liked by N: …" count + popover (social/_likers_popover.html) -----
 *
 * Shared by both like-heart surfaces. The wrapper is the Alpine root
 * (likersReveal); it lays the heart + count on one line and is the positioning
 * context for nothing (the box is position: fixed — see below).
 *
 * Device split: on a pointer device the box reveals on hover/focus of the heart
 * and NO count shows; on touch there's no hover, so a tappable count appears and
 * toggles the box. The reveal is Alpine-driven (x-show + :style) for both, so
 * there's no CSS :hover rule here — CSS only styles the box and shows/hides the
 * count per media. */
.likers-wrapper {
    display: inline-flex;
    align-items: center;
    /* Breathing room between the heart and the touch count sitting beside it. */
    gap: 0.3rem;
}
/* The box is position: fixed (set inline by likers.js from the heart's rect) so
 * it escapes the scroll-overflow ancestors — the comment list's overflow-y and
 * the rail carousels' overflow-x — that would clip an absolutely-positioned
 * child. Visual treatment borrows the subscribe-picker menu's tokens for a
 * consistent floating box. */
.likers-popover {
    background: var(--surface);
    /* Border tracks the author-hued rail-icon colour so the box reads as part
     * of that icon set — same inline OKLCH formula as .post-metacol-icon (the
     * popover stays inside the card's Alpine $root, so --card-author-color /
     * --rail-icon-chroma are in scope here too). Neutral first as the
     * relative-colour fallback. */
    border: 1px solid var(--rail-icon-color);
    border-color: oklch(from var(--card-author-color, var(--rail-icon-color)) var(--rail-icon-tint-l) var(--rail-icon-chroma, 0.042) h);
    border-radius: 5px;
    box-shadow: 0 2px 8px var(--shadow-md);
    z-index: 50;
    /* width: max-content sizes the box to its content rather than the space
     * available at its fixed position — so a box opened next to a screen edge
     * (the comment hearts are right-aligned, so it always is) expands to its
     * text instead of shrink-to-fit squishing into a thin column. Capped at
     * 20rem, and at the viewport on very narrow phones. likers.js clamps the
     * horizontal position so the widened box stays on screen. */
    width: max-content;
    max-width: min(20rem, calc(100vw - 1.5rem));
    padding: 0.4rem 0.6rem 0.2rem;
    font-size: 0.9rem;
}
.likers-popover-lead {
    margin: 0;
}
.likers-count {
    /* The mono metadata voice the old inert like-count carried. A real button
     * (tap target) but chrome-free so it reads as a count, not a pill. 30%
     * larger than the surrounding metadata so the tappable number stands out. */
    color: var(--text-muted);
    font-size: 1.17em;
    font-family: var(--font-mono);
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
}
/* Pointer / desktop: no count — the box reveals on hover/focus of the heart. */
@media (hover: hover) {
    .likers-count {
        display: none;
    }
}
/* Touch / mobile: show the count; tapping it toggles the box. */
@media (hover: none) {
    .likers-count {
        display: inline;
    }
}

/* "(edited)" marker — a subtle, non-interactive label in the meta row,
 * sitting just before the right-aligned timestamp (it has no margin-left
 * auto, so it stays in the left cluster with the heart/state). Shares the
 * timestamp's mono voice + size so the two read as one metadata run; the muted
 * colour keeps it reading as an annotation, not an action. */
.meta-panel-comment-edited {
    font-family: var(--font-mono);
    /* No font-size: inherit the 0.8rem from .meta-panel-comment-meta so the
     * whole row is one size (an explicit value here would break that). */
    color: var(--text-subtle);
}

.meta-panel-comment-time {
    /* Mono, matching the site-nav / metadata voice rather than body text. */
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    /* No font-size: inherit the 0.8rem from .meta-panel-comment-meta so the
     * whole row is one size (an explicit value here would break that).
     * Date-first: the timestamp LEADS the meta row (the edit / delete / heart
     * cluster is pushed to the right by .meta-panel-comment-actions), so it sits
     * at the natural start with no auto margin. */
}

/* The edit / delete / heart affordances cluster at the row's right edge:
 * margin-left:auto eats the free space between the left-aligned timestamp (+
 * state / edited annotations) and this group. A flex row so the icons share one
 * gap and one baseline. */
.meta-panel-comment-actions {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
/* Match the like heart to the edit / delete masked icons in this row: strip
 * the roomy ``.affordance`` min-width + padding so the heart's glyph sits tight
 * against the shared 0.5rem flex gap. Without this the padded ~2rem button
 * would open a visibly wider gap on the heart's side than between edit and
 * delete. Scoped to the comment meta row — the post / riff action rows keep the
 * larger like button. */
.meta-panel-comment-actions .affordance-like-button {
    min-width: 0;
    min-height: 0;
    padding: 0;
}

/* Author edit/delete affordances, inline on the meta row after the timestamp
 * (time · edit · delete · heart). They're masked-SVG icon controls — the same
 * edit.svg / delete.svg the reading rail uses — rendered in currentColor, so
 * the dense row reads as icons rather than sprouting pill buttons or text
 * links. They sit as direct flex items of ``.meta-panel-comment-meta``; the
 * delete <form> is ``display: contents`` so its <button> is the flex item (the
 * form box itself adds no layout). The base rule strips button chrome; the
 * ``--edit`` / ``--delete`` modifiers layer the mask. Accessible name rides
 * aria-label/title (the controls carry no visible text). */
.meta-panel-comment-delete-form {
    display: contents;
}
.meta-panel-comment-action {
    appearance: none;
    -webkit-appearance: none;
    margin: 0;
    padding: 0;
    border: 0;
    /* Reset the global ``button { min-height: 2.5rem }`` floor so the
     * <button> delete sits inline like its <a> sibling. */
    min-height: 0;
    background: none;
    font: inherit;
    color: var(--text-subtle);
    cursor: pointer;
}
/* Icon rendering: a currentColor box clipped to the SVG via mask. Sized to sit
 * level with the row's mono time text; hover borrows the link colour so it
 * reads as interactive, the same cue the old underlined text links gave. */
.meta-panel-comment-action--edit,
.meta-panel-comment-action--delete {
    display: inline-block;
    width: 0.95rem;
    height: 0.95rem;
    background-color: currentColor;
    -webkit-mask: var(--comment-action-icon) center / contain no-repeat;
    mask: var(--comment-action-icon) center / contain no-repeat;
}
.meta-panel-comment-action--edit {
    --comment-action-icon: url("../icons/edit.5c619f0087e2.svg");
}
.meta-panel-comment-action--delete {
    --comment-action-icon: url("../icons/delete.95e7f1d4da44.svg");
}
.meta-panel-comment-action:hover,
.meta-panel-comment-action:focus-visible {
    color: var(--link);
}
/* The delete affordance is a <button>, so the global ``button:hover`` /
 * focus background shorthand (pages.css) would reset its background-color —
 * but that IS the icon paint (currentColor under an SVG mask), so the glyph
 * would repaint in the surface tint (reading as near-black) instead of the
 * --link hover colour the edit <a> beside it gets. Two classes + the
 * pseudo-class out-specify that global rule so the mask keeps its currentColor
 * fill. */
.meta-panel-comment-action.meta-panel-comment-action--delete:hover,
.meta-panel-comment-action.meta-panel-comment-action--delete:focus-visible {
    background-color: currentColor;
}

/* Inline comment editor (the Alpine ``editing`` textarea). Reuses the
 * quick-comment input's compact writing-surface treatment; the button-row
 * below it holds Save / Cancel. */
.meta-panel-comment-edit {
    margin: 0.25rem 0 0;
}
.meta-panel-comment-edit-input {
    /* Match the quick-comment input's writing-surface tint + compact rhythm,
     * overriding the global heavy-textarea ``min-height: 6rem`` / 0.5rem
     * padding so the inline editor fits the panel. Stays resizable (unlike
     * the auto-grow quick-comment field) since an edit may add lines. */
    background: var(--surface);
    width: 100%;
    min-height: 3rem;
    padding: 0.3rem 0.5rem;
    font-size: 0.9rem;
    line-height: 1.35;
    resize: vertical;
}

.meta-panel-empty {
    color: var(--text-subtle);
    /* Mono, matching the site-nav / metadata voice rather than body text. */
    font-family: var(--font-mono);
    font-size: 0.85em;
    margin: 0 0 0.4rem;
    font-style: italic;
}

/* Inline quick-comment form. Sits below the comments list and is
 * the only compose surface inside the panel — the redundant
 * in-panel 💬 affordance was removed because the post / riff card
 * already exposes its own 💬 icon for the heavy compose form.
 *
 * The form is a single-row container with the input filling the
 * available width. We override the global ``input[type="text"]
 * { padding: 0.5rem }`` rule with a tighter padding so the input
 * fits the panel's compact rhythm without feeling like the heavy
 * compose textarea snuck in. Font-size stays at 1rem so iOS
 * Safari doesn't auto-zoom on focus (the same constraint that
 * shapes the global input rule).
 */
.meta-panel-quick-comment {
    /* Top margin only — it separates the input from the last comment; the
     * bottom margin was dead space against the panel's lower edge. */
    margin: 0.4rem 0 0;
    /* The input + send button share one row. ``stretch`` makes the send button
     * exactly as tall as the textarea (the height-driver), so the two read as a
     * matched pair; the button grows with the field if it wraps to more lines. */
    display: flex;
    align-items: stretch;
    gap: 0.4rem;
}

.meta-panel-quick-comment-send {
    /* An icon button sized to the textarea, not the chunky 2.5rem .btn pill:
     * drop the min-height floor and the text padding so the glyph centres in a
     * compact square whose height comes from the stretched row. ``flex:none``
     * keeps the textarea the one that takes the row's slack width. */
    flex: none;
    min-height: 0;
    padding: 0 0.7rem;
}
.meta-panel-quick-comment-send::before {
    content: "";
    display: block;
    width: 1.15rem;
    height: 1.15rem;
    background-color: currentColor; /* inherits .btn-primary's --accent */
    -webkit-mask: url("../icons/send.05b32402bc17.svg") center / contain no-repeat;
    mask: url("../icons/send.05b32402bc17.svg") center / contain no-repeat;
}

/* The chat-subscribe bell lives in the comments-panel header now (not the
 * body), so it needs no spacing of its own — the header's flex layout places it
 * at the right. The button reuses the rail's .post-metacol-icon bell glyph. */
.meta-panel-subscribe {
    display: flex;
    align-items: center;
}
.comment-subscribe {
    margin: 0;
    display: flex;
}
/* Chat-header bell: match the edit / delete icons beside it. Resting colour is
 * the neutral --text-subtle (not the rail's author-tinted oklch), and the glyph
 * is 25% smaller than the reading-rail bell's 1.35rem. Hover still lifts to
 * --link via the shared .post-metacol-icon:hover rule — the same cue edit /
 * delete get. State (subscribed / super) stays carried by the glyph shape, so
 * the flat resting colour doesn't lose any information. */
.comment-subscribe-btn.post-metacol-icon {
    color: var(--text-subtle);
}
.comment-subscribe-btn.post-metacol-icon::before {
    width: calc(1.35rem * 0.75);
    height: calc(1.35rem * 0.75);
}

.meta-panel-quick-comment-input {
    /* The page beige (--surface) — a touch lighter than the box's --bg — so
     * the add-comment field reads as the writing surface inside the box. */
    background: var(--surface);
    /* Override the global form-control ``padding: 0.5rem`` so the
     * field visually matches the panel's tight per-row vertical
     * rhythm. Width inherits the 100% from the global rule. */
    padding: 0.3rem 0.5rem;
    /* Sans face for both the "Add comment…" placeholder and what's typed in,
     * matching the sans chat-message body it sits among. */
    font-family: var(--font-sans);
    font-size: 0.9rem;
    /* Now a <textarea> (chat-style multi-line). The global
     * ``textarea`` rule sets ``min-height: 6rem`` and
     * ``resize: vertical`` for the heavy compose box; neither fits a
     * one-line quick-comment field that grows to fit via the
     * ``@input`` auto-grow handler. Reset to a single-row baseline,
     * disable the manual resize grabber (it would fight the
     * auto-grow), and hide the scrollbar that would otherwise flash
     * mid-grow. */
    min-height: 0;
    line-height: 1.35;
    resize: none;
    overflow-y: hidden;
    /* Take the row's leftover width beside the send button; min-width:0 lets it
     * shrink below its intrinsic width instead of overflowing the flex row. */
    flex: 1;
    min-width: 0;
}

/* Two-column layout for post / riff card + meta panel.
 *
 * The mount wrapper is a ``.contribution-meta-mount`` div that
 * sits as a sibling of ``<article class="contribution-body">``
 * inside the treeitem. On narrow viewports the mount stacks
 * below the body card (single column). On wide viewports the
 * containing card transitions to a 2-column grid: card body
 * on the left, panel on the right.
 *
 * Implementation: the per-card grid lives on the ``<li
 * role="treeitem">`` element when the viewport is wide enough,
 * so the article + meta-mount + children-ul split into named
 * grid rows. The two-column treatment runs at >=720px to match
 * the existing breakpoint pattern in this file.
 *
 * Children ``<ul role="group">`` always spans the full grid
 * row so nested riffs read as a continuation of the thread
 * rather than as a column under the parent. */
.contribution-meta-mount {
    margin: 0.25rem 0;
}

.post-meta-mount {
    margin: 0.75rem 0;
}

/* Task U.5 — Subgrid riff tree.
 *
 * The outer ``.post-detail-layout`` (Task U.4) lays the page-level
 * column tracks at ≥880 px: ``minmax(0, 1fr) 22rem``. This block
 * lifts those tracks into every level of the riff tree via CSS
 * subgrid so the meta column lands in the SAME page-level
 * right-rail position regardless of nesting depth — no matter how
 * deep a riff is in the tree, its meta panel is column-2 at the
 * page level.
 *
 * ``.contribution-tree`` (the top-level ``<ul>``) and every nested
 * ``.contribution-children`` ``<ul>`` are grid containers whose
 * column tracks are inherited from the parent (``subgrid``). Each
 * ``.contribution <li>`` spans both columns and is itself a subgrid
 * so its inner ``.contribution-body`` (col 1), placeholder card
 * (col 1), meta panel (col 2), and children ``<ul>`` (full-width
 * spanning) line up against the page-level tracks.
 *
 * Below 880 px the block doesn't apply; the tree falls back to
 * default block flow with the body-indent rule above doing the
 * visual stair-step. The 880 px breakpoint matches U.4's outer
 * grid breakpoint exactly — pulling them apart would create a
 * window where the outer grid has two columns but the inner tree
 * doesn't, breaking the column alignment.
 *
 * Browser support: subgrid is in Chromium 117+, Firefox 71+,
 * Safari 16+. Older browsers fall back to the legacy per-row grid
 * via the ``@supports not`` block below. */
@media (min-width: 880px) {
    @supports (grid-template-columns: subgrid) {
        .contribution-tree,
        .contribution-children {
            display: grid;
            grid-template-columns: subgrid;
            grid-column: 1 / -1;
            row-gap: 0.5rem;
        }
        .contribution {
            display: grid;
            grid-template-columns: subgrid;
            grid-column: 1 / -1;
        }
        .contribution > .contribution-body,
        .contribution > .contribution-placeholder {
            grid-column: 1;
        }
        .contribution > .contribution-meta-mount {
            grid-column: 2;
            margin: 0;
        }
        .contribution > .contribution-children {
            grid-column: 1 / -1;
        }
    }

    /* Fallback for browsers without subgrid: the legacy per-row
     * grid (1fr + 18rem). Same shape as the pre-U.5 layout, so
     * non-modern browsers get the pre-existing
     * shrinking-meta-panel behavior rather than no layout at all.
     * The page-level ``.post-detail-layout > .contribution-tree {
     * grid-column: 1 / -1 }`` rule from U.6 keeps the tree's outer
     * wrapper spanning both page columns here too. */
    @supports not (grid-template-columns: subgrid) {
        .contribution {
            display: grid;
            grid-template-columns: minmax(0, 1fr) minmax(0, 18rem);
            grid-template-areas:
                "body meta"
                "children children";
            gap: 0.5rem 1rem;
            align-items: start;
        }
        .contribution > .contribution-body,
        .contribution > .contribution-placeholder {
            grid-area: body;
        }
        .contribution > .contribution-meta-mount {
            grid-area: meta;
            margin: 0;
        }
        .contribution > .contribution-children {
            grid-area: children;
        }
    }

}

/* ----- Contribution placeholder (Task H.12, _contribution.html).
 * Renders in place of the contribution body when the viewer has
 * muted or blocked the author. Sits inside the same treeitem ``<li>``
 * as the (Alpine-hidden) body and children container, so the tree
 * layout / indent rules above still apply — we only style the inner
 * card.
 *
 * Visual goals: read as "neutral hidden content" rather than as
 * either a chip / badge or a regular contribution. A muted
 * neutral background, a softer foreground color, and a small
 * inline button. We deliberately reuse a muted blue-grey chip
 * palette (``#eef3f9`` background, ``#133a64`` text) so the page
 * doesn't grow a new design token for one surface. */
.contribution-placeholder {
    background: var(--accent-tint);
    color: var(--accent-strong);
    border: 1px solid var(--accent-tint-hover);
    border-radius: 4px;
    padding: 0.5rem 0.75rem;
    font-size: 0.9em;
}

.contribution-placeholder-text {
    margin: 0;
    /* Wrap the inline button onto a new line at very narrow widths
     * rather than forcing horizontal overflow. The default inline-
     * flow does this anyway; the rule is here as an anchor for any
     * future flex-row reshuffle that would otherwise hide it. */
}

.contribution-placeholder-reveal {
    /* A small inline button rather than a full-width control: the
     * placeholder is a quick read with a single action, and a
     * full-width button would suggest this is a primary affordance
     * for the page. It's not — most readers will skip past muted /
     * blocked content without revealing. */
    background: transparent;
    border: 1px solid var(--accent-strong);
    color: var(--accent-strong);
    border-radius: 0.6rem;
    padding: 0.05rem 0.5rem;
    font-size: 0.95em;
    cursor: pointer;
    margin-left: 0.25rem;
}

.contribution-placeholder-reveal:hover,
.contribution-placeholder-reveal:focus {
    background: var(--accent-tint-hover);
}
