/*
 * site.css — public-facing styles (§8, §6d).
 *
 * ONE FONT SIZE for all public content; hierarchy comes from position and
 * spacing alone, never size variation. Admin chrome uses its own single,
 * smaller size — see edit.css.
 *
 * Colours are all derived custom properties from services/colors.php (§9):
 * nothing here hardcodes a colour. Text colour in particular is computed for
 * contrast and is never the artist's choice.
 */

:root {
    /* Fallback only, for a render with no site settings loaded. The real
       value arrives from the palette block in layout.php, which is emitted
       AFTER this file precisely so it overrides these — same specificity,
       last one wins. Do not move that block above the stylesheets. */
    --font: 'Inter', system-ui, sans-serif;
    --size: 16px;
    --unit: calc(1rem * var(--spacing-scale));
    --measure: 34rem;

    /*
     * TWO CONTROL HEIGHTS, sibling to --unit: that one is the shared spacing
     * grid, these are the shared control-height grid.
     *
     *   --line-l  a distinct, standalone control — Info/Updates
     *             (.page-button), a category tag (.filter button),
     *             Save/Cancel/Delete (.actions button), Menu/Close
     *             (.menu-toggle), the icon anchors beside the title
     *             (.anchor). Things that read as THE control for what they do.
     *   --line-s  a small, single-purpose icon action riding on top of
     *             something else — a block's edit/move/delete row
     *             (.controls), the "+" that adds a block or a page
     *             (.chrome-button, .add-block). Things that read as a detail
     *             of the thing they sit on, not as a control in their own
     *             right.
     *
     * Two sizes, not one, because collapsing them was tried and looked wrong:
     * a delete-block icon and the Save button are not the same kind of
     * control, and forcing them to the same footprint made the whole editor
     * read as louder than it needed to be everywhere the small ones appear.
     *
     * NOT derived from the title text's own line box (was 1.5rem once, to
     * match it exactly) — deliberately decoupled. The title keeps its own
     * `line-height: 1.5` regardless of these; nav.top centres it against
     * --line-l (see nav.top below) rather than the two being forced to the
     * same number.
     *
     * REM, DELIBERATELY, NOT EM, for both. A custom property's relative units
     * resolve where the property is USED, not where it is declared, and these
     * are read on elements that set their own font-size — `body > aside` is
     * 14px (--edit-size), while a public `.page-button` is the root's 16px.
     * `1em` would resolve against whichever of those happened to be reading
     * it, and the same button would render two different heights depending on
     * where it appeared. `rem` always resolves against the root, so every
     * consumer gets the identical pixel value regardless of local font-size.
     */
    --line-l: 2.25rem;
    --line-s: 1.5rem;

    /*
     * The height of the fixed top bar: one control's height plus its padding.
     * Everything that has to sit clear of it — the drawer, the page body —
     * measures from this one value rather than each guessing its own.
     */
    --topbar-h: calc(var(--line-l) + var(--unit) * 2);

    /*
     * The menu slide. One value drives nav.top, .shell and nav.menu together —
     * that is what makes them read as a single surface rather than three
     * elements animating in parallel.
     */
    /* Layout tracks (§6d). --pad is the page's outer margin, and it is a GRID
       TRACK rather than padding on a container, so a full-bleed block can be
       assigned across it instead of fighting it. */
    --pad: var(--unit);
    --gutter: calc(var(--unit) * 2);
    --rhythm: calc(var(--unit) * 2);

    --menu-w: min(92vw, 44rem);
    --shift: 0px;
}

body[menu] { --shift: calc(-1 * var(--menu-w)); }

/*
 * The page and the menu slide; THE TOP BAR DOES NOT. It stays put so the Menu
 * button never leaves the viewport — which is what lets the same button close
 * the menu again.
 *
 * Three layers, and the menu is deliberately BETWEEN the two halves of the bar:
 *
 *     .menu-toggle   8   must stay clickable over the open menu
 *     nav.menu       7
 *     nav.top        6   titles pass UNDER the menu
 *     .shell         -   ordinary content
 *
 * The whole site layer stays below 9, which is where edit.css starts: its
 * pane scrim is 9 and the chrome anchor 10, so edit chrome dims and covers
 * the site rather than interleaving with it.
 *
 * The titles used to share the bar's layer and sit above the menu, so on a
 * tablet-width viewport the panel's left edge cut through the page title and
 * the text showed on top of the panel. They belong under it; the button does
 * not. Since `position: fixed` establishes a stacking context regardless of
 * z-index, the button had to leave nav.top entirely to outrank the menu — see
 * layout.php.
 */
