/*
 * The Conversatron — structural stylesheet.
 *
 * Layout, sizing and spacing, plus the *role bindings* at the foot of this file
 * (REWRITE_PLAN §6.2: "themes are data, not code"). The division of labour is:
 *
 *   base.css   says which element plays which role — the entry panel takes the mood
 *              colour, the inbox header takes the title-strip colour, and so on.
 *   theme-*.css says what colour each role is, and nothing else.
 *
 * So adding a theme is writing a ~30-line block of custom properties. It is not touching
 * this file, and it is never touching C#. A theme that defines the documented properties
 * gets every surface — thread view, front page, forms, admin tables — for free.
 *
 * There are deliberately **no colour literals** here, and a *role* — something every theme
 * owes — is read as a bare `var(--x)` with no fallback. If a theme forgets one the page
 * should look obviously broken rather than quietly wrong, which is the failure mode §7's
 * notes on the original warn about, and ThemeCatalogTests enforces it.
 *
 * The one exception is *decoration*: the framed-entry chrome at the foot of this file, which
 * three themes have and fifteen do not. Those are read as `var(--x, none)`, because absent
 * is the correct answer for most themes rather than a mistake. The fallback is what makes
 * the property optional, and the test derives its required set from the bare reads only —
 * so writing a fallback is how you say "this one is a decoration".
 *
 * Standards mode, responsive, no layout tables except the thread list — which is
 * genuinely tabular data (§6.1).
 */

/* ------------------------------------------------------------------ *
 * Structural constants
 * ------------------------------------------------------------------ */

:root {
    /*
     * Portraits are native-size raster GIFs at 80x100 and stay that size at every
     * viewport (§6.3). Scaling them down only loses detail, and 80 CSS pixels is
     * already physically small on a retina screen.
     */
    --portrait-width: 80px;
    --portrait-height: 100px;

    /*
     * The original forced a minimum body-column width with a 1x1 spacer GIF at
     * width=100 (`/img/_.gif`). CSS does it directly.
     */
    --entry-body-min-width: 100px;

    /* The original's inner `<table cellpadding=4>` around entry text. */
    --entry-body-padding: 0.25rem;

    /* `<br><br>` between entries. */
    --entry-gap: 1.5rem;

    /*
     * `<font size="-1">` and `<font size="-2">` against a default size of 3.
     * The original's small type is a real part of the look, so it is named here
     * rather than sprinkled around.
     */
    --font-size-small: 0.8125rem;
    --font-size-tiny: 0.6875rem;
    --font-size-large: 1.5rem;

    /*
     * The classic layout is a centred column. The thread table was `width="66%"`;
     * a max-width in ch/rem behaves better than a percentage on a wide monitor.
     */
    --page-max-width: 60rem;
}

/* ------------------------------------------------------------------ *
 * Page chrome
 * ------------------------------------------------------------------ */

html {
    /* Never let a stray wide element in an entry body scroll the whole document. */
    overflow-x: hidden;
}

body {
    margin: 0;
    padding: 1rem 0.75rem;

    /*
     * Helvetica-first, matching the archived pages' `body, td, p { font-family:
     * helvetica }`. The live classic theme defined no stylesheet at all and so
     * rendered in the browser's default serif; every archived page disagrees with it,
     * and §6.1 specifies a sans stack. We follow the archive.
     */
    font-family: Helvetica, Arial, "Helvetica Neue", sans-serif;
    line-height: 1.35;
}

.page {
    max-width: var(--page-max-width);
    margin: 0 auto;
}

.masthead {
    text-align: center;
}

/* Logos are fixed-size GIFs — blahlogo.gif is 470x86, wider than a 375px viewport. */
.masthead img,
.site-logo {
    max-width: 100%;
    height: auto;
}

.slogan {
    margin: 0.5rem 0 1.25rem;
}

/*
 * The link bar. The original separated items with a literal " . . . ", which is part
 * of the site's voice, so it stays in the markup rather than becoming a CSS border.
 */
.linkbar {
    margin: 0 0 1.5rem;
    line-height: 1.8;
}

.copyright {
    margin: 2rem 0 0;
    text-align: right;
    font-size: var(--font-size-tiny);
    font-family: Arial, Helvetica, sans-serif;
}

/* ------------------------------------------------------------------ *
 * The entry component
 *
 * The original was a two-row, two-cell table: a title strip spanning both columns,
 * then the portrait beside the body panel. Grid rather than flex, because the strip
 * spans the portrait column too — with flex that needs wrapping plus flex-basis:100%,
 * where grid places it directly.
 * ------------------------------------------------------------------ */

.entry {
    display: grid;
    grid-template-columns: var(--portrait-width) 1fr;
    margin: 0 0 var(--entry-gap);

    /*
     * The single most easily lost part of the look. The original's entry `<table>`
     * carried no width attribute, so it shrank to fit its content: a one-word reply
     * rendered as a small box, not a full-bleed panel. That ragged rhythm of varying
     * box widths *is* the classic thread view. A plain block <article> would silently
     * destroy it.
     */
    width: fit-content;
    max-width: 100%;
}

/*
 * The title strip runs the full width of the box — in the original, the cell above the
 * portrait was a `&nbsp;` painted the same colour. The name text sits above the *body*,
 * so on left-side entries it is indented past the portrait.
 */
.entry-name {
    grid-row: 1;
    grid-column: 1 / -1;
    padding: 0.15rem 0.35rem 0.15rem calc(var(--portrait-width) + 0.35rem);
    font-weight: bold;
}

.entry-portrait {
    grid-row: 2;
    grid-column: 1;
    display: block;
    width: var(--portrait-width);
    height: var(--portrait-height);
    border: 0;
}

.entry-body {
    grid-row: 2;
    grid-column: 2;
    min-width: var(--entry-body-min-width);
    padding: var(--entry-body-padding);
}

/*
 * Side alternation. Writers set this deliberately to stage back-and-forth exchanges,
 * so it carries authorial intent and survives unchanged at every viewport (§6.3) —
 * there is no mobile variant of the entry.
 */
.entry--right {
    grid-template-columns: 1fr var(--portrait-width);
}

.entry--right .entry-name {
    /* Body column is now on the left, so the name needs no indent. */
    padding-left: 0.35rem;
    padding-right: calc(var(--portrait-width) + 0.35rem);
}

.entry--right .entry-portrait {
    grid-column: 2;
}

.entry--right .entry-body {
    grid-column: 1;
}

/*
 * Writer diagnostics line printed under an entry — "Posted by X at [IP] on macOS".
 * Writer-only, so it is markup that simply is not emitted for the public (Phase 3).
 */
.entry-meta {
    margin: -1rem 0 var(--entry-gap);
    font-size: var(--font-size-small);
}

/*
 * The platform stands in for the whole User-Agent header, which is in the title attribute.
 * The dotted underline is what says so — without it nobody discovers the hover, and a
 * writer chasing a spammer has no way to know the full string is still there.
 */
.entry-platform {
    text-decoration: underline dotted;
    text-underline-offset: 0.2em;
    cursor: help;
}

