/* GENERATED from design/_mosaic.css by scripts/css-from-mockup.py.
   Do not edit. Change the mock-up and run `npm run design`.

   The design tokens are NOT here: they are served per theme from
   /theme/<key>.css, so this file is the rules only. */

/* ============================================================================
   Mosaic — tokens and every rule.

   This file is the specification. public/css/site.css is GENERATED from it by
   scripts/css-from-mockup.py, which strips the palette blocks and everything
   below the RIG marker. Do not hand-edit the generated file.

   The governing idea: this is a photo app, so the chrome recedes. Nothing in
   the interface may compete with the pictures for attention, which is why the
   accent appears on perhaps three things per page and the grounds are within a
   few percent of each other. A gallery whose furniture is louder than its
   images is a gallery you stop looking at.

   Every text token clears 7:1 on every ground token in both modes, in all three
   themes — AAA, not AA. python3 _contrast.py is the gate.
   ========================================================================== */



/* ============================================================================
   Base
   ========================================================================== */

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

/* `hidden` is display:none from the UA stylesheet, which ANY component rule
   setting display beats on specificity — so an element with the attribute
   stays on screen and the bug looks like broken JS. Settle it once here rather
   than per component, which is what was happening: three components had already
   grown their own [hidden] rule. */
[hidden] { display: none !important; }

html { color-scheme: light; }
html.dark { color-scheme: dark; }

body {
	margin: 0;
	padding-top: var(--nav-height);
	background: var(--bg);
	color: var(--text);
	font-family: var(--font-body);
	font-size: 1rem;
	line-height: 1.6;
	-webkit-font-smoothing: antialiased;
}

a { color: var(--accent); }
a:hover { text-decoration: underline; }

img, video { max-width: 100%; }

button { font: inherit; }

/* Every text-ish input, not a hand-listed few. The first version left out
   `password` and the sign-in form had one narrow box among three wide ones. */
textarea, select,
input:is([type="text"], [type="email"], [type="password"], [type="search"],
         [type="url"], [type="number"], [type="datetime-local"], [type="date"]) {
	width: 100%;
	font: inherit;
	font-size: .95rem;
	color: var(--text);
	background: var(--bg-card);
	border: 1px solid var(--border-firm);
	border-radius: var(--radius-sm);
	padding: .5rem .65rem;
}
textarea { resize: vertical; min-height: 4.5rem; }

:where(a, button, input, textarea, select, [tabindex]):focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
	border-radius: 3px;
}

.visually-hidden {
	position: absolute; width: 1px; height: 1px;
	margin: -1px; padding: 0; overflow: hidden;
	clip-path: inset(50%); white-space: nowrap;
}

/* Small type never goes below 13px, and never gets wide tracking. All-caps
   removes word shape; wide tracking stops letters grouping into words. Together
   they make the most important labels the least readable ones. */
.label {
	font-size: var(--text-sm);
	font-weight: var(--weight-small);
	letter-spacing: .04em;
	color: var(--text-muted);
}

/* ============================================================================
   Nav
   ========================================================================== */

.nav {
	position: fixed;
	inset: 0 0 auto 0;
	z-index: 100;
	height: var(--nav-height);
	display: flex;
	align-items: center;
	gap: .35rem;
	padding: 0 .5rem;
	background: var(--bg-nav);
	border-bottom: 1px solid var(--border);
}

.nav-brand {
	display: inline-flex;
	align-items: center;
	gap: .5rem;
	color: var(--text);
	text-decoration: none;
	font-weight: 700;
	letter-spacing: -.01em;
}
.nav-brand:hover { text-decoration: none; }
.nav-mark {
	/* The mark is 181x165, not square — a square box squashes it. Height is
	   what is fixed; width follows. */
	height: 24px;
	width: auto;
	flex: none;
	display: block;
}

.nav-links { display: flex; align-items: center; gap: .25rem; }

.nav-link {
	display: inline-flex;
	align-items: center;
	gap: .4rem;
	padding: .4rem .6rem;
	border-radius: var(--radius-sm);
	color: var(--text-muted);
	text-decoration: none;
	font-size: var(--text-sm);
	font-weight: var(--weight-small);
}
.nav-link:hover { background: var(--bg-sunken); color: var(--text); text-decoration: none; }
.nav-link[aria-current="page"] { color: var(--text); background: var(--bg-sunken); }
.nav-link .icon { flex: none; }
/* Below 48rem the labels go and the icons carry the links. They are not
   decoration: without them these become blank, clickable gaps — which is worse
   than no navigation at all, because it looks like nothing is there. */
.nav-link-label { display: none; }

/* Below the label breakpoint the wordmark goes too, leaving the mark on its own.
   The mark IS the brand and it is already a link home; the word beside it was
   costing sixty pixels on the narrowest screens to repeat what the tab title
   and the mark both already say.

   The links keep their full padding — the room is there now, and a tap target
   is not the thing to trim on a phone — but they still sit a little clear of
   the mark, because the link for the page you are on is filled and would
   otherwise read as attached to it. */
@media (width < 48rem) {
	.nav-brand > span { display: none; }
	.nav-links { margin-left: .4rem; }
}
@media (width >= 48rem) {
	.nav-link-label { display: inline; }
	.nav { gap: .75rem; padding: 0 .75rem; }
}

.nav-spacer { flex: 1; }

.nav-search { display: none; }
@media (width >= 48rem) {
	.nav-search {
		display: flex;
		align-items: center;
		gap: .45rem;
		background: var(--bg-sunken);
		border: 1px solid transparent;
		border-radius: var(--radius-sm);
		padding: .35rem .6rem;
		min-width: 15rem;
	}
	.nav-search:focus-within { border-color: var(--border-firm); }
	.nav-search input {
		/* The wrapper supplies the padding and the ground; the input contributes
		   nothing but the text. Resetting padding matters — left at the base
		   value it made the search 50px tall in a 56px nav. */
		border: 0; background: none; color: var(--text); padding: 0;
		font-size: var(--text-sm); width: 100%; outline: none;
	}
	.nav-search input::placeholder { color: var(--text-muted); }
}
.icon-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 34px; height: 34px;
	border: 0;
	border-radius: var(--radius-sm);
	background: none;
	color: var(--text-muted);
	cursor: pointer;
}
.icon-btn:hover { background: var(--bg-sunken); color: var(--text); }

/* Laying a tile is the one thing the nav exists to get you to, so it is the one
   control that is filled rather than quiet. */
/* A dot rather than a number: the count matters once you are on the page, and a
   badge that says 47 mostly teaches people to ignore it. */
.icon-btn.has-dot { position: relative; }
.icon-btn.has-dot::after {
	content: '';
	position: absolute;
	top: 5px; right: 5px;
	width: 8px; height: 8px;
	border-radius: 50%;
	background: var(--accent);
	border: 2px solid var(--bg-nav);
}

.icon-btn.is-primary { background: var(--accent); color: var(--on-accent); }
.icon-btn.is-primary:hover { background: var(--accent); color: var(--on-accent); filter: brightness(1.08); }

/* Below the breakpoint the search BOX goes but searching must not. A magnifier
   that opens the search page costs one icon's width, which is why the nav's
   gaps tighten on small screens — losing the feature entirely to save 34px was
   the wrong trade. Above the breakpoint the box is there, so the magnifier is
   redundant and goes.

   AFTER `.icon-btn`, and that is the whole point. The element carries both
   classes, both rules set `display`, and neither selector is more specific than
   the other — so whichever is written last wins. These rules used to sit above
   `.icon-btn`, which quietly meant the magnifier showed at every width, beside
   a search box that already had one in it. The same source-order trap put a
   second box around the search field in phase 1. */
.nav-search-btn { display: inline-flex; }
@media (width >= 48rem) { .nav-search-btn { display: none; } }

.nav-avatar {
	width: 30px; height: 30px;
	border-radius: 50%;
	object-fit: cover;
	border: 1px solid var(--border-firm);
}

/* ============================================================================
   Page furniture
   ========================================================================== */