.shell,
nav.menu {
    transform: translateX(var(--shift));
    transition: transform 0.28s ease;
}

@media (prefers-reduced-motion: reduce) {
    .shell, nav.menu { transition: none; }
}

* { box-sizing: border-box; }

/*
 * Markers off for every list this design uses as a layout structure — nav,
 * menu, filters, footer, and the computed listings. Declared once here rather
 * than repeated per component: each of those was previously turning it off
 * for itself, and a new list meant remembering to do it again. Prose lists
 * A `list` block's <ul> is NOT included — that is content, and a list an
 * artist made should look like one. It takes the browser's own bullets and
 * indent rather than a rule here: §1's barebones default is correct for once,
 * and markdown has no list syntax to disagree with it (§14a).
 */
nav ul,
footer ul,
.filter,
.works,
.work-grid,
.updates,
.pages,
.contact,
.legal,
.recent-works ul,
.recent-updates ul,
.thumbnails ul {
    list-style: none;
    margin: 0;
    padding: 0;
}

html {
    font-size: var(--size);
    background: var(--color-bg);
    color: var(--color-ink);
}

body {
    /* Full-viewport height without nesting <footer> inside <main>, which would
       cost the contentinfo landmark (§8). */
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    margin: 0;
    font-family: var(--font);
    font-size: 1rem;
    line-height: 1.5;
}

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

/*
 * --color-muted is reserved for SYSTEM TEXT: words Poco puts on the page
 * itself, never words the artist typed or content they chose to present.
 * "Muted = said by Poco."
 *
 * By that test it belongs on the footer (housekeeping, not the work), on
 * unlisted markers (which no visitor ever sees), and on error-page detail.
 * It does NOT belong on a price, a caption, a date or a quote's attribution —
 * those are all the artist's own, and de-emphasising them says they matter
 * less than the system's own chrome, which is backwards.
 */

img { max-width: 100%; height: auto; display: block; }

/*
 * Visually hidden, still read aloud.
 *
 * MOVED HERE FROM edit.css, where it was defined for the colour swatches.
 * The public join form's honeypot (§16.4) needs it too, and edit.css is not
 * loaded on a public page — so the trap field rendered fully visible, asking
 * every real visitor to "leave this empty". Both stylesheets load together in
 * edit mode, so one definition here serves both.
 */
.sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/*
 * Small print beside a field. Also previously edit.css-only, which left the
 * login page's own `.hint` unstyled on the public side — a pre-existing gap
 * this found rather than introduced.
 */
.hint {
    display: block;
    color: var(--color-muted);
    font-size: 0.875rem;
}

/*
 * SELECTED TEXT, in the artist's own two colours — the site's ink as the
 * highlight, its paper as the letters, so a selection is the page's own
 * inversion rather than the browser's stock blue arriving uninvited over a
 * palette someone chose deliberately.
 *
 * Legibility comes free: §9 already guarantees a contrast ratio between the
 * two seeds, and this rule only swaps which of them is behind and which in
 * front, so it cannot produce a pair the palette had not already cleared.
 *
 * ::selection ACCEPTS ALMOST NOTHING. The spec allows only color,
 * background-color, text-decoration and its longhands, text-shadow,
 * -webkit-text-stroke-color and caret-color; anything else here — a padding, a
 * border, a radius, an opacity — is parsed and then silently dropped. Written
 * as background-color rather than the `background` shorthand for that reason:
 * the shorthand is honoured only for its colour, so the longhand is the honest
 * spelling of what actually happens.
 *
 * No ::-moz-selection twin. Firefox has supported the unprefixed form since
 * 62, which is older than :has(), color-mix() and dvh — all of which this
 * stylesheet already requires — so the prefix would guard nothing.
 */
::selection {
    background-color: var(--color-ink);
    color: var(--color-bg);
}

/* ---- top bar ---------------------------------------------------- */

nav.top {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 6;
    /*
     * EXPLICIT, not left to content. nav.top holds the title always and
     * .page-anchors only while editing a page — an admin-only, admin-only-
     * sometimes child, in a row everything else measures against via
     * --topbar-h. Sized by content, the bar would be one height for a
     * visitor and a taller one for the artist, and --topbar-h can only ever
     * be right for one of them. Fixing it here means it always is.
     */
    height: var(--topbar-h);
    display: flex;
    align-items: center;
    gap: var(--unit);
    /*
     * Deliberately nowrap: a fixed bar whose height changes would invalidate
     * --topbar-h, and everything below measures from that. Overflowing links
     * scroll rather than pushing the bar taller.
     */
    flex-wrap: nowrap;
    overflow-x: auto;
    padding: var(--unit);
    /*
     * The top-right cluster is not in this flex row taking up space, so the row
     * has to keep its place clear or a long title would scroll underneath it.
     * Approximate on purpose — it only has to be wide enough that the title
     * ellipsizes before it reaches the cluster. Widened in v22 for the
     * site-settings anchor that now sits beside Menu.
     */
    padding-right: calc(var(--unit) * 3 + 5ch + 2rem);
}