/*
 * "[not a browser]" — a form post that claimed to be Chrome and wasn't. Meant to catch the
 * eye while triaging an inbox, which is the only moment it is useful.
 *
 * The angry panel, for the same reason .thread-state-note and .picture-flag--bad take it:
 * it is the site's existing "watch out", every theme already defines it, and there is
 * rarely a spare colour in a theme to spend on a new one.
 */
.entry-suspect {
    padding: 0 0.3rem;
    background: var(--entry-angry);
    cursor: help;
    white-space: nowrap;
}

/* ------------------------------------------------------------------ *
 * The entry component, flow variant
 *
 * The other half of the 2005 collection (`default.xml`, `2000.xml`, `orange.xml`):
 *
 *     <img src="..." width="80" height="100" align="left|right" hspace="5">
 *     <b>{name}</b><br>
 *     {text}<br clear="all">
 *
 * A floated portrait with the text running around it. No title strip, no panel, and —
 * because that template never referenced `{color}` — no mood colours whatsoever. That
 * last part is the layout's defining feature, not an omission to be helpfully corrected.
 *
 * Selected by `data-entry-layout` on <html>, set from Theme.EntryLayout.
 * ------------------------------------------------------------------ */

[data-entry-layout="flow"] .entry {
    display: block;

    /*
     * The opposite of the panel variant's `fit-content`. Text that wraps around a float
     * needs the full measure to wrap *into*; shrinking the box to its content would leave
     * the portrait with nothing beside it.
     */
    width: auto;
}

/*
 * `align="left"` + `hspace="5"`. hspace is horizontal space on *both* sides, so the
 * portrait sits 5px in from the edge as well as 5px from the text.
 */
[data-entry-layout="flow"] .entry-portrait {
    float: left;
    margin: 0 5px;
}

[data-entry-layout="flow"] .entry--right .entry-portrait {
    float: right;
}

/* `<b>{name}</b>` inline on the first line beside the portrait — not a strip. */
[data-entry-layout="flow"] .entry-name {
    display: block;
    padding: 0;
}

[data-entry-layout="flow"] .entry-body {
    /*
     * No `_.gif` spacer in this template, so no minimum column width — the body is the
     * whole measure and the float is carved out of it.
     */
    min-width: 0;
    padding: 0;
}

/* `<br clear="all">`, which is what stopped the next entry riding up beside the portrait. */
[data-entry-layout="flow"] .entry::after {
    content: "";
    display: block;
    clear: both;
}

/*
 * Suppressing the panel colours by *redefining the properties* rather than by out-
 * specifying the role bindings below. Custom properties inherit, so one declaration here
 * reaches every mood variant at once — and it says what is true (this layout has no
 * painted surfaces) instead of starting a specificity argument that each new mood would
 * have to be added to.
 */
[data-entry-layout="flow"] .entry {
    --entry-title-bg: transparent;
    --entry-neutral: transparent;
    --entry-happy: transparent;
    --entry-angry: transparent;
    --entry-sad: transparent;
    --entry-name-askee: inherit;
    --entry-name-asker: inherit;
}

/*
 * The rule or ornament between flowed entries. `default.xml` used a bare <hr>; the 2000
 * family centred a row of dots.
 */
.entry-separator {
    margin: 0 0 var(--entry-gap);
    text-align: center;
}

.entry-separator hr {
    margin: 0;
}

/* ------------------------------------------------------------------ *
 * Legacy HTML containment
 *
 * Writer posts are raw HTML by design (a comedic device) and contain <br>, <i>, <font>,
 * inline <img>, and occasionally worse. Everything defensive is scoped here so it
 * cannot leak into the rest of the page, and it backs up — rather than replaces — the
 * parse-and-reserialize repair pass in §7.8.
 * ------------------------------------------------------------------ */

.entry-body {
    overflow-wrap: break-word;
}

.entry-body img {
    max-width: 100%;
    height: auto;
}

.entry-body table {
    max-width: 100%;
}

.entry-body pre {
    max-width: 100%;
    overflow-x: auto;
}

/*
 * Askee names and slogans are HTML fragments too, not plain text — two askees are named
 * literally `&nbsp;`, and others carry <i>, <blink>, <font> (§3 Encoding). They render
 * raw, so they need the same containment.
 */
.entry-name,
.slogan {
    overflow-wrap: break-word;
}

/* <blink> was removed from every engine years ago. The joke is load-bearing. */
blink {
    animation: conversatron-blink 1.4s steps(1) infinite;
}

@keyframes conversatron-blink {
    50% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
    blink {
        animation: none;
    }
}

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

/*
 * `<font size="+2">{topic_leadin}<b>{subject}</b></font>` — larger than a default h1 is
 * not. The weight lives on the inner <b>, because the leadin some themes put ahead of the
 * subject sat *outside* it and was not bold.
 */
.thread-subject {
    margin: 0 0 1rem;
    font-size: var(--font-size-large);
    font-weight: normal;
}

/* `<font size="+2"><hr>Responses:</font>` between the prompt and the replies. */
.entry-responses {
    margin: 0 0 1rem;
    font-size: var(--font-size-large);
}

.entry-responses hr {
    margin-bottom: 0.5rem;
}

/*
 * Image nav buttons: 196 + 76 + 160 = 432px of GIFs, which will not fit a 375px
 * viewport in a row (§6.4). They wrap, and shrink only if a single button is itself
 * wider than the screen.
 */
.thread-nav {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 0.5rem 1rem;
    margin: 1.5rem 0;
}

.thread-nav img {
    max-width: 100%;
    height: auto;
    border: 0;
}

/*
 * A navigation button that is words rather than a GIF. Six themes set these two
 * properties to put the label in Trebuchet MS at `<font size=4>`; the rest inherit the
 * body font, which is what their `&lt;&lt; Prev` did.
 */
.theme-label {
    font-family: var(--nav-label-font, inherit);
    font-size: var(--nav-label-size, inherit);
}

/*
 * A visible separator between the buttons — only `expert` and `elvis` have one. The flex
 * gap already spaces them, so this element carries the mark and no margin of its own.
 */
.thread-nav-sep img {
    vertical-align: middle;
}

/* ------------------------------------------------------------------ *
 * Front page thread list
 *
 * A real <table>: four columns of genuinely tabular data (§6.1). The responsive work
 * concentrates here, because four columns is too many at 375px (§6.3).
 * ------------------------------------------------------------------ */

/*
 * The original was `<table width="66%" align="center">` nested inside another table
 * whose 1px cellspacing showed through as a frame. A max-width in rem behaves better
 * than a percentage on a wide monitor; the 1px frame is padding on the wrapper.
 */
.thread-list-frame {
    max-width: 42rem;
    margin: 0 auto;
    padding: 1px;
}

.thread-list {
    width: 100%;
    margin: 0 auto;
    border-collapse: collapse;
    border-spacing: 0;
}

.thread-list caption {
    padding: 0.25rem;
    font-size: var(--font-size-small);
    font-family: Verdana, Helvetica, Arial, sans-serif;
    font-weight: bold;
}

.thread-list td {
    padding: 0.25rem;
    vertical-align: top;
}