.page-head {
	display: flex;
	align-items: flex-end;
	justify-content: space-between;
	gap: 1rem;
	padding: 1.25rem var(--grid-gap) .75rem;
	max-width: 100%;
}
.page-title { margin: 0; font-size: 1.35rem; font-weight: 700; letter-spacing: -.015em; }
.page-sub { margin: .15rem 0 0; font-size: var(--text-sm); color: var(--text-muted); }

/* The grid/stacked switch. One preference, applied to every tile listing —
   flip it here and the Mosaic, Explore, tags and search all follow. */
/* ── Where the view switch lives ───────────────────────────────────────────
   Inline with the page heading, and a small floating box on the right once the
   heading has scrolled away — so it stays reachable four hundred tiles down
   without going back to the top for it.

   It was briefly a full-width sticky bar, which cost a strip of screen the
   whole way down a listing to hold one control forty pixels wide. The control
   floats; the bar it came in does not need to.

   `position: fixed` rather than `sticky`, because sticky is bounded by its
   containing block and this one's containing block is the page head — it would
   unstick the moment the heading left. The slot keeps the space in the layout
   so the heading does not reflow underneath it when it detaches. */
.view-slot { flex: none; }
.view-slot.is-floating > .view-toggle {
	position: fixed;
	top: calc(var(--nav-height) + .5rem);
	right: var(--grid-gap);
	z-index: 40;
	/* Against tiles rather than the page, now, so it needs an edge of its own. */
	border: 1px solid var(--border-firm);
	box-shadow: var(--shadow);
}

.view-toggle {
	display: inline-flex;
	background: var(--bg-sunken);
	border-radius: var(--radius-sm);
	padding: 2px;
	flex: none;
}
/* Icons, not words. The control floats over somebody's pictures once the
   heading has gone, and "Grid | Stacked" is 118px of chrome to say what two
   16px shapes say. The words live on as `aria-label` and `title`.

   Solid shapes rather than the nav's outlined ones: at this size, on an accent
   fill, an outline is mostly gaps. */
.view-toggle button {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	border: 0;
	background: none;
	color: var(--text-muted);
	padding: .35rem .5rem;
	border-radius: 4px;
	cursor: pointer;
}
.view-toggle button svg { width: 16px; height: 16px; display: block; }
.view-toggle button[aria-pressed="true"] {
	background: var(--bg-card);
	color: var(--text);
	box-shadow: 0 1px 2px rgb(0 0 0 / .12);
}

/* ============================================================================
   The grid — crafty's cover grid, ported. The column counts are the SHIPPED
   ones (2 / 2 / 3 / 4 / 5), which is what has actually been tested on real
   devices, not the 1 / 2 / 3 / 4 in the old instructions.md.
   ========================================================================== */

.tile-grid {
	display: grid;
	gap: var(--grid-gap);
	padding: var(--grid-gap);
	grid-template-columns: repeat(2, 1fr);
}
@media (width >= 48rem) { .tile-grid { grid-template-columns: repeat(3, 1fr); } }
@media (width >= 64rem) { .tile-grid { grid-template-columns: repeat(4, 1fr); } }
@media (width >= 80rem) { .tile-grid { grid-template-columns: repeat(5, 1fr); } }

.tile-cell {
	display: block;
	position: relative;
	aspect-ratio: 1 / 1;
	overflow: hidden;
	background: var(--bg-sunken);
	text-decoration: none;
}
.tile-cell img,
.tile-cell .tile-cell-gradient {
	width: 100%; height: 100%;
	object-fit: cover;
	display: block;
	transition: transform .3s ease;
}
.tile-cell:hover img { transform: scale(1.03); }

/* A video with no cover supplied gets a gradient derived from the tile id, so
   the same tile always looks the same, plus a play badge. People do their own
   thumbnails; this is what happens when they don't. */
.tile-cell-gradient { background: linear-gradient(135deg, var(--g1), var(--g2)); }

.tile-badges {
	position: absolute;
	top: .4rem; right: .4rem;
	display: flex; gap: .25rem;
	pointer-events: none;
}
.tile-badge {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 22px; height: 22px;
	border-radius: 4px;
	background: rgb(0 0 0 / .55);
	color: #fff;
	backdrop-filter: blur(2px);
}
.tile-badge svg { width: 13px; height: 13px; }

.play-badge {
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	pointer-events: none;
	color: #fff;
	filter: drop-shadow(0 1px 4px rgb(0 0 0 / .6));
}
.play-badge svg { width: 38px; height: 38px; }

/* Status flags on your own tiles. Never shown to anyone else. */
.tile-flag {
	position: absolute;
	left: .4rem; bottom: .4rem;
	font-size: 11px;
	font-weight: 700;
	letter-spacing: .03em;
	text-transform: uppercase;
	padding: .15rem .35rem;
	border-radius: 3px;
	background: rgb(0 0 0 / .62);
	color: #fff;
}

/* ============================================================================
   The stacked view — crafty's feed, ported. One column, covers uncropped at
   full column width, two lines of caption and one row of tags before the
   expanders.
   ========================================================================== */

.tile-stack {
	max-width: var(--stack-width);
	margin: 0 auto;
	padding: .5rem 1rem 2rem;
	display: flex;
	flex-direction: column;
	gap: 2.5rem;
}

.stack-item { }

.stack-byline {
	display: flex;
	align-items: center;
	gap: .5rem;
	padding-bottom: .5rem;
}
.stack-byline img { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; }
.stack-byline a { color: var(--text); font-weight: 600; font-size: .9rem; text-decoration: none; }
.stack-byline a:hover { text-decoration: underline; }

.stack-cover {
	display: block;
	width: 100%;
	overflow: hidden;
	border-radius: var(--radius-sm);
	background: var(--bg-sunken);
	position: relative;
}
.stack-cover img { width: 100%; height: auto; display: block; transition: transform .3s ease; }
.stack-cover:hover img { transform: scale(1.02); }

.stack-meta {
	padding: .6rem .15rem 0;
	display: flex;
	flex-direction: column;
	gap: .4rem;
}
/* The date on the left, the counts on the right. `baseline` rather than
   `center` so the numbers sit on the same line as the date rather than being
   optically centred against a taller icon. */
.stack-head { display: flex; align-items: baseline; justify-content: space-between; gap: .75rem; }
.stack-date { font-size: var(--text-sm); color: var(--text-muted); }
.stack-counts { display: inline-flex; align-items: center; gap: .1rem; flex: none; }
/* They are not controls here, so they must not offer the hover a control does. */
.stack-counts .action-btn { padding: .1rem .35rem; cursor: default; }
.stack-counts .action-btn:hover { background: none; color: var(--text-muted); }

.clamp-wrap { display: flex; align-items: flex-start; gap: .4rem; }
.clamp {
	overflow: hidden;
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 2;
	line-clamp: 2;
	font-size: .95rem;
}
.clamp.expanded { display: block; -webkit-line-clamp: unset; line-clamp: unset; }
.clamp p { margin: 0; }
.clamp p + p { margin-top: .5em; }

.tags-row { display: flex; flex-wrap: wrap; gap: .3rem; }
.tags-row.clamped { max-height: 2em; overflow: hidden; }
.tags-row.expanded { max-height: none; }

/* Crafty's expander, wording and all. It had been a "…" that became a "×",
   which is two glyphs asking somebody to work out that one is the opposite of
   the other — and an ellipsis beside a clamped caption reads as part of the
   text rather than as a control. The words are in the markup in sentence case
   and uppercased here, so a screen reader is not handed SHOW MORE to spell. */
.expand-btn {
	flex: none;
	font-size: .65rem;
	font-weight: 600;
	letter-spacing: .06em;
	text-transform: uppercase;
	color: var(--accent);
	cursor: pointer;
	padding: .2rem;
	background: none;
	border: 0;
	/* Stacked: SHOW over MORE, centred on each other. `min-content` wraps the
	   label at its own space without a <br> in the markup, which matters because
	   the script swaps the text by `textContent` — a line break baked into the
	   markup would survive exactly one press.

	   It only ever appears beside a caption long enough to be clamped to two
	   lines, so there are two lines of room for it, and taking one word's width
	   instead of two gives that caption back about forty pixels. */
	width: min-content;
	text-align: center;
	line-height: 1.2;
}
.expand-btn:hover { text-decoration: underline; }