nav.top .site-title { font-weight: normal; text-decoration: none; color: var(--color-ink); }

/*
 * Site title and page title in one row, one spacing unit apart (§8d).
 *
 * align-items: baseline HERE, not on nav.top — the outer row centres its
 * children against --line-l (below), which the titles must not follow: they
 * are text, and their own natural baseline relationship to each other is
 * what this rule protects regardless of how tall a sibling button gets.
 */
nav.top .titles { display: flex; align-items: baseline; gap: var(--unit); min-width: 0; }
nav.top .page-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/*
 * THE TOP-RIGHT CLUSTER: Menu, and — while editing — site settings beside it.
 * Both are site-wide controls, which is why they are together and why the
 * page's own controls are over by the page title instead.
 *
 * Fixed rather than flex-positioned, because it is not inside the bar: `top`
 * and `right` of one unit reproduce nav.top's own padding, and since these
 * share the chrome font and size with the titles, aligning their tops puts
 * their baselines in the same place.
 *
 * The z-index is the reason this is a sibling of nav.top rather than a child.
 * The open menu passes OVER the titles, but these must stay over the menu —
 * one of them closes it. `position: fixed` establishes a stacking context
 * whatever its z-index, so inside the bar no value could lift them past the
 * bar's own layer. Titles below, cluster above, menu between.
 *
 * --unit for the gap, not --tab-gap (edit.css): that one is 2px, deliberately
 * tight, because it also pulls a pane up flush against the anchor row it
 * opened from. Two independent site-wide controls need enough air to read as
 * two things rather than one control with two heads — the same reasoning
 * --unit is used for everywhere else two unrelated elements need daylight
 * between them, so it is reused rather than picking a new number for it.
 */
.top-right {
    position: fixed;
    top: var(--unit);
    right: var(--unit);
    z-index: 8;
    display: flex;
    align-items: center;
    gap: var(--unit);
}

/*
 * A stable width, so that the site-settings anchor beside it does not shift
 * sideways when the label changes. The cluster is anchored right and laid out
 * in order, so any width change in the last item moves everything before it —
 * and "Menu" becoming "Close" is exactly such a change, on every open.
 */
/*
 * Height pinned to --line-l for the same reason as .anchor (edit.css):
 * this rides beside the settings anchor in the same cluster while editing, and
 * a taller Menu button next to a shorter one reads as two different systems
 * rather than one row.
 *
 * line-height: 1 rather than the inherited 1.5 — with the box fixed at exactly
 * one line's height and a border to fit inside it, the ordinary 1.5 line box
 * leaves no room for that border and would clip it by a pixel on each edge.
 * Centring is what flex does instead, so nothing is lost by shrinking it.
 */
.menu-toggle {
    min-width: 5ch;
    height: var(--line-l);
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
}

/* One word or the other, never both. */
.menu-toggle .when-open,
body[menu] .menu-toggle .when-closed { display: none; }
body[menu] .menu-toggle .when-open { display: inline; }

[current] { text-decoration: underline; }

/* ---- menu ------------------------------------------------------- */

/*
 * Parked off the right edge and brought in by the shared --shift, rather than
 * animating its own position: one transform, three elements, no drift between
 * them. Always in the DOM and always laid out, so opening costs no reflow.
 */
nav.menu {
    position: fixed;
    top: 0;
    right: calc(-1 * var(--menu-w));
    width: var(--menu-w);
    height: 100vh;
    z-index: 7;
    overflow-y: auto;
    display: flex;
    gap: var(--unit);
    padding: var(--unit);
    padding-top: var(--topbar-h);
    background: var(--color-bg);
    border-left: 1px solid var(--color-border);
}

nav.menu .col { flex: 1 1 0; min-width: 0; }
nav.menu .meta-col { flex: 0 1 14rem; }

/*
 * THE YEAR IS PUSHED RIGHT BY AN AUTO MARGIN, not by space-between.
 *
 * With `justify-content: space-between` the free space is shared between
 * EVERY pair of items, so on an unlisted row — title, eye, year — the eye
 * landed marooned in the middle of the gap instead of beside the title it
 * belongs to. It reads as a third column that way, when it is really part of
 * the title: "this one, the hidden one".
 *
 * `margin-left: auto` on the year takes all the free space in one place, so
 * everything before it stays packed at the start in source order and the eye
 * sits directly after the words. The row looks identical on the listed rows
 * that have no eye at all, which is why this went unnoticed.
 */