/* Weekday separator rows. */
.thread-list .thread-day th {
    padding: 0.25rem;
    text-align: center;
    font-size: var(--font-size-small);
}

.thread-list .col-star,
.thread-list .col-count {
    width: 1px;
    white-space: nowrap;
    text-align: center;
}

.thread-list .col-time {
    width: 1px;
    white-space: nowrap;
    font-size: var(--font-size-small);
}

.thread-list .col-star img,
.thread-list .col-time img {
    vertical-align: middle;
}

@media (max-width: 34rem) {
    /*
     * Two lines per thread: subject on top, then length / time / star as a muted meta
     * row. Grid on <tr> rather than a card rewrite, so the markup stays a real table.
     */
    /*
     * `caption` must be in this list. A table-caption left at its default display
     * inside a block-display table gets wrapped in an anonymous table box and shrinks
     * to its content instead of filling the frame.
     */
    .thread-list,
    .thread-list caption,
    .thread-list tbody,
    .thread-list tr,
    .thread-list td {
        display: block;
    }

    /*
     * Must follow the rule above and be at least as specific. Weekday separators are
     * emitted for every thread and hidden unless they open a new day — and `display:
     * block` on a class selector outranks the user agent's `[hidden] { display: none }`,
     * which would reveal every one of them.
     */
    .thread-list tr[hidden] {
        display: none;
    }

    .thread-list tr.thread-row {
        display: grid;
        grid-template-columns: 1fr auto auto;
        align-items: baseline;
        column-gap: 0.5rem;
        padding: 0.35rem 0.25rem;
    }

    .thread-list .col-subject {
        grid-row: 1;
        grid-column: 1 / -1;
        padding: 0;
    }

    /*
     * The row is explicit, not auto-placed. Source order is subject, star, count, time
     * — the original's column order, which the desktop table keeps — so sparse
     * auto-placement would take the star first, refuse to move the cursor backwards for
     * the count, and spill the meta line across two rows.
     */
    .thread-list .col-star,
    .thread-list .col-count,
    .thread-list .col-time {
        grid-row: 2;
        width: auto;
        padding: 0;
        font-size: var(--font-size-small);
    }

    .thread-list .col-count {
        grid-column: 1;
        text-align: left;
    }

    .thread-list .col-time {
        grid-column: 2;
    }

    .thread-list .col-star {
        grid-column: 3;
    }

    .thread-list .thread-day th {
        display: block;
    }
}

/* ------------------------------------------------------------------ *
 * Static documents — about, faq, follow-ups, links
 * ------------------------------------------------------------------ */

/*
 * The banner every htmldocs page carried: logo left, title in a black strip filling the
 * rest. The original used `<table width="100%">`, which on a phone squeezed the title
 * into a column two words wide. Flex-wrap drops it below the logo instead.
 */
.doc-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
    margin: 0 0 1.5rem;
}

.doc-header img {
    max-width: 100%;
    height: auto;
    border: 0;
}

.doc-title {
    flex: 1 1 12rem;
    margin: 0;
    padding: 0.35rem 0.75rem;
    text-align: center;
    font-size: var(--font-size-large);
    font-weight: normal;
}

/*
 * The `<table cellpadding=8>` panels on about.html: a titled dark strip over a body.
 * Same shape as an entry, without the portrait.
 */
.doc-panel {
    margin: 0 0 1.5rem;
}

.doc-panel h2 {
    margin: 0;
    padding: 0.4rem 0.6rem;
    font-size: var(--font-size-large);
    font-weight: normal;
}

.doc-panel p,
.doc-panel ul {
    margin: 0;
    padding: 0.6rem;
}

.doc-panel p + p,
.doc-panel p + ul {
    padding-top: 0;
}

.doc-panel ul {
    padding-left: 1.75rem;
    list-style: square;
}

/* ------------------------------------------------------------------ *
 * Forms — the new-topic box, and the writer composer later
 * ------------------------------------------------------------------ */

/*
 * The original's new-topic form was a centred block of content-sized controls —
 * `size="32"` and `cols="60"` inside a `<div align=center>` — not a full-bleed form.
 * The subject box now sizes itself from `MaxSubjectLength` instead, and `max-width: 100%`
 * below keeps that from outgrowing the column on a narrow screen.
 * Matching the thread list's width keeps the front page reading as one column.
 */
.form-block {
    max-width: 42rem;
    margin: 1.5rem auto;
}

.form-block textarea,
.form-block input[type="text"] {
    max-width: 100%;
    box-sizing: border-box;
    font-family: inherit;
    font-size: 1rem;
}

.form-block textarea {
    width: 100%;
    min-height: 10rem;
}

/* ------------------------------------------------------------------ *
 * Panels — the account pages
 *
 * `StartBox` in the original: a table with a centred `<font size="4">` title over a body.
 * Same two-part shape as `.doc-panel`, with the title centred rather than ranged left,
 * which is the only structural difference between the two in the 2005 code.
 *
 * Note that in `classic` these boxes were *unpainted*: StartBox defaulted its colour to
 * `settings.get('ucolor','')` and classic.ini never defines `ucolor`, so the login form
 * rendered as bare text on the tiled grey background. We paint them like every other
 * titled panel on the site instead — an unstyled form in the middle of a themed page reads
 * as a page that failed to load, and §6.4 is explicit about not inventing values for keys
 * the theme never defined. So this is the theme's existing panel colours, reused.
 * ------------------------------------------------------------------ */

.panel {
    max-width: 42rem;
    margin: 0 auto 1.5rem;
}

.panel h2 {
    margin: 0;
    padding: 0.4rem 0.6rem;
    text-align: center;
    font-size: var(--font-size-large);
    font-weight: normal;
}

.panel-body {
    padding: 0.75rem;
}

.panel-body > :first-child {
    margin-top: 0;
}

.panel-body > :last-child {
    margin-bottom: 0;
}

.panel-body hr {
    border: 0;
    border-top: 1px solid currentcolor;
    opacity: 0.35;
    margin: 1rem 0;
}

/* The WOPR acknowledgement on a posted topic: portrait left, machine output beside it. */
.processing {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
}

/* A one-shot banner after a settings change. */
.notice {
    max-width: 42rem;
    margin: 0 auto 1rem;
    padding: 0.5rem 0.75rem;
}

/*
 * The unconfirmed-address nag, which is on every page rather than one-shot. Narrower type
 * than a success notice, because it is standing furniture and should not shout.
 */
.notice--nag {
    font-size: var(--font-size-small);
}

/* ------------------------------------------------------------------ *
 * Form fields
 * ------------------------------------------------------------------ */

.field {
    margin: 0 0 1rem;
}

.field label {
    display: block;
    margin-bottom: 0.2rem;
    font-weight: bold;
}

.field input,
.field textarea {
    width: 100%;
    max-width: 24rem;
    box-sizing: border-box;
    font-family: inherit;
    font-size: 1rem;
    padding: 0.25rem;
}

.field textarea {
    max-width: 100%;
    min-height: 10rem;
}