/* ============================================================================
   Tags
   ========================================================================== */

.pill {
	display: inline-block;
	padding: .2rem .6rem;
	background: var(--pill-bg);
	color: var(--pill-text);
	border-radius: var(--radius-pill);
	font-size: var(--text-sm);
	font-weight: var(--weight-small);
	text-decoration: none;
	line-height: 1.5;
}
.pill:hover { background: var(--border-firm); text-decoration: none; }

/* ============================================================================
   The mosaic header — the one thing the gallery has that crafty never did
   ========================================================================== */

.mosaic-head {
	display: flex;
	align-items: flex-start;
	gap: 1rem;
	padding: 1.5rem 1rem 1rem;
	max-width: 52rem;
	margin: 0 auto;
	/* For the view switch, which hangs in the corner rather than sitting in the
	   row — see below. */
	position: relative;
}
@media (width >= 48rem) { .mosaic-head { gap: 1.5rem; padding-top: 2.25rem; } }

.mosaic-avatar {
	width: 72px; height: 72px;
	border-radius: 50%;
	object-fit: cover;
	flex: none;
	border: 1px solid var(--border);
	background: var(--bg-sunken);
}
@media (width >= 48rem) { .mosaic-avatar { width: 104px; height: 104px; } }

.mosaic-ident { min-width: 0; flex: 1; }
.mosaic-name { margin: 0; font-size: 1.25rem; font-weight: 700; letter-spacing: -.015em; }
.mosaic-handle { margin: 0; font-size: var(--text-sm); color: var(--text-muted); font-weight: var(--weight-small); }

/* ── The view switch in a mosaic header ────────────────────────────────────
   FLOATED, not a third item in the flex row and not absolutely positioned.

   In the flex row it took its width out of the one column holding the name, the
   handle, the bio, the counts and the buttons — on a phone that column is 255px
   and the switch wanted 118 of them, so Edit profile and Lay a tile wrapped onto
   separate lines and hid down the left.

   Absolute fixed that, but reserving room for it meant padding the name, and
   padding applies to every line — so a display name of any length wrapped inside
   a column 119px wide for all of them.

   A float is the thing that actually describes this: the name wraps around the
   corner it occupies and has the full column back on the line below. When the
   switch detaches on scroll the float keeps its measured size, so nothing
   reflows underneath it on the way past. */
.mosaic-ident > .view-slot { float: right; margin: 0 0 .35rem 1rem; }
.mosaic-bio { margin: .5rem 0 0; font-size: .95rem; }

.mosaic-counts {
	display: flex;
	gap: 1.25rem;
	margin: .75rem 0 0;
	padding: 0;
	list-style: none;
	font-size: var(--text-sm);
	color: var(--text-muted);
}
.mosaic-counts b { color: var(--text); font-weight: 700; }
/* The numbers are public to anyone who can see the mosaic; the LISTS are the
   owner's alone, so only the owner's own counts are links. */
.mosaic-counts a { color: inherit; }

/* `align-items: center` matters more than it looks: a flex item stretches to the
   row's height by default, so the <details> holding the menu was 37px tall next
   to a 26px summary — and the panel, positioned at 100% of the details, hung
   11px lower than intended and lost its connection to the control. */
.mosaic-actions { display: flex; align-items: center; gap: .5rem; margin-top: .85rem; flex-wrap: wrap; }

.btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: .4rem;
	padding: .45rem .9rem;
	border-radius: var(--radius-sm);
	border: 1px solid var(--border-firm);
	background: var(--bg-card);
	color: var(--text);
	font-size: var(--text-sm);
	font-weight: var(--weight-small);
	cursor: pointer;
	text-decoration: none;
}
.btn:hover { background: var(--bg-sunken); text-decoration: none; }
.btn-primary {
	background: var(--accent);
	border-color: var(--accent);
	color: var(--on-accent);
}
.btn-primary:hover { filter: brightness(1.08); background: var(--accent); }

/* ── A button that cannot be pressed ───────────────────────────────────────
   There was no style for this at all, so the Install app button — which the
   server deliberately renders disabled, and which only a browser offering
   one-tap install ever enables — looked exactly like a button you could press,
   pointer cursor and all. The same went for the push toggle and the time zone
   detect button.

   Muted text on the sunken ground rather than an `opacity`, because opacity
   silently drops whatever contrast the tokens were picked for and the 7:1 gate
   only measures tokens. A disabled control's label still has to be readable —
   it is the thing that explains why it is disabled. */
.btn:disabled,
.btn-primary:disabled {
	background: var(--bg-sunken);
	border-color: var(--border);
	color: var(--text-muted);
	cursor: not-allowed;
}
.btn:disabled:hover,
.btn-primary:disabled:hover { background: var(--bg-sunken); filter: none; }

/* ── The locked states ─────────────────────────────────────────────────────
   A private mosaic and a suspended one both stop at the header. They look
   different on purpose: private is a door you may knock on, suspended is a
   notice. Neither renders a single tile. */

.locked {
	max-width: 52rem;
	margin: 0 auto;
	padding: 2.5rem 1rem 4rem;
	text-align: center;
	color: var(--text-muted);
}
.locked-mark {
	width: 40px; height: 40px;
	margin: 0 auto .75rem;
	color: var(--text-muted);
	opacity: .8;
}
.locked h2 { margin: 0 0 .35rem; font-size: 1.05rem; color: var(--text); font-weight: 700; }
.locked p { margin: 0 auto; max-width: 30rem; font-size: .95rem; }
.locked-rule { border: 0; border-top: 1px solid var(--border); margin: 0 auto 2rem; max-width: 52rem; }

/* ============================================================================
   The tile view — ported from crafty unchanged. The layout, the gallery
   navigation, the ALT button and its modal, the date/caption/tags column and
   the two-thirds/one-third split at lg+ are all settled and tested; the only
   additions are the byline and the comments, which sit around it, not in it.
   ========================================================================== */

.single-tile { padding: 1rem; max-width: 96rem; margin: 0 auto; }

.tile-header {
	position: relative;
	display: flex;
	align-items: center;
	justify-content: center;
	margin-bottom: .75rem;
}
.back-btn {
	display: inline-flex;
	align-items: center;
	gap: .3rem;
	position: absolute;
	left: 0;
	font-size: var(--text-sm);
	font-weight: var(--weight-small);
	color: var(--text-muted);
	background: none;
	border: 0;
	padding: 0;
	cursor: pointer;
}
.back-btn:hover { color: var(--text); }
.tile-header > .menu { position: absolute; right: 0; }
.tile-title {
	font-size: clamp(1.4rem, 4vw, 2rem);
	font-weight: 600;
	text-align: center;
	padding: 0 6rem;
	margin: 0;
}
/* Crafty's 6rem each side keeps the title clear of the Back button, and at
   375px that leaves about 121px of text — three lines for a three-word title,
   and the menu added to the right made it tighter still. Below the breakpoint
   the clearance becomes asymmetric and actual: the width each control really
   needs, nothing more. The title stays centred between them, which is the
   centre that matters. */
@media (width < 48rem) {
	.tile-title { padding: 0 2.5rem 0 4.5rem; }
}

.tile-layout { display: flex; flex-direction: column; gap: 1.5rem; }
@media (width >= 64rem) {
	.tile-layout { flex-direction: row; align-items: flex-start; gap: 2rem; }
	.tile-gallery { flex: 2; min-width: 0; }
	.tile-details { flex: 1; min-width: 0; position: sticky; top: calc(var(--nav-height) + 1rem); }
}

.gallery-viewer {
	position: relative;
	background: var(--bg-sunken);
	border-radius: var(--radius-sm);
	overflow: hidden;
}
.gallery-slide { display: none; position: relative; }
.gallery-slide.active { display: block; }
.gallery-media {
	display: block;
	width: 100%;
	height: auto;
	max-height: 80vh;
	object-fit: contain;
	background: #000;
}