nav.menu .works a {
    display: flex;
    align-items: center;
    gap: calc(var(--unit) * 0.5);
    padding: calc(var(--unit) * 0.25) 0;
    text-decoration: none;
}

/* padding-left, so a long title still keeps its distance from the year once
   the auto margin has no free space left to give. */
nav.menu .works a time { margin-left: auto; padding-left: var(--unit); }

nav.menu .works a:hover { text-decoration: underline; }

.filter { display: flex; flex-wrap: wrap; gap: calc(var(--unit) * 0.5); }

/* ---- buttons & forms: mostly-default, flat, 1px borders --------- */

/*
 * `.page-button` is an <a> that should read as one of these, so it is listed
 * alongside rather than given a lookalike of its own — one box, one padding,
 * one border, whatever the element underneath. The Menu toggle and the
 * category filters are ordinary <button>s and need nothing extra.
 */
/*
 * Opaque by default, in the site's own background — not transparent. These
 * controls sit in the fixed top bar and the menu, both of which have content
 * moving underneath them; a transparent button shows whatever it happens to be
 * over. The edit chrome does not inherit this: edit.css re-points every
 * control inside `body > aside` and `dialog.editor` at the inverted pair.
 */
button, input, select, textarea, .page-button {
    font: inherit;
    color: inherit;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    /*
     * UA rounding off — Safari and iOS ship rounded inputs, selects and
     * buttons, and none of them agree on how much.
     *
     * THE PUBLIC SITE STAYS SQUARE, all of it, and this is the rule that says
     * so. The edit chrome overrides it (edit.css, --radius) precisely so that
     * a rounded corner MEANS "this is Poco, not your site" — which only works
     * as long as nothing out here has one.
     */
    border-radius: 0;
    padding: calc(var(--unit) * 0.25) calc(var(--unit) * 0.5);
}


/*
 * A BUTTON'S BORDER IS THE FULL INK. --color-border is a 25% mix toward the
 * ink, and dimming is reserved for buttons standing in a group where one of
 * them is active — the category filter, where the quieter outline is what
 * lets the chosen one read as chosen. A control with no siblings to be
 * contrasted against has nothing to express by being dimmer, so Info, Updates
 * and Menu take the ink.
 *
 * Inputs keep the dimmer line: a field is a place to type, not a thing to
 * press, and outlining every one of them in full ink turns a form into a grid.
 */
button, .page-button { border-color: var(--color-ink); }

.filter button { border-color: var(--color-border); }

button, .page-button { color: var(--color-accent); cursor: pointer; }
button[current], .page-button[current] { border-color: var(--color-accent); }

.page-button { display: inline-block; text-decoration: none; }

/*
 * ONE HEIGHT ACROSS EVERY DISTINCT CONTROL — Info/Updates, a category tag,
 * Save/Cancel/Delete (`.actions` — login.php as well as every admin pane and
 * dialog) — all pinned to --line-l rather than left to fall out of font-size and
 * padding, which is what let them drift to different heights depending on how
 * much padding each happened to carry.
 *
 * inline-flex + align-items: center rather than relying on the text's own
 * line-height to fill the box: the fixed height and the 1.5 line-height rarely
 * land on the same number, and flex centring is what keeps the label centred
 * in the box regardless. `.page-button { display: inline-block }` above is
 * overridden here on purpose — inline-flex is what centring needs, and this
 * rule intentionally comes after it.
 *
 * NOT the universal `button` selector two rules up — that one also reaches
 * every <input>, <select> and <textarea> on the site, none of which should be
 * squeezed to a fixed control height.
 */
.page-button,
.filter button,
.actions button {
    height: var(--line-l);
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
}

/*
 * Spaced like the category filter, which is the other place buttons sit
 * together — same half-unit gap, so the two groups are recognisably the same
 * kind of thing. `align-items: start` keeps each button its own width rather
 * than stretching them to the column.
 */
nav.menu .pages {
    margin-top: var(--unit);
    display: flex;
    flex-direction: column;
    align-items: start;
    gap: calc(var(--unit) * 0.5);
}