/*
 * A checkbox and its label read as one line, not as a field with a heading above it —
 * which is what the generic rule above would produce.
 */
.field--check {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

.field--check input {
    width: auto;
    margin: 0;
}

.field--check label {
    margin: 0;
    font-weight: normal;
}

.field-hint {
    margin: 0.25rem 0 0;
    font-size: var(--font-size-small);
    opacity: 0.85;
}

/*
 * Validation messages.
 *
 * The summary is always in the markup, even with nothing to say — asp-validation-summary
 * emits <div class="… validation-summary-valid"><ul><li style="display:none"></li></ul>,
 * which `:empty` does not match because that placeholder <li> is a child. So the hook is
 * the class the tag helper swaps: validation-summary-valid when the form is clean,
 * validation-summary-errors when it is not. Without this an untouched form opens with a
 * blank red bar across the top of it.
 */
.form-errors.validation-summary-valid {
    display: none;
}

.form-errors ul {
    margin: 0 0 1rem;
    padding: 0.5rem 0.75rem 0.5rem 1.75rem;
}

.form-errors-large {
    font-size: 1.15rem;
}

.form-aside {
    font-size: var(--font-size-small);
}

button,
input[type="submit"] {
    font-family: inherit;
    font-size: 1rem;
    padding: 0.2rem 0.75rem;
}

/*
 * Sign-out is a POST, so the link bar carries a form. It has to sit inline between two
 * " . . . " separators and look like the anchors either side of it.
 */
.linkbar-form {
    display: inline;
}

.linkbar-button {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    text-decoration: underline;
    cursor: pointer;
}

/* ------------------------------------------------------------------ *
 * Rate this thread
 *
 * Reproduces `ShowRateThisThread`'s layout table — the label ends flanking a centred
 * column of the rating GIF over five radio buttons — as a flex row that can wrap on a
 * phone, where 108px of GIF plus two text labels does not fit.
 * ------------------------------------------------------------------ */

.rate-thread {
    max-width: 42rem;
    margin: 1.5rem auto;
    text-align: center;
}

.rate-heading {
    margin: 0 0 0.25rem;
}

.rate-heading img {
    vertical-align: middle;
}

.rate-scale {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: center;
    gap: 0.5rem;
    margin: 0;
    padding: 0;
    border: 0;
}

.rate-choices {
    display: inline-block;
}

/*
 * The five radios sit under the gradient bar and span its width, because the bar is the
 * scale they are choosing a point on.
 */
.rate-radios {
    display: flex;
    justify-content: space-between;
    width: 108px;
}

.rate-radios input {
    margin: 0;
}

.rate-end {
    font-size: var(--font-size-small);
}

.rate-buttons {
    margin: 0.75rem 0 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    justify-content: center;
}

.rate-average,
.rate-invitation {
    margin: 0.5rem 0 0;
    font-size: var(--font-size-small);
}

/*
 * Labels that exist for screen readers and for the radios' hit targets, but that the
 * original expressed only as position along a picture of a gradient.
 */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ------------------------------------------------------------------ *
 * The writer engine (Phase 3)
 *
 * Desktop-first by explicit decision (§6.3): the composer is N reply forms, each with a
 * shortname box, a browse link, two dropdowns, a checkbox and a textarea, and it is dense
 * by nature. The rule is that it must be *usable* on a phone, not optimised for one — so
 * the control rows wrap rather than getting a breakpoint of their own.
 * ------------------------------------------------------------------ */

/*
 * A button that has to read as a link, because in the original it *was* one: [edit],
 * [up], [down], [Delete], "Empty Trash, FOREVER!". Those are all state changes and so
 * they are POSTs here — a GET that mutates is one link prefetcher away from a thread
 * deleting itself — but the label and the square brackets are the site's voice and they
 * survive intact. Colour comes from the theme, like every other link.
 */
.linkish {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    text-decoration: underline;
    cursor: pointer;
}

/* Keeps a form wrapping one of those buttons from breaking the line it sits in. */
.entry-buttons form,
.col-actions form,
.control-toggles form,
.askee-result-actions form,
.favorite-list form,
.slogan-list form,
.file-list form,
.trash-empty form {
    display: inline;
}

/* ----- The writer link bar ----- */

.linkbar--writer {
    margin-bottom: 0.15rem;
}

/* ----- Per-entry writer controls ----- */

/*
 * Sits in the title strip after the name, which is where buttonStr went. It widens the
 * box, and that is faithful — the entry table was content-width in the original too, so a
 * writer's view of a short reply was always a little wider than a reader's.
 */
.entry-buttons {
    margin-left: 0.75rem;
    font-size: var(--font-size-small);
    font-weight: normal;
    white-space: nowrap;
}

.entry-buttons > * {
    margin-right: 0.25rem;
}

.entry-edited {
    cursor: help;
}

.thread-state-note {
    margin: 0 0 var(--entry-gap);
    font-size: var(--font-size-small);
}

/* ----- Inbox and trash ----- */

.inbox {
    width: 100%;
    border-collapse: collapse;
}

.inbox th {
    text-align: left;
    font-size: var(--font-size-small);
}

.inbox td,
.inbox th {
    padding: 0.2rem 0.5rem 0.2rem 0;
    vertical-align: top;
}

.inbox .col-asker,
.inbox .col-time,
.inbox .col-actions {
    white-space: nowrap;
    font-size: var(--font-size-small);
}

/*
 * The prompt itself, under its subject. A capped subject frequently says nothing at all —
 * the original inbox showed only the subject, so triaging meant opening every one.
 */
.inbox-preview {
    display: block;
    font-size: var(--font-size-small);
    opacity: 0.75;
}

.inbox-empty {
    font-size: var(--font-size-large);
}

.trash-empty {
    margin-bottom: 0;
}

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

.control-toggles {
    margin: 0.25rem 0;
    padding-left: 1.25rem;
}

/* ----- The composer ----- */

.composer-form {
    margin: 0 0 1.25rem;
    padding: 0;
    border: 0;
}

.composer-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    margin-bottom: 0.35rem;
    font-size: var(--font-size-small);
}

.composer-shortname {
    width: 8em;
}

/*
 * The shortname type-ahead (conversatron.js). The wrapper is a positioning context the
 * dropdown hangs off; both it and the list are built by script, so with scripting off the
 * shortname box is just a plain input and none of this applies.
 *
 * Painted from the theme's own chrome colour rather than a colour of its own — a floating
 * menu the theme never defined would read as not belonging to the page. The title-bar
 * background sits a shade off the panel body it floats over, and the highlight is a neutral
 * translucent grey so it lands on both the light themes and the dark ones without a variable
 * per theme.
 */
.composer-typeahead {
    position: relative;
    display: inline-block;
}

.composer-suggest {
    position: absolute;
    top: 100%;
    left: 0;
    z-index: 20;
    min-width: 15rem;
    max-width: 22rem;
    max-height: 15rem;
    margin: 0.15rem 0 0;
    padding: 0;
    overflow-y: auto;
    list-style: none;
    background: var(--entry-title-bg);
    border: 1px solid currentColor;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
}

.composer-suggest[hidden] {
    display: none;
}