.alt-btn {
	position: absolute;
	bottom: .5rem; right: .5rem;
	font-size: .6rem;
	font-weight: 700;
	letter-spacing: .08em;
	color: #fff;
	background: rgb(0 0 0 / .5);
	border: 0;
	border-radius: 3px;
	padding: .2em .45em;
	cursor: pointer;
	z-index: 2;
}
.alt-btn:hover { background: rgb(0 0 0 / .75); }

.alt-modal {
	position: fixed;
	inset: 0;
	background: var(--scrim);
	display: flex;
	align-items: center;
	justify-content: center;
	z-index: 300;
	padding: 1.5rem;
}
.alt-modal-box {
	position: relative;
	background: rgb(0 0 0 / .92);
	color: #fff;
	border-radius: var(--radius-sm);
	padding: 2.25rem 1.5rem 1.5rem;
	max-width: 480px;
	width: 100%;
}
.alt-modal-close {
	position: absolute;
	top: .5rem; right: .75rem;
	font-size: .6rem;
	font-weight: 700;
	letter-spacing: .08em;
	color: #fff;
	background: none;
	border: 0;
	cursor: pointer;
	opacity: .8;
}
.alt-modal-close:hover { opacity: 1; }
.alt-modal-text { font-size: .95rem; line-height: 1.6; margin: 0; }

.gallery-nav {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 1rem;
	padding: .6rem 0;
}
.gallery-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 34px; height: 34px;
	border: 1px solid var(--border-firm);
	border-radius: var(--radius-sm);
	background: var(--bg-card);
	color: var(--text);
	cursor: pointer;
}
.gallery-btn:hover { background: var(--bg-sunken); }
.gallery-counter { font-size: var(--text-sm); color: var(--text-muted); font-variant-numeric: tabular-nums; }

.gallery-strip {
	display: flex;
	gap: 4px;
	padding: .25rem 0 0;
	overflow-x: auto;
}
.gallery-strip button {
	flex: none;
	width: 52px; height: 52px;
	padding: 0;
	border: 2px solid transparent;
	border-radius: 4px;
	overflow: hidden;
	cursor: pointer;
	background: var(--bg-sunken);
}
.gallery-strip button[aria-current="true"] { border-color: var(--accent); }
.gallery-strip img { width: 100%; height: 100%; object-fit: cover; display: block; }

.tile-byline {
	display: flex;
	align-items: center;
	gap: .55rem;
	padding-bottom: .85rem;
	margin-bottom: .85rem;
	border-bottom: 1px solid var(--border);
}
.tile-byline img { width: 34px; height: 34px; border-radius: 50%; object-fit: cover; }
.tile-byline .who { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.tile-byline .who a { color: var(--text); font-weight: 600; text-decoration: none; }
.tile-byline .who a:hover { text-decoration: underline; }
.tile-byline .who span { font-size: var(--text-sm); color: var(--text-muted); }

.tile-date { display: block; font-size: var(--text-sm); color: var(--text-muted); margin-bottom: .5rem; }
.tile-caption { font-size: 1rem; }
.tile-caption p { margin: 0 0 .75em; }
.tile-caption :last-child { margin-bottom: 0; }
.tile-tags { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: 1rem; }

.tile-actions {
	display: flex;
	align-items: center;
	gap: .35rem;
	margin-top: 1rem;
	padding-top: .85rem;
	border-top: 1px solid var(--border);
}
.action-btn {
	display: inline-flex;
	align-items: center;
	gap: .35rem;
	border: 0;
	background: none;
	color: var(--text-muted);
	padding: .35rem .5rem;
	border-radius: var(--radius-sm);
	cursor: pointer;
	font-size: var(--text-sm);
	font-weight: var(--weight-small);
}
.action-btn:hover { background: var(--bg-sunken); color: var(--text); }
.action-btn[aria-pressed="true"] { color: var(--accent); }
.action-btn svg { width: 17px; height: 17px; }

/* ============================================================================
   The overflow menu

   Reporting has to be reachable from everything that can be reported — a tile,
   a comment, a person — without putting a Report button on screen beside things
   nobody is reporting. One menu, at the far right of whatever it acts on.
   ========================================================================== */

/* A native <details>, so the menu — and everything in it, including Block —
   works with no JavaScript at all. A safety control that needs a script is not
   a safety control. */
.menu { position: relative; flex: none; }
.menu > summary {
	/* `flex`, NOT `inline-flex`. An inline-level summary sits on a line box
	   inside the block <details>, and the line box keeps the strut's descender
	   space below it — so the <details> came out 37px tall around a 30px
	   trigger, with the trigger flush to the top. Centred as a flex item in the
	   nav, that put the avatar four pixels above every icon beside it. The same
	   phantom sat under every other menu too; it was only invisible because
	   nothing was lined up against them. */
	display: flex;
	align-items: center;
	justify-content: center;
	width: 30px; height: 26px;
	border-radius: var(--radius-sm);
	color: var(--text-muted);
	cursor: pointer;
	line-height: 1;
	font-size: 15px;
	letter-spacing: .08em;
	list-style: none;          /* Firefox */
	user-select: none;
}
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary::marker { content: ''; }
.menu > summary:hover { background: var(--bg-sunken); color: var(--text); }
/* While it is open the trigger is clearly lit, so the panel has something to
   belong to. The panel is wider than the 30px control it hangs from and so
   reaches back across whatever sits to its left — without this, it reads as
   belonging to that neighbour instead. */
.menu[open] > summary { background: var(--accent); color: var(--on-accent); }
.menu > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The account menu in the nav. Same disclosure, but the trigger is a picture
   rather than a 30px glyph slot, so the summary becomes the avatar: same 30px
   box, so the caret still lands under it, and no padding to push the image
   off-centre. Lighting the trigger with an accent BACKGROUND does nothing when
   an image covers it, so the open state is a ring instead. */
.menu-avatar > summary { width: 30px; height: 30px; padding: 0; border-radius: 50%; }
.menu-avatar > summary:hover { background: none; }
.menu-avatar[open] > summary {
	background: none;
	box-shadow: 0 0 0 2px var(--accent);
}
.menu-avatar > summary > .nav-avatar { display: block; }
/* Clear of the bar rather than starting 4px inside it. The avatar is centred in
   the nav, so the gap from the bottom of the trigger to the bottom of the bar is
   half the leftover height — worked out from the token so it stays right if the
   bar changes, rather than a magic 19px that quietly stops matching. */
.menu-avatar > .menu-list { top: calc(100% + (var(--nav-height) - 30px) / 2 + 6px); }

.menu-list {
	position: absolute;
	z-index: 60;
	/* Close enough to read as attached. A larger gap plus a panel wider than its
	   trigger is what made this look like it belonged to the button beside it. */
	top: calc(100% + 6px);
	right: 0;
	min-width: 11rem;
	background: var(--bg-card);
	border: 1px solid var(--border-firm);
	border-radius: var(--radius-sm);
	box-shadow: var(--shadow);
	padding: .2rem;
}
.menu-list :is(button, a) {
	display: block;
	width: 100%;
	text-align: left;
	border: 0;
	background: none;
	color: var(--text);
	padding: .35rem .5rem;
	border-radius: 4px;
	cursor: pointer;
	font-size: var(--text-sm);
	white-space: nowrap;
	text-decoration: none;
	font-family: inherit;
}
.menu-list :is(button, a):hover { background: var(--bg-sunken); text-decoration: none; }
.menu-list hr { border: 0; border-top: 1px solid var(--border); margin: .2rem .1rem; }

/* Who this menu is about. Not a menuitem — there is nothing here to choose, so
   it is outside the list's reading order and takes no hover. The handle under
   the name is the part that actually answers the question, which is why it is
   there at all: the avatar alone does not say which account is signed in. */
.menu-list .menu-head {
	margin: 0;
	padding: .35rem .5rem .4rem;
	font-size: var(--text-sm);
	font-weight: 700;
	color: var(--text);
	white-space: nowrap;
}
.menu-list .menu-head > span {
	display: block;
	font-weight: var(--weight-small);
	color: var(--text-muted);
}

/* A caret under the trigger, not under the middle of the panel: it points at
   the control the menu came from, which is the whole question a wide panel
   raises. 15px is the centre of the 30px summary. */
.menu-list::before,
.menu-list::after {
	content: '';
	position: absolute;
	right: 9px;
	width: 0; height: 0;
	border-left: 7px solid transparent;
	border-right: 7px solid transparent;
}
.menu-list::before {
	top: -7px;
	border-bottom: 7px solid var(--border-firm);
}
.menu-list::after {
	top: -6px;
	border-bottom: 7px solid var(--bg-card);
}
/* Reporting and blocking are not destructive to anyone but read as heavier than
   copying a link, so they are set apart rather than coloured red — red here
   would suggest the reporter is doing something dangerous. */
.menu-list .set-apart { color: var(--text-muted); }

/* ============================================================================
   Comments
   ========================================================================== */

.comments { margin-top: 1.25rem; }
.comments-head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: .75rem;
	margin-bottom: .75rem;
}
.comments-head h2 { margin: 0; font-size: .95rem; font-weight: 700; }