@media (max-width: 34rem) {
    /*
     * Full width on a phone: leaving a sliver of page showing costs menu room
     * and buys nothing, since the fixed top bar already provides the way out.
     *
     * 100%, NOT 100vw — that was cutting the menu's own left padding off the
     * screen. `vw` counts the classic scrollbar's width; a `position: fixed`
     * element's containing block does not. So the menu was drawn wider than
     * the space it slides into and, pinned to the right, hung off the left by
     * exactly the scrollbar's width — enough to swallow one --unit of padding
     * and leave the text against the edge. Only this breakpoint ever showed
     * it: the wider one is `min(92vw, 44rem)`, and the 8vw it leaves behind
     * absorbs the same overflow without anyone noticing.
     *
     * `100%` resolves against the same containing block the menu is
     * positioned in, so width, `right`, and the shared --shift transform all
     * agree on one number. It stays correct for `.shell` too, which the same
     * --shift translates: as body's only in-flow child it is exactly that
     * wide as well.
     */
    :root { --menu-w: 100%; }
    nav.menu { flex-direction: column; }
    nav.menu .meta-col { flex: none; }

    /*
     * FULL WIDTH, CENTRED — the singleton pages are the menu's primary
     * destinations, and at their natural width they were the same size and
     * shape as the category tags and Close sitting above them, which are
     * filters and a dismissal rather than places to go. Stretching them says
     * which is which by shape alone, without a second colour or a heading.
     *
     * Phone only. On a wide screen these sit in a narrow side column beside
     * the works list, where full width is the column's width and the
     * distinction is already made by position.
     */
    nav.menu .pages { align-items: stretch; }
    nav.menu .pages .page-button { justify-content: center; }
}

/* ---- main ------------------------------------------------------- */

/*
 * body{min-height:100vh} only reaches <main> if every box between them grows
 * too. nav.top and nav.menu are fixed and out of flow, so .shell is body's
 * only in-flow child and has to pass the height down.
 */
.shell { flex: 1; display: flex; flex-direction: column; }

/*
 * Vertical padding only. Horizontal spacing is a grid track inside the article
 * (§6d) — on <main> it would apply to everything, and a cover block could not
 * step outside it. Whatever main holds that is NOT an article (the works
 * listing, login, error) has no grid of its own, so it takes the same value as
 * ordinary padding.
 */
main { flex: 1; padding: calc(var(--topbar-h) + var(--unit)) 0 var(--unit); }
main > :not(article) { padding-inline: var(--pad); }

/*
 * A cover FIRST means the page starts with it: no clearance above, so the
 * media touches the top, left and right edges and the titles float over it.
 *
 * Declarative rather than a flag the template has to set — the condition is
 * "this page happens to begin with a cover", which is a fact about the
 * rendered document, and :has() can read it. Nothing in PHP needs to know.
 *
 * Both shapes again: the first child is the section publicly and the block
 * wrapper while editing. This is also what stops the cover overflowing the
 * viewport in the editor — the admin bar no longer pushes it down, it simply
 * overlays the top of it, so 100dvh still means one screen in both modes.
 */
main:has(> article > .cover:first-child),
main:has(> article > [block][type^="cover-"]:first-child) { padding-top: 0; }

h1, h2 { font-size: 1rem; font-weight: normal; margin: 0 0 var(--unit); }

/*
 * The h1 now lives in the top bar (v19), so it takes the bar's layout rather
 * than a block heading's: no bottom margin, and no underline — that rule
 * existed to mark a page heading sitting above content, and there is no longer
 * one there. h2 keeps both, since `heading` blocks render as h2 in content.
 */
nav.top h1 { margin: 0; }

p { margin: 0 0 var(--unit); max-width: var(--measure); }

/*
 * §6d: inter-block spacing comes from ONE mechanism, on the parent, so two
 * adjacent blocks never each contribute their own margin and double it. This
 * is also what makes two-column's row -> stacked reflow work at the breakpoint
 * with no per-block exceptions.
 */
/*
 * ONE grid for every template (§6d) — REVISED in v20, replacing a flex column
 * plus a separate grid for two-column.
 *
 * Named lines, so a block is placed by naming tracks rather than by overriding
 * inherited padding. `full-start / full-end` is how a cover block goes
 * edge-to-edge; nothing has to be undone for it.
 *
 * row-gap ONLY, never the `gap` shorthand: column-gap would insert space
 * between the padding tracks and the content, making the effective outer
 * margin --pad plus the gap.
 */
/*
 * Keyed on `article`, not `article[region]` — the works listing is an article
 * too and needs the same gutters, but `region` is the editor's async swap
 * target (logic.js queries `[region]`) and borrowing it for a page with no
 * blocks would make the listing look swappable to the JS.
 */
article {
    display: grid;
    grid-template-columns:
        [full-start] var(--pad)
        [content-start] 2fr
        [gutter] var(--gutter)
        [split] 1fr
        [content-end] var(--pad) [full-end];
    row-gap: var(--rhythm);
    align-items: start;
}