.composer-suggest-option {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.2rem 0.4rem;
    cursor: pointer;
}

.composer-suggest-option img {
    flex: none;
    width: 24px;
    height: 30px;
    object-fit: cover;
}

.composer-suggest-name {
    flex: 1 1 auto;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.composer-suggest-shortcut {
    flex: none;
    font-family: monospace;
    font-size: var(--font-size-small);
    opacity: 0.7;
}

.composer-suggest-option:hover,
.composer-suggest-option[aria-selected="true"] {
    background: rgba(127, 127, 127, 0.4);
}

.composer-side {
    display: inline-flex;
    align-items: center;
    gap: 0.2rem;
}

/*
 * The confirmation thumbnail — not in the original, which gave no feedback at all until
 * after you posted. Reserved even when empty so naming a character does not shove the row
 * sideways mid-type.
 */
.composer-preview {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-height: 50px;
}

.composer-preview img {
    width: 40px;
    height: 50px;
}

.composer-preview-name {
    font-weight: bold;
}

.composer-form textarea,
.composer .panel-body textarea {
    width: 100%;
    max-width: 44rem;
}

.composer-post {
    margin: 0.5rem 0;
}

.composer-more {
    margin-top: 1rem;
    font-size: var(--font-size-small);
}

.composer-more input[type="text"] {
    width: 3em;
}

/* ----- Askee picker ----- */

.askee-search {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    margin-bottom: 0.75rem;
}

.askee-results,
.favorite-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.askee-result {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    padding: 0.35rem 0;
}

.askee-result img {
    width: var(--portrait-width);
    height: var(--portrait-height);
}

.askee-result-detail {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.askee-result-name {
    font-weight: bold;
}

/*
 * The shortcut is the entire point of this page — the handbook told writers to copy and
 * paste it — so it is monospaced and selectable rather than decorative.
 */
.askee-shortname {
    user-select: all;
}

.askee-retired,
.askee-moods {
    font-size: var(--font-size-small);
    opacity: 0.75;
}

.askee-result--retired .askee-result-name {
    text-decoration: line-through;
}

.askee-result-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    font-size: var(--font-size-small);
}

.favorite-list li {
    padding: 0.15rem 0;
}

/* ------------------------------------------------------------------ *
 * The askee library (Phase 4)
 *
 * Wider than the 42rem the account panels use, because this is the one place on the site
 * that is genuinely a control surface: 77 tags, a list of portraits with a form on each
 * row, and a name field holding HTML. Squeezing it into the reading measure would make it
 * worse at its job for no gain — nothing here is prose.
 * ------------------------------------------------------------------ */

.askee-panel {
    max-width: 60rem;
}

.askee-add {
    margin-top: 0;
}

.askee-browsing {
    margin: 0.75rem 0 0.25rem;
}

.askee-back {
    max-width: 60rem;
    margin: 0 auto 1.5rem;
}

/* The portrait and the name both go to the character's page. */
.askee-result-portrait {
    flex: none;
    line-height: 0;
}

/*
 * The newest-added strip. The original laid this out as a 5×3 grid of bare portraits with
 * the name in a `title` attribute — which meant the one listing showing you what everyone
 * looked like was also the one that would not tell you their shortcut. Same rows as every
 * other listing here, wrapped into columns.
 */
.askee-results--newest {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
    gap: 0.25rem 1rem;
}

.favorite-summary {
    list-style: none;
    margin: 0 0 0.75rem;
    padding: 0;
    columns: 12rem;
}

.favorite-sort {
    margin-top: 0.75rem;
}

/* ----- Facts, for a writer who cannot edit them ----- */

.askee-facts {
    margin: 0;
}

.askee-facts dt {
    font-weight: bold;
}

.askee-facts dd {
    margin: 0 0 0.5rem;
}

/* ----- Tags ----- */

.tag-picker {
    margin: 0 0 1rem;
    padding: 0.5rem;
    border: 1px solid currentColor;
}

.tag-groups {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    gap: 0.75rem;
}

.tag-group h3,
.tag-browse h3 {
    margin: 0 0 0.25rem;
    font-size: 1rem;
}

.tag-list {
    list-style: none;
    margin: 0;
    padding: 0;
    font-size: var(--font-size-small);
}

.tag-list label {
    display: flex;
    align-items: baseline;
    gap: 0.3rem;
    font-weight: normal;
}

.tag-new {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    margin-top: 0.75rem;
}

.tag-new label {
    font-weight: bold;
}

/* The browse listing: every tag at once, with its count, because they all fit. */
.tag-browse {
    margin-bottom: 0.75rem;
}

.tag-cloud {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 0.9rem;
    list-style: none;
    margin: 0;
    padding: 0;
    font-size: var(--font-size-small);
}

.tag-count {
    opacity: 0.7;
}

/* ----- The image list editor ----- */

/*
 * Verbatim from askeeman2.py. It earns the shout: [retire] refuses to take the last
 * picture away and a missing file gets called out per row, both because of this line.
 */
.picture-warning {
    margin-top: 0;
}

.picture-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.picture {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    padding: 0.4rem 0;
}

.picture img {
    width: var(--portrait-width);
    height: var(--portrait-height);
    flex: none;
}

.picture--retired img {
    opacity: 0.5;
}

.picture-detail {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    min-width: 0;
}

/*
 * One form per row, rather than one form around the whole list with per-row buttons in it.
 * The buttons are formaction submits of *this* row's form, so pressing [up] cannot discard
 * a label you were half way through typing two rows down.
 */
.picture-form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    font-size: var(--font-size-small);
}

.picture-static {
    margin: 0;
}

.picture-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: 0;
    font-size: var(--font-size-small);
    opacity: 0.85;
}

.picture-flag {
    white-space: nowrap;
}

.picture-add,
.picture-first {
    margin: 0;
    padding: 0;
    border: 0;
}

.picture-first {
    margin-bottom: 1rem;
}

.picture-add h3,
.picture-first legend {
    margin: 0 0 0.5rem;
    padding: 0;
    font-size: var(--font-size-large);
    font-weight: normal;
}

/*
 * What the file you just picked will look like at the size it will be used. The original
 * gave no preview at all — you uploaded in one window, typed the filename in another, and
 * found out on the next page load.
 */
.upload-preview {
    display: block;
    margin: 0.35rem 0;
    width: var(--portrait-width);
    height: var(--portrait-height);
}

/*
 * The rule above is an author rule and `[hidden]` in the UA stylesheet is not, so `display:
 * block` wins and the empty preview renders as an 80×100 hole until a file is picked. The
 * script toggles the attribute; this is what makes toggling it do anything.
 */
.upload-preview[hidden] {
    display: none;
}

/* ----- Resolver popup ----- */

.resolver {
    font-size: var(--font-size-large);
}

.handbook-byline {
    text-align: right;
    font-size: var(--font-size-small);
}

/* The writer-only block under the new-topic form. */
.prompt-as-askee {
    margin: 0 0 0.75rem;
    padding: 0.5rem;
    border: 1px solid currentColor;
}

.prompt-as-askee legend {
    font-size: var(--font-size-small);
}