.comment-form { display: flex; gap: .5rem; margin-bottom: 1.25rem; }
.comment-form img { width: 30px; height: 30px; border-radius: 50%; flex: none; object-fit: cover; }
.comment-form .field { flex: 1; }

.comment-list { display: flex; flex-direction: column; gap: 1rem; margin: 0; padding: 0; list-style: none; }
.comment { display: flex; gap: .55rem; }
.comment img.avatar { width: 30px; height: 30px; border-radius: 50%; flex: none; object-fit: cover; }
.comment-body { min-width: 0; flex: 1; }
.comment-who { font-size: .9rem; display: flex; align-items: center; gap: .1rem; }
.comment-who .menu { margin-left: auto; }
.comment-who a { color: var(--text); font-weight: 600; text-decoration: none; }
.comment-who a:hover { text-decoration: underline; }
/* A paused account keeps its comments, with the name as plain text rather than
   a link — deleting them would tear holes in other people's conversations. */
.comment-who .plain { color: var(--text); font-weight: 600; }
.comment-when { font-size: var(--text-sm); color: var(--text-muted); margin-left: .35rem; }
.comment-text { margin: .15rem 0 0; font-size: .95rem; }
.comment-tools { display: flex; gap: .15rem; margin-top: .2rem; }
.comment-tools button {
	border: 0; background: none; cursor: pointer;
	font-size: var(--text-sm); font-weight: var(--weight-small);
	color: var(--text-muted); padding: .15rem .35rem; border-radius: 4px;
}
.comment-tools button:hover { background: var(--bg-sunken); color: var(--text); }

.comment-replies {
	list-style: none;
	margin: .75rem 0 0;
	padding: 0 0 0 1.1rem;
	border-left: 2px solid var(--border);
	display: flex;
	flex-direction: column;
	gap: .85rem;
}

/* Pending, tombstoned and hidden all have to READ differently at a glance, or
   the four comment modes are invisible to the person using them. */
.comment-note {
	display: inline-block;
	font-size: var(--text-sm);
	font-weight: var(--weight-small);
	color: var(--text-muted);
	background: var(--bg-sunken);
	border-radius: 4px;
	padding: .05rem .35rem;
	margin-left: .35rem;
}
.comment-tomb { color: var(--text-muted); font-style: italic; font-size: .95rem; }

.comments-closed {
	font-size: var(--text-sm);
	color: var(--text-muted);
	background: var(--bg-sunken);
	border-radius: var(--radius-sm);
	padding: .6rem .75rem;
}

/* ============================================================================
   Laying a tile
   ========================================================================== */

.compose { max-width: 96rem; margin: 0 auto; padding: 1rem; }
.compose-layout { display: flex; flex-direction: column; gap: 1.75rem; }
@media (width >= 64rem) {
	.compose-layout { flex-direction: row; align-items: flex-start; gap: 2rem; }
	.compose-media { flex: 2; min-width: 0; }
	.compose-fields { flex: 1; min-width: 0; }
}

.dropzone {
	border: 2px dashed var(--border-firm);
	border-radius: var(--radius-md);
	background: var(--bg-sunken);
	padding: 2rem 1rem;
	text-align: center;
	color: var(--text-muted);
}
.dropzone strong { display: block; color: var(--text); font-size: 1rem; margin-bottom: .25rem; }
.dropzone p { margin: .35rem auto 0; max-width: 32rem; font-size: var(--text-sm); }

/* Said once, where someone uploading will read it. Mosaic keeps one 1600px copy
   and no original, which is the right trade for a storage cap but means this is
   a publishing tool and not a backup. Burying that would be a betrayal the
   first time someone deleted their only copy. */
.lossy-note {
	margin-top: .75rem;
	font-size: var(--text-sm);
	color: var(--text-muted);
	background: var(--bg-card);
	border: 1px solid var(--border);
	border-radius: var(--radius-sm);
	padding: .55rem .7rem;
	text-align: left;
}

.tray { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: .6rem; margin-top: 1rem; }
.tray-item {
	position: relative;
	aspect-ratio: 1 / 1;
	border-radius: var(--radius-sm);
	overflow: hidden;
	background: var(--bg-sunken);
	border: 1px solid var(--border);
}
.tray-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tray-item.is-feature { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }

.tray-tag {
	position: absolute; left: 0; bottom: 0; right: 0;
	display: flex; align-items: center; justify-content: space-between; gap: .25rem;
	padding: .2rem .3rem;
	background: rgb(0 0 0 / .62);
	font-size: 11px; font-weight: 700; letter-spacing: .02em; color: #fff;
}
.tray-tag .need-alt { color: #ffd4a8; }
.tray-tools { position: absolute; top: .25rem; right: .25rem; display: flex; gap: .2rem; }
.tray-tools button {
	width: 22px; height: 22px; padding: 0;
	display: grid; place-items: center;
	border: 0; border-radius: 4px;
	background: rgb(0 0 0 / .58); color: #fff; cursor: pointer;
	font-size: 12px; line-height: 1;
}
.tray-tools button:hover { background: rgb(0 0 0 / .8); }

.field-row { margin-bottom: 1.1rem; }
.field-row > label, .field-legend {
	display: block;
	font-size: var(--text-sm);
	font-weight: var(--weight-small);
	margin-bottom: .3rem;
}
.field-hint { margin: .3rem 0 0; font-size: var(--text-sm); color: var(--text-muted); }

/* One joined bar rather than separate buttons — the shape used in calc-tools.
   Segments of a single control read as "pick one of these"; detached buttons
   read as "here are some things you can press", which is the wrong sentence for
   a choice that always has exactly one answer. */
.seg {
	display: flex;
	gap: 0;
	background: var(--bg-card);
	border: 1px solid var(--border-firm);
	border-radius: var(--radius-sm);
	overflow: hidden;
}
.seg button {
	flex: 1;
	text-align: center;
	border: 0;
	border-right: 1px solid var(--border-firm);
	background: transparent;
	color: var(--text-muted);
	padding: .45rem .7rem;
	font-size: var(--text-sm);
	font-weight: var(--weight-small);
	cursor: pointer;
	user-select: none;
	transition: background .15s, color .15s;
}
.seg button:last-child { border-right: 0; }
.seg button:hover { background: var(--bg-sunken); color: var(--text); }
.seg button[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); }
.seg button[aria-pressed="true"]:hover { background: var(--accent); color: var(--on-accent); }
/* The outline would otherwise be clipped by the bar's overflow:hidden. */
.seg button:focus-visible { outline-offset: -3px; }

/* The radio form of the same bar, for a control sitting inside a larger form
   where a submit button would save that form instead of answering a question.
   Identical to look at on purpose: which one is in use is a matter of what
   clicking it should DO, never of what it should look like. The input is
   visually hidden rather than display:none, so it still takes focus and still
   answers to the arrow keys as a radio group. */
.seg label { flex: 1; display: flex; }
.seg label > span {
	flex: 1;
	text-align: center;
	border-right: 1px solid var(--border-firm);
	color: var(--text-muted);
	padding: .45rem .7rem;
	font-size: var(--text-sm);
	font-weight: var(--weight-small);
	cursor: pointer;
	user-select: none;
	transition: background .15s, color .15s;
}
.seg label:last-child > span { border-right: 0; }
.seg label > span:hover { background: var(--bg-sunken); color: var(--text); }
.seg input:checked + span { background: var(--accent); color: var(--on-accent); }
.seg input:checked + span:hover { background: var(--accent); color: var(--on-accent); }
.seg input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: -3px; }