/*
 * :where() zeroes this rule's specificity, so it is a DEFAULT rather than a
 * winner. Written plainly as `article > *` it scores (0,0,2) and beats
 * `.col-media` at (0,1,0) — which silently collapsed two-column layouts into
 * one, since every column took the full content track no matter what order the
 * rules appeared in. Zero specificity means any placement rule below simply
 * wins, with no escalation to reason about when the next one is added.
 */
:where(article) > * { grid-column: content-start / content-end; }

section > div:empty { display: none; }

/* ---- work layouts ------------------------------------------------ */

/*
 * The two columns are grid items assigned to tracks, and stay flex columns
 * inside. They are deliberately NOT `display: contents`: that would make every
 * block its own grid item, and auto-placement fills rows monotonically, so the
 * first text block would land below the last media block instead of beside it.
 */
.col-media { grid-column: content-start / gutter; }
.col-text  { grid-column: split / content-end; }

.col-media, .col-text {
    display: flex;
    flex-direction: column;
    gap: var(--rhythm);
}

/*
 * A reduced block is narrower, but everything ABOVE the section stays full
 * width. The admin control row is positioned against the block wrapper
 * (position:absolute, right:0), so any constraint on that wrapper parks the
 * buttons at a different X for reduced blocks than for full ones.
 *
 * Both shapes are named because the section sits one level deeper in admin:
 *
 *     public   <div scale><section>
 *     editing  <div scale><div block><section>
 *
 * A `> *` child selector therefore hits the section on a public page but the
 * BLOCK WRAPPER while editing — which is precisely the bug this replaces.
 * Naming the section explicitly makes the rule mean the same thing in both.
 *
 * No opacity: reduced is about size, not about being faded out.
 */
[scale="reduced"] > section:not(.quote),
[scale="reduced"] > [block] > section:not(.quote) { max-width: var(--measure); }

/*
 * HOW BIG AN IMAGE IS, PER LAYOUT — three answers, because the three layouts
 * are three different arguments about what the page is for.
 *
 * The base rule is `img { max-width: 100% }` (see "images", above): an image
 * is drawn at its natural size and only shrinks if it would not fit. That is
 * the right default for an image dropped into prose, and the wrong one inside
 * a work layout, where the column IS the composition and an image that stops
 * short of it reads as a mistake rather than as a choice. Each layout below
 * therefore states a width rather than a ceiling.
 *
 * All three name `section.image` rather than a bare `img`, so nothing here
 * reaches the cover block — a cover is sized by the viewport (`.cover img`)
 * and is not part of this argument.
 */

/*
 * TWO-COLUMN — media always fills its column, whatever its natural size.
 * The column is the narrower of the two visual masses on the page and its
 * edge is what the eye lines the text up against; a short image breaks that
 * edge. Embeds already fill: `.embed` is a block-level section stretching in
 * a flex column, so only the image needs saying.
 */
.col-media section.image img { width: 100%; }

/*
 * TEXT-LED — media fills the reduced measure, whatever its height.
 * `[scale="reduced"]` already caps the section at --measure; this is what
 * makes every image actually reach that cap, so a column of images down a
 * text-led page has one left and one right edge rather than a ragged stack.
 * No height limit: in this layout the text sets the pace and an image is
 * allowed to be as tall as it is.
 */
article[layout="text-led"] section.image img { width: 100%; }

/*
 * IMAGE-LED — full width, but never taller than the screen.
 *
 * The cap is what keeps this layout scrollable: at full content width a
 * portrait image can run several screens tall, and the artist loses any sense
 * of the sequence they arranged. dvh rather than vh for the same reason
 * `.cover` uses it — vh ignores mobile browser chrome collapsing on scroll.
 *
 * object-fit is doing the real work. With a width AND a max-height both in
 * force, a replaced element is stretched to whatever box survives; `contain`
 * keeps the pixels' own ratio and letterboxes inside that box instead. So a
 * tall image is drawn at the capped height, centred, with page either side —
 * which is the picture the artist expects — rather than squashed to fit.
 */
article[layout="image-led"] section.image img {
    width: 100%;
    max-height: 100dvh;
    object-fit: contain;
}

@media (max-width: 40rem) {
    /* The reflow is one rule: both columns take the whole content track and
       stack. No per-block breakpoint exceptions (§6d). */
    .col-media, .col-text { grid-column: content-start / content-end; }
}

/* ---- embeds ------------------------------------------------------- */

/*
 * The consent facade (§15b) occupies the same box the iframe will, so nothing
 * reflows when it is replaced — the page does not jump under the click that
 * loaded it.
 */