.prompt-as-askee .field-hint {
    margin-bottom: 0;
}

/* ------------------------------------------------------------------ *
 * The admin tools (Phase 5)
 *
 * Same 60rem measure as the askee library, and for the same reason: these are control
 * surfaces, not prose. Nothing here invents a colour — the flags and panels reuse what the
 * writer engine already defines.
 * ------------------------------------------------------------------ */

.admin-lists {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* "View banned users" sat under a blank line in userman.py. It still does. */
.admin-lists-gap {
    margin-top: 0.6rem;
}

.user-table {
    border-collapse: collapse;
    width: 100%;
}

.user-table th {
    text-align: left;
}

.user-table th,
.user-table td {
    padding: 0.15rem 0.6rem 0.15rem 0;
    vertical-align: top;
}

/* Portrait beside the facts, like every other place a picture and a name go together. */
.user-summary {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
}

.user-summary img {
    width: var(--portrait-width);
    height: var(--portrait-height);
    flex: none;
}

.user-paging {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.75rem;
    margin-bottom: 0;
}

.rating-history {
    list-style: none;
    margin: 0;
    padding: 0;
    columns: 22rem;
    font-size: var(--font-size-small);
}

/* ----- Theme manager ----- */

/*
 * The mode chooser and each mode's controls are fieldsets, so the radio group and the
 * day/night pair each read as one thing. Same thin frame as the prompt-as-askee block; no
 * new colour, and the selects keep the plain look every other admin dropdown has.
 */
.theme-choose,
.theme-mode-panel {
    margin: 0 0 1rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid currentColor;
}

.theme-choose legend {
    font-weight: bold;
}

/* A theme and its "starts at" hour sit on one row where there is room, and stack on a phone. */
.theme-schedule {
    display: flex;
    flex-wrap: wrap;
    gap: 0 1.5rem;
}

.theme-schedule .field {
    flex: 1 1 14rem;
}

/* ----- File manager ----- */

.file-dirs {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 0.9rem;
}

/*
 * fileman.py printed its listing into a four-column layout table by dividing the file
 * count by four and counting rows. Columns do that without arithmetic, and they reflow.
 */
.file-list {
    list-style: none;
    margin: 0;
    padding: 0;
    columns: 24rem;
    font-size: var(--font-size-small);
}

.file-list li {
    break-inside: avoid;
}

.file-size {
    opacity: 0.7;
}

/* ----- Slogan manager ----- */

.slogan-list {
    list-style: none;
    margin: 0 0 1rem;
    padding: 0;
}

.slogan-list li {
    padding: 0.15rem 0;
}

/*
 * A slogan is an HTML fragment and some of them are <marquee>. Containing it here keeps a
 * runaway one from dragging the row's [edit] and [delete] off the page with it.
 */
.slogan-text {
    display: inline-block;
    max-width: 40rem;
    vertical-align: bottom;
    overflow: hidden;
}

/* ------------------------------------------------------------------ *
 * Archives — the month/day indexes and the best-of
 * ------------------------------------------------------------------ */

/*
 * The 2005 archive banner was `<td align="center" bgcolor=#000000><font color=#ffffff
 * size="+2">October, 2005</font></td>` in a full-width table beside the mini logo. Same
 * shape as .doc-title, constrained to the thread list's own width so the heading and the
 * table under it share an edge.
 */
.archive-heading {
    max-width: 42rem;
    margin: 0 auto 1rem;
    padding: 0.35rem 0.75rem;
    text-align: center;
    font-size: var(--font-size-large);
    font-weight: normal;
}

.archive-intro {
    max-width: 42rem;
    margin: 0 auto 1.5rem;
    text-align: center;
    font-size: var(--font-size-small);
}

/*
 * Prev / up / next across the foot of an index. Newer on the left and older on the right,
 * matching the direction prev/next runs on a thread.
 */
.archive-nav {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 0.5rem 1rem;
    max-width: 42rem;
    margin: 1rem auto 0;
    font-size: var(--font-size-small);
}

/* An end of the list renders unlinked rather than missing, so the row keeps its shape. */
.archive-nav-end {
    opacity: 0.55;
}

.archive-note {
    max-width: 42rem;
    margin: 1rem auto 0;
    text-align: center;
    font-size: var(--font-size-small);
    opacity: 0.7;
}

@media (max-width: 34rem) {
    /*
     * A period index is two columns, not four, so it keeps its single line on a phone.
     * The generic .thread-row grid drops everything but the subject onto a second row,
     * which is right for a thread — subject, then a meta line — and wrong for "October,
     * 2005 … 74", where the count belongs beside the name it counts.
     */
    .thread-list.period-list tr.thread-row {
        grid-template-columns: 1fr auto;
    }

    .thread-list.period-list .col-subject {
        grid-row: 1;
        grid-column: 1;
    }

    .thread-list.period-list .col-count {
        grid-row: 1;
        grid-column: 2;
        text-align: right;
    }

    .archive-nav {
        justify-content: center;
        text-align: center;
    }
}

/*
 * The archives row that closes the front page thread table. `standard_boxes.xml` had it
 * right-aligned across all four columns in Georgia, with a 5x9 arrow ahead of the word.
 */
.thread-list .thread-archives td {
    padding: 0.25rem;
    text-align: right;
    font-family: Georgia, "Times New Roman", serif;
}

.thread-list .thread-archives img {
    margin-right: 0.25rem;
    vertical-align: baseline;
}

@media (max-width: 34rem) {
    /*
     * tfoot needs the same display:block the rest of the table gets, or it stays a
     * table-footer-group inside a block-display table and gets wrapped in an anonymous
     * box — the same trap documented on `caption` above.
     */
    .thread-list tfoot,
    .thread-list tr.thread-archives,
    .thread-list .thread-archives td {
        display: block;
    }
}

/*
 * "Walking Best Of — every thread". Says why the arrows above just skipped four months,
 * which is otherwise unexplainable from the page. Only rendered for a non-default lane.
 */
.thread-nav-lane {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: center;
    gap: 0.25rem 0.5rem;
    margin: 0.25rem 0 1rem;
    font-size: var(--font-size-small);
}

/*
 * "14 of 340" — the completion hook. Only ever rendered for a bounded lane, because
 * counting means scanning the whole run (see ThreadLane.Counts).
 */
.thread-nav-count {
    font-variant-numeric: tabular-nums;
}

/* "that's all of them, next starts over". */
.thread-nav-end {
    opacity: 0.75;
}

.thread-nav-links {
    white-space: nowrap;
}

/*
 * An arrow that loops back to the other end rather than stepping. Dimmed a little so the
 * difference is visible before it is clicked, not only after.
 */
.thread-nav a.thread-nav-wrap {
    opacity: 0.65;
}

.thread-nav a.thread-nav-wrap:hover,
.thread-nav a.thread-nav-wrap:focus {
    opacity: 1;
}

.archive-random {
    max-width: 42rem;
    margin: 1rem auto 0;
    text-align: center;
    font-size: var(--font-size-small);
}

/* ================================================================== *
 * Theme role bindings
 *
 * The half of theming that is the same for every theme: which element plays which
 * role. Colours come from the custom properties a theme stylesheet defines, and every
 * value below is a bare `var(--x)` with no literal and no fallback (see the file header).
 *
 * This section used to live inside `theme-classic.css`, repeated per theme. Lifting it
 * here is what makes a theme a block of custom properties rather than 280 lines of
 * selectors, and it is why `classic` and the archive themes cannot drift apart in which
 * surfaces they cover.
 *
 * The properties a theme must define are exactly the ones referenced below. Most map
 * 1:1 onto a key from the 2005 `.ini` files; the three that do not are called out where
 * they are used.
 * ================================================================== */

/* ----- Page ----- */

body {
    color: var(--body-text);
    background-color: var(--body-bg);
    background-image: var(--body-bg-image);
}

a {
    color: var(--link);
}

a:visited {
    color: var(--link-visited);
}

a:active {
    color: var(--link-active);
}

/* ----- Entries ----- */

.entry-name {
    color: var(--entry-name-askee);
    background: var(--entry-title-bg);
}

/* An asker speaking as themselves, rather than a writer voicing a character. */
.entry--asker .entry-name {
    color: var(--entry-name-asker);
}

/*
 * Mood panels. Note these are a property of the *image* a writer picked, not an
 * independent axis — see §3 `mood`. ~80% of imported characters have a single Neutral
 * image and so render the neutral panel until someone adds a labelled variant.
 */
.entry-body {
    background: var(--entry-neutral);
}

.entry--happy .entry-body {
    background: var(--entry-happy);
}

.entry--angry .entry-body {
    background: var(--entry-angry);
}

.entry--sad .entry-body {
    background: var(--entry-sad);
}

/* Ghost entries: an asker's suggested follow-up, visible only to writers. */
.entry--ghost .entry-body {
    color: var(--entry-ghost-text);
    background: var(--entry-ghost-bg);
}

/*
 * NOT a 2005 key. A ghost panel is a light grey island in an otherwise dark theme, so the
 * body link colour lands on it unreadable — classic's yellow on `#cccccc`. Each theme
 * names a link colour that works on its own ghost panel.
 */
.entry--ghost .entry-body a {
    color: var(--entry-ghost-link);
}

.entry-responses hr {
    border: 0;
    border-top: 1px solid var(--body-text);
}

/* ----- Front page thread list ----- */

.thread-list-frame {
    background: var(--home-table-color);
}

.thread-list caption,
.thread-list .thread-day th {
    background: var(--home-table-color);
}

/*
 * Banding comes from an explicit class, not :nth-of-type. The original toggled a flag
 * once per thread and did not reset it at a weekday separator, so the stripes run
 * continuously through the separator rows — which :nth-of-type cannot express, because
 * it would count the separators as siblings and flip the phase at every one.
 */
.thread-list .thread-row--odd {
    background: var(--home-odd-color);
}

.thread-list .thread-row--even {
    background: var(--home-even-color);
}

/* The archives row takes the table's own heading colour, as in standard_boxes.xml. */
.thread-list .thread-archives td {
    background: var(--home-table-color);
}

/* ----- Panels: account pages, forms, notices ----- */

/*
 * No 2005 theme styled these — `StartBox` painted them with whatever `ucolor` held, and
 * most themes never defined it, so the login form rendered as unstyled text on the tiled
 * background. Every theme reuses its own entry strip and panel colours here rather than
 * inventing a second palette (§6.4: don't invent values).
 */
.panel h2 {
    color: var(--entry-name-askee);
    background: var(--entry-title-bg);
}

.panel-body {
    background: var(--entry-neutral);
}

/* An error box takes the angry panel — the same red an angry askee gets. */
.panel--error .panel-body {
    background: var(--entry-angry);
}

/* A completed action takes the happy one. */
.notice {
    background: var(--entry-happy);
}

/* The nag is not a success and not an error — the neutral panel, so it recedes. */
.notice--nag {
    background: var(--entry-neutral);
}

/* A refusal — a taken shortcut, a file that is not a picture. The angry panel. */
.notice--bad,
.form-errors ul {
    background: var(--entry-angry);
}

.linkbar-button {
    color: var(--link);
}

.linkbar-button:active {
    color: var(--link-active);
}

/* ----- Static documents ----- */

/*
 * NOT a 2005 key. The htmldocs banner and the archive heading were `bgcolor=#000000` with
 * white text in every theme — black rather than the entry strip's colour, and the original
 * hardcoded it in the document rather than the theme. Named here so a light theme can
 * avoid a black bar it never had.
 */
.doc-title,
.archive-heading {
    color: var(--doc-banner-text);
    background: var(--doc-banner-bg);
}

.doc-panel h2 {
    background: var(--entry-title-bg);
}

.doc-panel p,
.doc-panel ul {
    background: var(--entry-neutral);
}

/* links.html wrapped its whole list in a green panel. */
.doc-panel--links p,
.doc-panel--links ul {
    background: var(--entry-happy);
}

/* ----- The writer engine ----- */

/*
 * Writer controls are links in everything but their HTTP method, so they take the link
 * colours rather than a colour of their own.
 */
.linkish {
    color: var(--link);
}

.linkish:active {
    color: var(--link-active);
}

/* The button strip sits in the title bar, so its time and asterisk match the name. */
.entry-buttons {
    color: var(--entry-name-askee);
}

.inbox th,
.user-table th {
    background: var(--entry-title-bg);
}

/*
 * A portrait whose file is not on disk. The same red the error box takes, because it is
 * the thing askeeman2.py shouted about in blink and the only thing on that page that is
 * actually broken.
 */
.picture-flag--bad {
    padding: 0 0.3rem;
    background: var(--entry-angry);
    opacity: 1;
}

/*
 * A thread the public cannot see yet. The angry panel, because it is the same "watch out"
 * the error box uses and there is rarely a fifth colour in a theme to spend on it.
 */
.thread-state-note {
    padding: 0.25rem 0.5rem;
    background: var(--entry-angry);
}

/* ================================================================== *
 * Entry chrome
 *
 * Three themes drew a frame around the entry instead of filling a rectangle, and in 2005
 * each cost a whole nested-table template — which is what made them the expensive ones to
 * port. Layered backgrounds do it from the same GIFs: no sprite sheets, no new assets, no
 * markup beyond what every other theme already emits.
 *
 * All three keep the panel grid above and only decorate it, which is why they are a
 * separate axis (`data-entry-chrome`) from `data-entry-layout` rather than more layouts.
 *
 * Every image below arrives as a custom property with a `none` fallback — see the note in
 * the file header on why decoration is allowed a fallback and a role is not.
 * ================================================================== */

/* ----- aquaui: an Aqua window per entry ----- */

/*
 * `aquaui.xml` built this from a six-cell table wrapping two more: a title bar of
 * left cap + tiled middle + right cap, vertical shadow strips down each side, and a
 * three-part bottom cap. Here the sides are the entry's own background and the two caps
 * are one element each.
 */
[data-entry-chrome="aqua"] .entry {
    padding: 0 13px 0 12px;
    background-image:
        var(--aqua-shadow-left, none),
        var(--aqua-shadow-right, none);
    background-repeat: repeat-y, repeat-y;
    background-position: left top, right top;
}

/*
 * The title bar. Three layers on the strip itself: the two end caps pinned left and right,
 * and the 1px-wide middle tiled behind them. The padding keeps the name clear of the caps —
 * 87px of left cap plus the 25px icon that sat next to it.
 */
[data-entry-chrome="aqua"] .entry-name {
    height: 24px;
    box-sizing: border-box;
    padding: 0 54px 0 112px;
    line-height: 24px;
    background-color: transparent;
    background-image:
        var(--aqua-title-icon, none),
        var(--aqua-title-left, none),
        var(--aqua-title-right, none),
        var(--aqua-title-fill, none);
    background-repeat: no-repeat, no-repeat, no-repeat, repeat-x;
    background-position: 87px top, left top, right top, left top;
}

/* The rounded bottom, same trick: two caps over a tiled middle. */
[data-entry-chrome="aqua"] .entry::after {
    content: "";
    grid-column: 1 / -1;
    display: block;
    height: 20px;
    background-image:
        var(--aqua-bottom-left, none),
        var(--aqua-bottom-right, none),
        var(--aqua-bottom-fill, none);
    background-repeat: no-repeat, no-repeat, repeat-x;
    background-position: left top, right top, left top;
}

/*
 * `entry_cell_text` ended with a 250px spacer GIF, which is a minimum width written the
 * only way 2002 had of writing one.
 */
[data-entry-chrome="aqua"] .entry-body {
    min-width: 250px;
}

/* ----- sketch: hand-drawn frames, one round the portrait and one round the body ----- */

/*
 * The portrait frame is nine fixed pieces around an exactly 80x100 image, so it needs no
 * tiling at all — padding on the <img> opens the space and the backgrounds fill it. The
 * grid column widens to match, because the framed portrait is 139px across, not 80.
 */
[data-entry-chrome="sketch"] .entry {
    grid-template-columns: 139px 1fr;
}

[data-entry-chrome="sketch"] .entry--right {
    grid-template-columns: 1fr 139px;
}

[data-entry-chrome="sketch"] .entry-portrait {
    box-sizing: content-box;
    width: var(--portrait-width);
    height: var(--portrait-height);
    padding: 20px 39px 47px 20px;
    background-image:
        var(--sketch-pic-top-left, none),
        var(--sketch-pic-top, none),
        var(--sketch-pic-top-right, none),
        var(--sketch-pic-left, none),
        var(--sketch-pic-right, none),
        var(--sketch-pic-bottom-left, none),
        var(--sketch-pic-bottom, none),
        var(--sketch-pic-bottom-right, none);
    background-repeat: no-repeat;
    background-position:
        left top, 20px top, right top,
        left 20px, right 20px,
        left bottom, 20px bottom, right bottom;
}

/*
 * The body frame is one drawn box spanning *two* grid rows, because `sketch.xml` puts the
 * name inside it — its own cell above the text, `bgcolor="{namecolor}"`, with the template
 * even carrying the comment "330000 for askers, 000033 for askees".
 *
 * So the frame is split down the middle of the component: the name element carries the top
 * edge and corners, the body carries the bottom, and both tile the sides. Together they
 * read as a single frame. Drawing the whole thing on the body instead leaves the name
 * stranded above it, which is what the first attempt at this did.
 */
[data-entry-chrome="sketch"] .entry-name {
    grid-column: 2;
    padding: 17px 37px 5px 19px;
    color: var(--body-text);
    background-color: var(--entry-name-askee);
    background-image:
        var(--sketch-text-top-left, none),
        var(--sketch-text-top-right, none),
        var(--sketch-text-top, none),
        var(--sketch-text-left, none),
        var(--sketch-text-right, none);
    background-repeat: no-repeat, no-repeat, repeat-x, repeat-y, repeat-y;
    background-position: left top, right top, left top, left top, right top;
}

[data-entry-chrome="sketch"] .entry--right .entry-name {
    grid-column: 1;
}

/* The name colours are backgrounds here, not text colours — see above. */
[data-entry-chrome="sketch"] .entry--asker .entry-name {
    color: var(--body-text);
    background-color: var(--entry-name-asker);
}

/*
 * The writer button strip sits on the name cell, and the role binding paints it with the
 * name colour — which in this one theme is the colour *behind* it.
 */
[data-entry-chrome="sketch"] .entry-buttons {
    color: var(--body-text);
}

[data-entry-chrome="sketch"] .entry-body {
    padding: 5px 37px 46px 19px;
    background-image:
        var(--sketch-text-bottom-left, none),
        var(--sketch-text-bottom-right, none),
        var(--sketch-text-bottom, none),
        var(--sketch-text-left, none),
        var(--sketch-text-right, none);
    background-repeat: no-repeat, no-repeat, repeat-x, repeat-y, repeat-y;
    background-position: left bottom, right bottom, left bottom, left top, right top;
}

/* ----- trendwhore: a flat outer frame, portrait inside the panel ----- */

/*
 * `<table bgcolor="#26354F" cellpadding="3">` around the whole entry, with both the
 * portrait cell and the text cell painted the mood colour — so unlike every other theme the
 * portrait sits *on* the panel rather than beside it.
 */
[data-entry-chrome="framed"] .entry {
    padding: 3px;
    background: var(--entry-frame, transparent);
    width: 90%;
}

[data-entry-chrome="framed"] .entry-portrait {
    background: var(--entry-neutral);
}

[data-entry-chrome="framed"] .entry--happy .entry-portrait {
    background: var(--entry-happy);
}

[data-entry-chrome="framed"] .entry--angry .entry-portrait {
    background: var(--entry-angry);
}

[data-entry-chrome="framed"] .entry--sad .entry-portrait {
    background: var(--entry-sad);
}

/* `class="name"` — white on the frame colour, not a strip of its own. */
[data-entry-chrome="framed"] .entry-name {
    padding-left: 0.35rem;
    background: transparent;
}

/*
 * The banded stripe across the top of every trendwhore page. In 2002 this was three
 * absolutely-positioned layers stacked by z-index; it is one repeating background now.
 *
 * `content: none` suppresses a pseudo-element entirely, so the fallback is what makes this
 * opt-in: a theme turns the stripe on by setting `--page-stripe-content: ""`.
 */
.page::before {
    content: var(--page-stripe-content, none);
    display: block;
    height: var(--page-stripe-height, 0);

    /*
     * Full-bleed, because the original was `position: absolute; top: 0; right: 0` across
     * the whole viewport rather than the content column. `html` already hides horizontal
     * overflow, so 100vw is safe here.
     */
    width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-bottom: var(--page-stripe-gap, 0);

    /*
     * Stretched, not tiled: the 2002 markup was `<img width="100%" height="116">` on an
     * 84px-wide GIF, so the smear is the effect rather than an accident of scaling.
     */
    background-image: var(--page-stripe, none);
    background-repeat: no-repeat;
    background-size: 100% 100%;
}