/* ── Picking a theme ───────────────────────────────────────────────────────
   A name on its own tells you nothing about a palette, which is the whole
   thing being chosen. Each option carries the same two strips the admin theme
   cards use: light above, dark below, showing ground, accent and text. Not the
   three grounds — Mosaic's sit within a few percent of each other by design, so
   a strip of those would be three near-identical stripes.

   A radio in a form rather than a bar that posts on click, because there are as
   many of these as there are themes and a row of submit buttons that each save
   immediately is a row where a mis-click is a change. */
.themerow { display: grid; gap: .6rem; grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr)); }
.themepick { display: block; cursor: pointer; }
.themepick input { position: absolute; opacity: 0; width: 0; height: 0; }
.themepick-face {
	display: grid;
	grid-template-columns: 1fr 1fr;
	height: 46px;
	border: 1px solid var(--border-firm);
	border-radius: var(--radius-sm);
	overflow: hidden;
}
.swatchstrip { display: grid; grid-template-rows: 1fr 1fr 1fr; }
.swatchstrip i { display: block; }
/* A hairline between the modes, so a light palette does not appear to bleed
   into the dark one beside it. */
.themepick-face > .swatchstrip + .swatchstrip { border-left: 1px solid var(--border); }
.themepick-name {
	display: block;
	margin-top: .3rem;
	font-size: var(--text-sm);
	font-weight: var(--weight-small);
	color: var(--text-muted);
}
.themepick-name > span { display: block; font-size: 11px; }
.themepick input:checked + .themepick-face { box-shadow: 0 0 0 2px var(--accent); border-color: var(--accent); }
.themepick input:checked ~ .themepick-name { color: var(--text); font-weight: 700; }
.themepick input:focus-visible + .themepick-face { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The detect button and what it found, on one line until there is no room. */
.tz-detect { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin: 0 0 1rem; }
.tz-detect .field-hint { margin: 0; }

/* The manual install steps, for the browsers that will not offer one tap. */
.install-steps { margin: .5rem 0 0; padding-left: 1.1rem; }
.install-steps li { margin-bottom: .25rem; }

/* ── Tags, entered as pills ────────────────────────────────────────────────
   Typed text becomes a pill on Enter, comma or blur. A comma-separated string
   in a text box looks like one field but behaves like several: you cannot see
   where one tag ends, a stray space changes nothing but a stray comma makes an
   empty tag, and nothing tells you the text is being lowercased until later. */
.tagfield {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: .3rem;
	background: var(--bg-card);
	border: 1px solid var(--border-firm);
	border-radius: var(--radius-sm);
	padding: .35rem .4rem;
	cursor: text;
}
.tagfield:focus-within { border-color: var(--accent); }
.tagfield input {
	flex: 1;
	min-width: 7rem;
	border: 0;
	background: none;
	padding: .15rem .2rem;
	color: var(--text);
	font-size: var(--text-sm);
	outline: none;
}
.tagfield input::placeholder { color: var(--text-muted); }

.tagchip {
	display: inline-flex;
	align-items: center;
	gap: .3rem;
	background: var(--pill-bg);
	color: var(--pill-text);
	border-radius: var(--radius-pill);
	padding: .15rem .2rem .15rem .55rem;
	font-size: var(--text-sm);
	font-weight: var(--weight-small);
	line-height: 1.5;
}
.tagchip button {
	display: grid;
	place-items: center;
	width: 17px; height: 17px;
	border: 0;
	border-radius: 50%;
	background: none;
	color: inherit;
	cursor: pointer;
	font-size: 13px;
	line-height: 1;
	opacity: .65;
}
.tagchip button:hover { opacity: 1; background: var(--border-firm); }

.tagsuggest {
	position: relative;
}
.tagsuggest-list {
	position: absolute;
	z-index: 40;
	/* BELOW the field, not on top of it. `top: .25rem` put it a quarter-rem from
	   the top of the wrapper — and the wrapper holds the field too, so the
	   suggestions covered the pills you were adding them to. It went unnoticed
	   because seeing it requires typing a prefix that matches something. */
	top: calc(100% + .25rem); left: 0; right: 0;
	background: var(--bg-card);
	border: 1px solid var(--border-firm);
	border-radius: var(--radius-sm);
	box-shadow: var(--shadow);
	max-height: 12rem;
	overflow-y: auto;
	padding: .2rem;
}
.tagsuggest-list button {
	display: flex;
	justify-content: space-between;
	gap: .75rem;
	width: 100%;
	border: 0;
	background: none;
	color: var(--text);
	text-align: left;
	padding: .3rem .45rem;
	border-radius: 4px;
	cursor: pointer;
	font-size: var(--text-sm);
}
.tagsuggest-list button:hover,
.tagsuggest-list button[aria-selected="true"] { background: var(--bg-sunken); }
.tagsuggest-list .n { color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* ── The site's own images, in the admin ───────────────────────────────────
   A preview beside each, on a chequerboard: three of the four are usually
   transparent, and a transparent icon on a flat ground looks like whatever the
   ground is rather than like the file. */
.siteimg { display: flex; align-items: flex-start; gap: 1rem; }
.siteimg-preview {
	width: 64px; height: 64px;
	flex: none;
	object-fit: contain;
	padding: .35rem;
	border: 1px solid var(--border);
	border-radius: var(--radius-sm);
	background-color: var(--bg-sunken);
	background-image:
		linear-gradient(45deg, var(--border) 25%, transparent 25%, transparent 75%, var(--border) 75%),
		linear-gradient(45deg, var(--border) 25%, transparent 25%, transparent 75%, var(--border) 75%);
	background-size: 12px 12px;
	background-position: 0 0, 6px 6px;
}
.siteimg-controls { flex: 1; min-width: 0; }
.siteimg-controls form { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-top: .3rem; }
.siteimg-controls label.btn { cursor: pointer; }

/* ── Choosing a profile picture ────────────────────────────────────────────
   The preview is the real thing at the real shape: cropped square in the
   browser and shown in a circle, so what you approve is the file that gets
   stored rather than a rectangle the CSS will crop differently later. */
.avatar-row { display: flex; align-items: flex-start; gap: 1rem; margin-bottom: .5rem; }
.avatar-preview {
	width: 88px; height: 88px;
	border-radius: 50%;
	object-fit: cover;
	flex: none;
	border: 1px solid var(--border);
	background: var(--bg-sunken);
}
.avatar-controls { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.avatar-controls form { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
/* The file input itself is hidden; its <label> is the button. A bare file input
   cannot be styled and says "No file chosen" in a font nobody chose. */
.avatar-controls label.btn { cursor: pointer; }
[data-avatar-note].is-error { color: var(--accent); }

.meter { height: 6px; border-radius: 3px; background: var(--bg-sunken); overflow: hidden; margin: .35rem 0 .3rem; }
.meter > span { display: block; height: 100%; background: var(--accent); }

.compose-bar {
	display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
	margin-top: 1.25rem; padding-top: 1rem;
	border-top: 1px solid var(--border);
}
.compose-bar .spacer { flex: 1; }
/* Discard is not a peer of the three publish actions. Given the same border and
   ground it reads as a fourth thing you might mean to do — and when the row
   wraps on a narrow column it lands directly under "Lay tile", which is the
   worst place for it. Dropping it to a plain muted control settles both. */
.compose-bar .btn-quiet {
	border-color: transparent;
	background: none;
	color: var(--text-muted);
}
.compose-bar .btn-quiet:hover { background: var(--bg-sunken); color: var(--text); }

/* ============================================================================
   Tags and search
   ========================================================================== */

.sheet-narrow { max-width: 52rem; margin: 0 auto; padding: 0 1rem 3rem; }

/* ── The flash ─────────────────────────────────────────────────────────────
   One line saying what just happened, above the page it happened on.

   It used to borrow `.sheet-narrow`, which is a PAGE wrapper and carries 3rem
   of bottom padding — so a one-line "Saved." stood a hundred pixels tall and
   shoved the whole page down, at a width that matched none of the pages it
   appeared above (they run from 52rem to 96rem). Edge to edge with the nav's
   own gutter instead: a banner cannot be misaligned with anything.

   The accent rule down the leading edge is what makes it read as a notice
   without an icon or a colour wash behind text that then has to clear 7:1
   against it. */
.flash {
	margin: .6rem .5rem 0;
	padding: .5rem .7rem;
	background: var(--bg-sunken);
	border: 1px solid var(--border);
	border-left: 3px solid var(--accent);
	border-radius: var(--radius-sm);
	font-size: var(--text-sm);
	color: var(--text);
}

/* The cloud. Size carries the count, but only across a deliberately narrow
   range: the old site let the biggest tag dwarf the rest, which turns a
   navigation aid into a bar chart you cannot read the labels of. Weight and
   colour carry the rest, so the smallest tag is still comfortably legible. */
.cloud { display: flex; flex-wrap: wrap; gap: .5rem .7rem; align-items: baseline; }
.cloud a {
	color: var(--text-muted);
	text-decoration: none;
	font-weight: var(--weight-small);
	line-height: 1.3;
}
.cloud a:hover { color: var(--accent); text-decoration: underline; }
.cloud a[data-heat="4"] { font-size: 1.6rem; color: var(--text); font-weight: 700; }
.cloud a[data-heat="3"] { font-size: 1.3rem; color: var(--text); }
.cloud a[data-heat="2"] { font-size: 1.1rem; }
.cloud a[data-heat="1"] { font-size: .95rem; }
.cloud a[data-heat="0"] { font-size: var(--text-sm); }
.cloud .n { font-size: .7em; opacity: .65; margin-left: .15em; font-variant-numeric: tabular-nums; }

.result-group { margin-bottom: 2rem; }
/* A group with no results renders nothing, so it must take no room either —
   otherwise "no people matched" reads as a rendering fault. */
.result-group:empty { display: none; margin: 0; }
.result-head {
	display: flex; align-items: baseline; justify-content: space-between; gap: .75rem;
	margin-bottom: .6rem; padding-bottom: .35rem;
	border-bottom: 1px solid var(--border);
}
.result-head h2 { margin: 0; font-size: .95rem; font-weight: 700; }
.result-head .count { font-size: var(--text-sm); color: var(--text-muted); }

.people { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .15rem; }
.person {
	display: flex; align-items: center; gap: .7rem;
	padding: .55rem .5rem;
	border-radius: var(--radius-sm);
}
.person:hover { background: var(--bg-sunken); }
.person > img { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; flex: none; }
.person .who { min-width: 0; flex: 1; }
.person .who a { color: var(--text); font-weight: 600; text-decoration: none; }
.person .who a:hover { text-decoration: underline; }
.person .who p { margin: 0; font-size: var(--text-sm); color: var(--text-muted);
	overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.person .btn { flex: none; }

.searchbar { display: flex; gap: .5rem; margin: 0 0 1.5rem; }
.searchbar input { flex: 1; }

/* ============================================================================
   Settings and admin
   ========================================================================== */

.settings { max-width: 62rem; margin: 0 auto; padding: 0 1rem 3rem; }
.settings-layout { display: flex; flex-direction: column; gap: 1.5rem; }
@media (width >= 48rem) {
	.settings-layout { flex-direction: row; align-items: flex-start; gap: 2rem; }
	.settings-nav { flex: 0 0 11rem; position: sticky; top: calc(var(--nav-height) + 1rem); }
	.settings-panel { flex: 1; min-width: 0; }
}

.settings-nav ul {
	list-style: none; margin: 0; padding: 0;
	display: flex; flex-wrap: wrap; gap: .2rem;
}
@media (width >= 48rem) { .settings-nav ul { flex-direction: column; flex-wrap: nowrap; } }
.settings-nav :is(button, .settings-tab) {
	display: block; text-align: left; text-decoration: none;
	border: 0; background: none; cursor: pointer;
	color: var(--text-muted);
	padding: .45rem .6rem;
	border-radius: var(--radius-sm);
	font-size: var(--text-sm); font-weight: var(--weight-small);
	white-space: nowrap;
}
.settings-nav :is(button, .settings-tab):hover { background: var(--bg-sunken); color: var(--text); text-decoration: none; }
.settings-nav :is(button, .settings-tab)[aria-current="true"] { background: var(--bg-sunken); color: var(--text); }
@media (width >= 48rem) { .settings-nav :is(button, .settings-tab) { width: 100%; } }

.card {
	background: var(--bg-card);
	border: 1px solid var(--border);
	border-radius: var(--radius-md);
	padding: 1.1rem 1.15rem;
	margin-bottom: 1rem;
}
.card > h2 { margin: 0 0 .2rem; font-size: 1rem; font-weight: 700; }
.card > .card-sub { margin: 0 0 1rem; font-size: var(--text-sm); color: var(--text-muted); }

/* The two ways out sit together, so the reversible one is the obvious one.
   Offering only deletion is how somebody who wanted a quiet fortnight ends up
   with nothing (plan §5.13). */
.exits { display: grid; gap: .75rem; }
@media (width >= 40rem) { .exits { grid-template-columns: 1fr 1fr; } }
.exit {
	border: 1px solid var(--border);
	border-radius: var(--radius-sm);
	padding: .9rem;
}
.exit h3 { margin: 0 0 .25rem; font-size: var(--text-sm); font-weight: 700; }
.exit p { margin: 0 0 .75rem; font-size: var(--text-sm); color: var(--text-muted); }
.exit-danger { border-color: var(--border-firm); }

/* The row of figures. `margin-bottom` matches `.card`, because a stats row is
   almost always followed by one and without it the two sit flush against each
   other — 0px where every other pair on the page has 16px, which reads as a
   mistake rather than as grouping. The mock-up had been patching this per
   instance with an inline margin-top on the card after it. */
.stats {
	display: grid;
	gap: .75rem;
	grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
	margin-bottom: 1rem;
}
.stat {
	background: var(--bg-card);
	border: 1px solid var(--border);
	border-radius: var(--radius-md);
	padding: .8rem .9rem;
}
.stat b { display: block; font-size: 1.5rem; font-weight: 700; letter-spacing: -.02em; line-height: 1.1; }
.stat span { font-size: var(--text-sm); color: var(--text-muted); }
.stat.is-alert b { color: var(--accent); }

/* ── The quota grid ────────────────────────────────────────────────────────
   One row per limit, one column per role, exactly as plan §6.3's table has it.
   A table rather than six labelled fields because the question an admin is
   actually asking is comparative — "what do admins get that users do not" —
   and six separate rows make you hold both numbers in your head to answer it.
   The row header carries its own hint so the table needs no legend beneath. */
.quota-grid { width: 100%; border-collapse: collapse; }
.quota-grid th, .quota-grid td { text-align: left; padding: .4rem .5rem .4rem 0; vertical-align: top; }
.quota-grid thead th {
	font-size: var(--text-sm);
	font-weight: var(--weight-small);
	color: var(--text-muted);
	border-bottom: 1px solid var(--border);
	padding-bottom: .35rem;
}
.quota-grid tbody th { font-size: var(--text-sm); font-weight: var(--weight-small); padding-right: 1rem; }
.quota-grid tbody th span {
	display: block;
	font-weight: 400;
	font-size: var(--text-sm);
	color: var(--text-muted);
}
.quota-grid td { width: 7rem; }
.quota-grid input { width: 100%; }
@media (width < 34rem) {
	/* Narrow enough that a two-column table would scroll sideways: the hint
	   under each row header is what makes it readable stacked. */
	.quota-grid tbody th span { max-width: 14rem; }
	.quota-grid td { width: 4.5rem; }
}

/* The pause interstitial. A full page, not a dialog: while it is up the session
   can reach this and sign-out and nothing else (plan §5.13), and a dialog would
   imply there is a page behind it to dismiss back to. */
.interstitial {
	min-height: calc(100vh - var(--nav-height) - 3.25rem);
	display: grid;
	place-items: center;
	padding: 2rem 1rem;
}
.interstitial-box {
	max-width: 30rem;
	text-align: center;
}
.interstitial-box h1 { margin: 0 0 .5rem; font-size: 1.35rem; letter-spacing: -.015em; }
.interstitial-box p { margin: 0 0 1rem; color: var(--text-muted); }
.interstitial-box .btn-row { display: flex; gap: .5rem; justify-content: center; flex-wrap: wrap; }

.flagcard {
	background: var(--bg-card);
	border: 1px solid var(--border);
	border-radius: var(--radius-md);
	padding: .95rem 1rem;
	margin-bottom: .85rem;
}
.flagcard-head {
	display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
	margin-bottom: .6rem;
}
.flagcard-head .what { font-weight: 700; font-size: var(--text-sm); }
.badge {
	display: inline-block;
	font-size: 11px; font-weight: 700; letter-spacing: .02em;
	padding: .1rem .4rem; border-radius: var(--radius-pill);
	background: var(--pill-bg); color: var(--pill-text);
}
/* The reported thing, beside its evidence: for a tile the picture IS most of
   the judgement, so the card shows it rather than making somebody open it. */
.flagcard-body { display: flex; gap: .75rem; align-items: flex-start; margin-bottom: .7rem; }
.flagcard-body .flagcard-quote { flex: 1; margin-bottom: 0; }
.flag-thumb {
	position: relative;
	flex: none;
	width: 84px; height: 84px;
	border-radius: var(--radius-sm);
	overflow: hidden;
	background: var(--bg-sunken);
	display: block;
}
.flag-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.flag-thumb-note {
	position: absolute; inset: auto 0 0 0;
	background: rgb(0 0 0 / .62); color: #fff;
	font-size: 10px; font-weight: 700; text-align: center;
	padding: .1rem 0;
}

.flagcard-quote {
	background: var(--bg-sunken);
	border-radius: var(--radius-sm);
	padding: .6rem .7rem;
	font-size: .95rem;
	margin-bottom: .7rem;
}
.flagcard-reasons { list-style: none; margin: 0 0 .8rem; padding: 0; display: flex; flex-direction: column; gap: .4rem; }
.flagcard-reasons li { font-size: var(--text-sm); }
.flagcard-reasons .who { color: var(--text-muted); }
.flagcard-actions { display: flex; gap: .4rem; flex-wrap: wrap; }

/* ============================================================================
   Themes: the card grid

   The shape `notes` uses. A palette cannot be judged from its name and it
   cannot be judged from a hex value either, so every theme shows its actual
   colours — light down one side, dark down the other — before you are asked to
   choose between them.

   The three bands are ground, accent and text rather than the three grounds:
   Mosaic's grounds sit within a few percent of each other on purpose, so a
   strip of them would be three near-identical stripes and tell you nothing.
   ========================================================================== */

.themegrid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
	gap: 1rem;
}
.themecard {
	border: 1px solid var(--border);
	border-radius: var(--radius-md);
	overflow: hidden;
	background: var(--bg-card);
}
.themecard.is-editing { outline: 2px solid var(--accent); outline-offset: 2px; }

.themecard .prev { display: grid; grid-template-columns: 1fr 1fr; height: 74px; }
.themecard .prev > div { display: grid; grid-template-rows: 1fr 1fr 1fr; }
.themecard .prev span { display: block; }
/* A hairline between the two modes, so a light palette does not appear to bleed
   into the dark one it sits beside. */
.themecard .prev > div + div { border-left: 1px solid var(--border); }

.themecard .meta { padding: .6rem .7rem; border-top: 1px solid var(--border); }
.themecard .meta .n {
	font-weight: 700;
	font-size: .9rem;
	display: flex; align-items: center; gap: .35rem; flex-wrap: wrap;
}
.themecard .meta .n .badge { font-size: 10px; }
.themecard .meta .s {
	font-size: var(--text-sm);
	color: var(--text-muted);
	margin-top: .15rem;
}
.themecard .acts { display: flex; gap: .3rem; padding: 0 .7rem .7rem; flex-wrap: wrap; }
.themecard .acts .btn { padding: .25rem .5rem; }

/* The editor's own colour rows: a swatch and its hex, per mode. */
/* Wide enough for a swatch, a hex field and the token's full name. At 14rem
   the names truncated to `--bg-ca…`, which makes the one column that says what
   you are editing the one you cannot read. */
.tokengrid { display: grid; gap: .35rem; grid-template-columns: repeat(auto-fill, minmax(18.5rem, 1fr)); }
.tokenrow {
	display: flex; align-items: center; gap: .5rem;
	font-size: var(--text-sm);
}
/* The field inside is a text input that color-picker.js wraps in its own
   swatch button; see public/css/color-picker.css. */
.tokenrow .cp-wrap { flex: none; }
.tokenrow code {
	font-size: var(--text-sm);
	color: var(--text-muted);
	overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The infinite-scroll sentinel. It has height so the observer can see it
   arrive; without any it can sit exactly on the boundary and never intersect. */
.scroll-sentinel { height: 1px; margin-top: 2rem; }
.scroll-sentinel[data-loading]::after {
	content: 'Loading…';
	display: block;
	text-align: center;
	padding: 1rem 0;
	font-size: var(--text-sm);
	color: var(--text-muted);
}

/* ============================================================================
   The notification table

   A row per kind, a column per channel. On a phone the table becomes a stack of
   cards — each row keeps its own labels through `data-label`, because a bare
   column of checkboxes with the headings scrolled away is unanswerable.
   ========================================================================== */

.notif-table-wrap { overflow-x: auto; margin-bottom: 1.25rem; }
.notif-table { width: 100%; border-collapse: collapse; }
.notif-table th {
	text-align: left;
	font-size: var(--text-sm);
	font-weight: var(--weight-small);
	letter-spacing: .04em;
	text-transform: uppercase;
	color: var(--text-muted);
	padding: 0 .5rem .5rem;
	border-bottom: 1px solid var(--border);
	white-space: nowrap;
}
.notif-table th.notif-check, .notif-table td.notif-check { text-align: center; width: 5.5rem; }
.notif-table td { padding: .85rem .5rem; border-bottom: 1px solid var(--border); vertical-align: top; }
.notif-table tr:last-child td { border-bottom: 0; }
.notif-table .cell-main { font-size: .95rem; }
.notif-desc {
	display: block;
	font-size: var(--text-sm);
	color: var(--text-muted);
	margin-top: .15rem;
	max-width: 42rem;
}
.notif-box { display: inline-flex; padding: .25rem; cursor: pointer; }
.notif-box input { width: 18px; height: 18px; accent-color: var(--accent); cursor: pointer; }

@media (width < 48rem) {
	/* Each row becomes its own card, and every checkbox keeps a visible label —
	   a column of anonymous boxes with the heading row scrolled off the top is
	   a question nobody can answer. */
	.notif-table, .notif-table tbody, .notif-table tr, .notif-table td { display: block; width: auto; }
	.notif-table thead { display: none; }
	.notif-table tr {
		border: 1px solid var(--border);
		border-radius: var(--radius-sm);
		padding: .6rem .7rem;
		margin-bottom: .6rem;
	}
	.notif-table td { border: 0; padding: .2rem 0; }
	.notif-table td.notif-check {
		display: flex;
		align-items: center;
		justify-content: space-between;
		width: auto;
		padding: .3rem 0;
	}
	.notif-table td.notif-check::before {
		content: attr(data-label);
		font-size: var(--text-sm);
		font-weight: var(--weight-small);
		color: var(--text-muted);
	}
	.notif-table .cell-main { font-weight: 600; margin-bottom: .35rem; }
}

.notif-freq { display: block; font-size: var(--text-sm); font-weight: var(--weight-small); }
.notif-freq select { margin-top: .3rem; max-width: 22rem; }

/* ============================================================================
   Empty states
   ========================================================================== */

.empty {
	text-align: center;
	padding: 4rem 1rem;
	color: var(--text-muted);
}
.empty h2 { margin: 0 0 .4rem; font-size: 1.05rem; color: var(--text); }
.empty p { margin: 0 auto 1.25rem; max-width: 26rem; font-size: .95rem; }