.embed.facade > button {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--unit);
    width: 100%;
    aspect-ratio: 16 / 9;
    padding: var(--unit);
    text-align: center;
    border: 1px solid var(--color-border);
    background: var(--color-shade);
    cursor: pointer;
}

.embed.facade svg { width: 2.5rem; height: 2.5rem; }
.embed.facade span { max-width: var(--measure); }

/* Said by the artist, so not muted (§1.13a of decisions.md). */
.embed .caption { margin: calc(var(--unit) * 0.5) 0 0; }

/* ---- cover blocks (§5) -------------------------------------------- */

/*
 * Edge-to-edge by TRACK ASSIGNMENT, not by undoing padding — that is the whole
 * reason --pad is a grid track (§6d).
 *
 * Both shapes are named, because the section sits one level deeper in admin:
 *
 *     public   <article region><section class="cover">
 *     editing  <article region><div block type="cover-…"><section class="cover">
 *
 * The grid item is therefore the section on a public page and the block
 * wrapper while editing. Naming both is the same fix the [scale="reduced"]
 * rule above already uses, and it is what keeps a cover full-bleed in both.
 */
.cover,
[block][type^="cover-"] { grid-column: full-start / full-end; }

.cover {
    position: relative;
    /* dvh, not vh: vh ignores mobile browser chrome collapsing on scroll, so
       the cover is clipped and the page shifts as the bar hides. */
    height: 100dvh;
    overflow: hidden;
}

.cover img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/*
 * object-fit does not apply to iframes — they are not replaced elements in the
 * relevant sense — so the video is oversized and centred instead. The two
 * min-* values are what guarantee coverage in BOTH directions; without them a
 * portrait phone viewport letterboxes.
 */
.cover iframe {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    border: 0;
    width: 100%;
    height: 56.25vw;        /* 16:9 of viewport width */
    min-height: 100dvh;
    min-width: 177.78dvh;   /* 16/9 — covers tall, narrow viewports */
}

/*
 * The click layer AND the arrow, one element. An iframe swallows pointer
 * events, so the target has to sit above it. Identical in both modes — the
 * block controls simply outrank it on z-index, so nothing about pointer
 * handling changes depending on whether anyone is signed in.
 */
.cover-next {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    padding: 0 0 var(--unit);
    background: transparent;
    border: 0;
    color: inherit;
    cursor: pointer;
}

.cover .empty { padding: var(--unit); color: var(--color-muted); }

/*
 * The sign-in form, centred in the viewport — the one page with a single
 * purpose and nothing else on it, so the classic centred box is what it should
 * look like.
 *
 * Sized to the field rather than the measure: a wide box around one short
 * input reads as an empty page with a form lost in it.
 */
main:has(> .signin) {
    display: grid;
    place-items: center;
    min-height: calc(100dvh - var(--topbar-h) - var(--unit) * 4);
}

.signin {
    width: min(22rem, 100%);
    display: flex;
    flex-direction: column;
    gap: var(--unit);
}

.signin p { margin: 0; }

/*
 * A `.signin` MAY WRAP MORE THAN ITS FORM. The join page puts its intro and
 * its form in one, so `main:has(> .signin)` — a centring grid — receives a
 * single child rather than three. As three, each became its own grid row and
 * min-height pushed them down the viewport with the intro stranded in the
 * middle.
 *
 * The nested form takes the same column so every field, hint and line of text
 * shares one width.
 */
.signin form {
    display: flex;
    flex-direction: column;
    gap: var(--unit);
}

/* The button says what it does; it should not stretch to the column's width
   the way a field does — a field is a space to fill, a button is a target. */
.signin button { align-self: start; }

/*
 * A notice is not an error: the artist did nothing wrong, they were early.
 * It still has to be impossible to miss, because it is the entire answer to a
 * click that otherwise looks like it did nothing.
 */
.signin .notice {
    padding: calc(var(--unit) * 0.5);
    border: 1px solid var(--color-border);
    background: var(--color-shade);
}
.signin input[type="email"],
.signin input[type="text"] { width: 100%; }

/* ---- listings ---------------------------------------------------- */

.work-grid, .recent-works ul, .recent-updates ul, .thumbnails ul {
    display: grid;
    gap: var(--unit);
    margin: 0;
    padding: 0;
}

.work-grid { grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); }
.recent-works ul { grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); }
.thumbnails ul { grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr)); align-items: end; }

.work-grid a, .recent-works a { text-decoration: none; display: block; }
.work-grid a:hover span, .recent-works a:hover span { text-decoration: underline; }

[state] { color: var(--color-accent); font-style: normal; }

/*
 * An update's logo — a festival or gallery mark, at the size a mark is read
 * at. `.thumbnails ul` uses `minmax(6rem, 1fr)` for the same kind of image,
 * so 6rem is this design's answer to "a logo, small"; the height cap is what
 * keeps a tall or square mark from claiming three times the room a wide
 * wordmark does.
 *
 * object-fit: contain, because these arrive at every ratio and cropping a
 * logo is worse than letterboxing one.
 */
.update-logo { margin-bottom: calc(var(--unit) * 0.5); }

.update-logo img {
    width: auto;
    max-width: 6rem;
    max-height: 3rem;
    object-fit: contain;
    object-position: left center;
}

/* ---- heading and quote ------------------------------------------- */

/*
 * ALL CAPS is applied here, never to the stored text — the artist's own
 * capitalisation is what lives in the file, so this presentation can change
 * without rewriting a single page.
 */
.heading h2 {
    text-transform: uppercase;
    margin: 0;
}

/*
 * THE ONE PLACE TYPE SIZE CARRIES MEANING. §8 sets a single font size for
 * everything and this deliberately breaks it: a pulled quote that is not large
 * is not a pulled quote.
 *
 * clamp() ties the size to the viewport with a hard ceiling, and the wrapping
 * rules below are what make "as large as can safely be displayed" safe — a long
 * word breaks rather than pushing the page sideways.
 */
.quote blockquote {
    margin: 0;
    font-size: clamp(2rem, 7vw, 5.5rem);
    line-height: 1.02;
    text-transform: none;
    white-space: pre-line;      /* the artist's own line breaks, no markup */
    overflow-wrap: break-word;  /* a long word wraps instead of overflowing */
    hyphens: auto;
}

/*
 * Attribution, not more quote: ordinary size, em-dashed, aligned right.
 * Ordinary text colour too — --color-muted is reserved for the footer, which is
 * housekeeping rather than the artist's work, and for unlisted markers, which
 * no visitor ever sees.
 */
.quote cite {
    display: block;
    margin-top: var(--unit);
    font-size: 1rem;
    font-style: normal;
    text-align: right;
    color: var(--color-ink);
}

.quote cite::before { content: "— "; }

/* ---- media ------------------------------------------------------- */

figure { margin: 0; }

.embed { position: relative; aspect-ratio: 16 / 9; }
.embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.shop-link .title { margin: 0; }

/*
 * Two adjacent boxes with a small gap, the same flat 1px treatment the drawer
 * toggle gets. The price is the artist's own text and takes ordinary colour;
 * only the button reads as actionable, in the accent.
 */
.shop-link .offer {
    display: flex;
    gap: calc(var(--unit) * 0.5);
    align-items: stretch;
    margin: 0;
}

.shop-link .price,
.shop-link .buy {
    display: inline-block;
    border: 1px solid var(--color-border);
    padding: calc(var(--unit) * 0.25) calc(var(--unit) * 0.5);
}

.shop-link .price { color: var(--color-ink); }

.shop-link .buy {
    color: var(--color-accent);
    text-decoration: none;
}

.shop-link .buy:hover { border-color: var(--color-accent); }

/* ---- footer ------------------------------------------------------ */

footer {
    padding: var(--unit);
    color: var(--color-muted);
}

/* One row: legal pages, then the badge. */
footer {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--unit);
}

footer ul { display: flex; gap: var(--unit); margin: 0; padding: 0; }
footer .badge { margin: 0; }

/*
 * INK BEHIND, PAPER IN FRONT — the artist's own two colours, swapped.
 *
 * The word carries the meaning here, not a colour: warning red is a fixed
 * pair used only where it supplies its own ground (§9), and this text sits
 * directly on the paper, which spans nearly the whole luminance range, so no
 * fixed colour would stay legible across every site.
 *
 * Inverting solves that without introducing a third colour. It also fixes
 * what was actually wrong: as plain ink at the same size and weight as the
 * label above it, a validation message read as more form, and the eye
 * skipped it. Contrast is guaranteed for free, because ink and paper are the
 * one pair every Poco site is built to keep legible against each other.
 *
 * inline-block with padding, not a full-width band: the message is about one
 * field and should be the width of its own sentence.
 *
 * Only ever a FORM message — join, login. The error page's own prose is
 * `.detail`, below, and stays plain: a paragraph of inverted text would be a
 * banner rather than a note.
 */
.error {
    display: inline-block;
    background: var(--color-ink);
    color: var(--color-base);
    padding: 0.1em 0.4em;
}

.detail { white-space: pre-wrap; color: var(--color-muted); }


/* ---- section listings ---------------------------------------------- */


nav.menu .see-all { margin: var(--unit) 0 0; }
