:root {
	--bg: #1b1620;
	--panel: #251d2c;
	--panel-border: #453a54;
	--text: #f3ede4;
	--muted: #b6a9c4;
	--accent: #d1a441;
	--link: #b79bee;
	--good: #2fbf6a;
	--bad: #e0555a;
	--fatal: #7c1d2a;
	--unattempted: #4a4056;
}

* {
	box-sizing: border-box;
}

body {
	font-family: Georgia, "Iowan Old Style", "Palatino Linotype", serif;
	font-size: 16px;
	margin: 0;
	padding: 0;
	min-height: 100vh;
	background: var(--bg);
	color: var(--text);
	display: flex;
	flex-direction: column;
}

header {
	padding: 14px 16px 6px;
	text-align: center;
}

h1 {
	margin: 0;
	font-size: 22px;
}

.accent a {
	color: var(--accent);
	text-decoration: none;
}

.hub-link {
	color: var(--muted);
	text-decoration: none;
	font-size: 14px;
	font-weight: normal;
	margin-left: 8px;
}

.hub-link:hover {
	color: var(--text);
}

#subtitle {
	font-size: 13px;
	font-style: italic;
	color: var(--muted);
	margin-top: 2px;
}


main {
	flex: 1;
	width: 100%;
	max-width: 480px;
	margin: 0 auto;
	padding: 12px 16px 32px;
	display: flex;
	flex-direction: column;
	gap: 14px;
}

.panel {
	background: var(--panel);
	border: 1px solid var(--panel-border);
	border-radius: 10px;
	padding: 16px;
}

.hidden {
	display: none !important;
}

.banner {
	border-radius: 8px;
	padding: 10px 14px;
	font-size: 14px;
}

.banner-error {
	background: rgba(224, 85, 90, 0.18);
	border: 1px solid var(--bad);
	color: var(--text);
}

/*
 * The shared-link notice (script.js's renderSeedNotice()), shown when a
 * `?seed=` names a day other than today. Quieter than the test banner
 * beneath it -- this is a real run by a real visitor and the page should not
 * shout at them -- but present, because a page state the player cannot tell
 * they are in is the thing both of these exist to prevent.
 */
.banner-seed {
	background: rgba(255, 255, 255, 0.03);
	border: 1px solid var(--panel-border);
	color: var(--muted);
	font-size: 12.5px;
	padding: 7px 10px;
	text-align: center;
}

/*
 * The test-mode banner (script.js's renderTestBanner()). Deliberately plain
 * and deliberately unmissable: its whole job is that nobody looks at a
 * screen wondering whether what they're seeing is a real run. Dashed, so it
 * doesn't read as one of the game's own panels, and monospace, so the
 * parameters it echoes back read as the URL they came from.
 */
.banner-test {
	background: rgba(255, 255, 255, 0.04);
	border: 1px dashed var(--accent);
	color: var(--accent);
	font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
	font-size: 12px;
	padding: 7px 10px;
	text-align: center;
	overflow-wrap: anywhere;
}

/*
 * Heads the timeline (not the page — see script.js's renderAdventureHeader()
 * doc comment). Kept compact on purpose: it sits between the sheet's
 * skill grid and the beat strip, and mustn't grow tall enough to push the
 * first timeline entry below the fold on a phone.
 */
.adventure-header {
	text-align: center;
	margin: 4px 0 12px;
	padding-top: 12px;
	border-top: 1px solid var(--panel-border);
}

#adventure-title {
	margin: 0 0 4px;
	font-size: 19px;
	letter-spacing: 0.5px;
	color: var(--accent);
}

/*
 * The day's site: the Delve theme and domain names, side by side. `gap`
 * and the hairline are what separate them — there is deliberately no
 * separator string in the markup, because every character on this page
 * that isn't from a CC-BY source is counted (see script.js's
 * OBJECTIVE_LABEL). `flex` also means the whitespace text nodes between
 * the two spans in index.html collapse away entirely rather than
 * rendering as a space of their own.
 */
.adventure-site {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: baseline;
	gap: 8px;
	margin: 0 0 4px;
	font-size: 13px;
	letter-spacing: 0.4px;
}

.site-theme {
	font-weight: bold;
	color: var(--accent);
}

.site-domain {
	font-weight: bold;
	color: var(--muted);
	border-left: 1px solid var(--panel-border);
	padding-left: 8px;
}

/*
 * Ironsworn: Delve's site_domains names are lower-cased past the first word
 * -- "Frozen cavern", "Sea cave" -- while Delve's own oracle table writes
 * the same places "Frozen Cavern". On 16% of days that reads as a typo to
 * every reader, and they report it as one.
 *
 * Capitalised HERE and not in the data, which is the same call #adventure-
 * title already gets by being upper-cased in CSS rather than in the pack:
 * the shipped string stays exactly as its author wrote it, and anyone who
 * inspects site/data/delve.json finds Tomkin's text rather than ours. Safe
 * on all twelve domains -- `capitalize` raises the first letter of each word
 * and lowers nothing, and no domain carries an internal capital for it to
 * disturb.
 *
 * Applied to the header's own span and to the domain inside a lead sentence
 * alike; script.js's LEAD_SLOT_CLASS is what puts it on the latter.
 */
.domain-name {
	text-transform: capitalize;
}

/* The domain's own sentence then the theme's, verbatim — italic for the
   same reason the hook this replaced was: it is the one piece of prose in
   a page otherwise made of names and numbers. */
.adventure-summary {
	margin: 0;
	font-style: italic;
	font-size: 13px;
	color: var(--muted);
}

/* "Objective: Bag of Holding" — label and item in their own spans, gap
   rather than a joining string, same rule as .adventure-site above. */
.adventure-objective {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 6px;
	margin: 6px 0 0;
	font-size: 14px;
}

.objective-label {
	color: var(--muted);
}

.objective-name {
	font-weight: bold;
}

#picker-panel label {
	display: block;
	font-size: 13px;
	color: var(--muted);
	margin-bottom: 6px;
}

#class-select {
	width: 100%;
	padding: 10px;
	font-size: 16px;
	background: var(--bg);
	color: var(--text);
	border: 1px solid var(--panel-border);
	border-radius: 6px;
	margin-bottom: 10px;
}

button {
	font-family: inherit;
	font-size: 16px;
	font-weight: bold;
	padding: 12px 16px;
	width: 100%;
	border: none;
	border-radius: 6px;
	background: var(--accent);
	color: #241a08;
	cursor: pointer;
}

button:disabled {
	opacity: 0.5;
	cursor: default;
}

button:not(:disabled):hover {
	filter: brightness(1.08);
}

.hint {
	font-size: 12px;
	color: var(--muted);
	margin: 10px 0 0;
}

.muted {
	color: var(--muted);
	font-size: 13px;
}

.char-header {
	text-align: center;
	margin-bottom: 14px;
}

#char-name {
	margin: 0 0 2px;
	font-size: 24px;
	color: var(--accent);
}

#char-line {
	margin: 0 0 4px;
	font-size: 14px;
	color: var(--text);
}

#char-feat {
	margin: 0;
}

.sheet-section {
	margin-bottom: 14px;
}

/*
 * Flex rather than a float on the note, because the note now always has
 * content and its longest form ("Total 81 · top 4% · Rarity 37") does not
 * fit beside the heading at phone width. A float would overlap the heading
 * text there; a wrapping flex row drops the note onto its own line instead.
 * The headings with no note span are a single item and look identical.
 */
.sheet-heading {
	margin: 0 0 8px;
	display: flex;
	flex-wrap: wrap;
	justify-content: space-between;
	align-items: baseline;
	gap: 0 10px;
	font-size: 12px;
	text-transform: uppercase;
	letter-spacing: 0.6px;
	color: var(--muted);
	border-bottom: 1px solid var(--panel-border);
	padding-bottom: 4px;
}

.sheet-heading-note {
	text-transform: none;
	letter-spacing: normal;
	color: var(--accent);
	font-weight: bold;
}

.ability-grid {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 8px;
}

.ability-cell {
	background: rgba(255, 255, 255, 0.03);
	border: 1px solid var(--panel-border);
	border-radius: 6px;
	padding: 6px 8px;
	text-align: center;
}

.ability-code {
	font-weight: bold;
	color: var(--accent);
	font-size: 13px;
	letter-spacing: 0.5px;
}

/*
 * The four dice behind one ability's rolled value (script.js's diceRow()),
 * the discarded one struck through — the arithmetic the score came from,
 * checkable by eye. Six abilities x four dice is a lot of glyphs, so these
 * are deliberately small and tight, and `flex` with no wrap keeps one
 * ability's throw on one line at every width.
 */
.ability-dice {
	display: flex;
	justify-content: center;
	gap: 3px;
	margin-top: 4px;
}

.die {
	min-width: 17px;
	padding: 1px 2px;
	border: 1px solid var(--panel-border);
	border-radius: 3px;
	background: rgba(255, 255, 255, 0.04);
	font-size: 11.5px;
	line-height: 1.35;
	font-variant-numeric: tabular-nums;
}

/*
 * Dimmed AND struck through, not one or the other: the strike says
 * "discarded" to anyone reading the row, and the dimming is what makes the
 * remaining three read as the sum beneath them at a glance.
 */
.die-dropped {
	color: var(--muted);
	opacity: 0.55;
	text-decoration: line-through;
	/* A default-weight strike across an 11px digit closes it up into a
	   glyph of its own — a struck 1 reads as a symbol rather than a 1. The
	   discarded die still has to be readable; it is the fourth number in
	   arithmetic the player is invited to check. */
	text-decoration-thickness: 1px;
}

/*
 * Phase one of the reveal (script.js's renderRollStrip()): the six 4d6 sets
 * as thrown, before any of them belongs to an ability. Same dice glyphs as
 * the ability grid, since they are literally the same six sets — the point
 * of the strip is that the player can watch one of them arrive in a slot.
 */
.roll-strip {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 8px;
}

.roll-set {
	background: rgba(255, 255, 255, 0.03);
	border: 1px solid var(--panel-border);
	border-radius: 6px;
	padding: 6px 8px;
	text-align: center;
}

/*
 * A set that has not been rolled yet (script.js's animateRollStrip()). The
 * BOX stays and only its contents go, exactly as `.awaiting-placement` does
 * for the ability slots below: six empty frames waiting, and nothing on the
 * page moves as each one fills. Revealing the cells themselves one at a time
 * would reflow the grid six times and walk the whole sheet down the page
 * under the player's eyes.
 *
 * Scoped to `.roll-set-pending`, which exists only on a strip cell, so it
 * cannot reach the ability grid's identical dice markup — the mistake the
 * `.awaiting-placement` rules had to be scoped against.
 *
 * Hiding is all it does. Phase two removes the strip panel outright, so
 * anything else set here would be state nothing is left to undo.
 */
.roll-set-pending .ability-dice,
.roll-set-pending .roll-total {
	visibility: hidden;
}

.roll-total {
	margin-top: 3px;
	font-size: 19px;
	font-weight: bold;
	color: var(--accent);
}

.roll-strip-note {
	margin: 10px 0 0;
	font-size: 12.5px;
	text-align: center;
}

/*
 * The sheet, waiting for the placement. `visibility` rather than `display`
 * on purpose: every box keeps its exact final size and every heading stays
 * put, so when the values land in phase two nothing on the page moves and
 * the sheet reads as one being filled in.
 *
 * Every derived VALUE goes with the ability slots, because all of them are
 * computed from the placed scores — see animateReveal(). Their labels stay,
 * which is the difference between a sheet waiting to be filled in and a
 * page with holes punched in it. The ability rules are scoped to
 * `.ability-cell` on purpose: the roll strip above reuses the same dice
 * markup, and it is the one thing phase one exists to show.
 *
 * Removing this one class is the only thing phase two has to do to be
 * correct, which is what makes a frozen animation survivable.
 */
.awaiting-placement .stat-value {
	visibility: hidden;
}

/*
 * One ability slot, still waiting for its number (script.js's
 * animatePlacement()). Split out of `.awaiting-placement` when the placement
 * became sequential: the six slots no longer fill together, so the hide has
 * to come off one at a time.
 *
 * Deliberately a hide and nothing else, and deliberately NOT an un-hide rule
 * layered over the sheet-wide one — a `visibility: visible` override would be
 * state phase two had to undo in the right order, and the safety argument has
 * always been that it does not have to. settle() clears these the same way it
 * clears the sheet-wide class, so the finished sheet is one removal away from
 * any point in the sequence.
 *
 * The class is on the CELL, so it cannot reach the roll strip's identical
 * dice markup — the scoping mistake the sheet-wide rules had to be fixed for.
 */
.ability-cell-pending .ability-dice,
.ability-cell-pending .ability-rolled,
.ability-cell-pending .ability-score {
	visibility: hidden;
}

/*
 * A strip set that has given its number up. The dice stay — the player can
 * still see which of the six sets went — and only the total goes, because it
 * is in flight and a number cannot be in two places while someone is watching
 * one at a time.
 */
.roll-set-spent .roll-total {
	visibility: hidden;
}

/*
 * The number in flight, from its strip cell to its ability slot.
 *
 * `position: fixed` so it is out of BOTH containers' flow: the strip sits
 * directly above the Ability Array, and anything that changes layout in
 * either walks the whole sheet down the page while the player is reading it.
 * It is decoration only — the value it carries is already in the slot it is
 * flying to, and a `setTimeout` is what reveals it — so a browser that never
 * runs the transition loses the flight and keeps the sheet.
 */
.placement-flier {
	position: fixed;
	z-index: 50;
	pointer-events: none;
	margin: 0;
	text-align: center;
	font-size: 19px;
	font-weight: bold;
	color: var(--accent);
	will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
	.placement-flier {
		display: none;
	}
}

.ability-rolled {
	font-size: 12px;
	color: var(--muted);
	margin-top: 2px;
}

.ability-score {
	font-size: 12.5px;
	margin-top: 1px;
}

.stat-grid {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: 6px 10px;
}

.save-grid,
.skill-grid {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: 4px 10px;
}

.stat {
	display: flex;
	justify-content: space-between;
	gap: 6px;
	font-size: 13px;
	padding: 3px 0;
}

.stat-save-skill {
	position: relative;
	padding-right: 14px;
}

.stat-label {
	color: var(--muted);
}

.stat-value {
	font-weight: bold;
	text-align: right;
}

.stat-save-skill.proficient .stat-value {
	color: var(--accent);
}

.proficient-dot {
	position: absolute;
	right: 0;
	top: 6px;
	width: 6px;
	height: 6px;
	border-radius: 50%;
	background: var(--accent);
}

/*
 * The party roster (script.js's renderRoster()/updateRosterFinalState()) —
 * built and inserted at render time, not present in index.html's static
 * markup. Deliberately smaller and plainer than the ability grid above it:
 * "the player's character stays primary" is an explicit design instruction,
 * not a preference, so nothing here should compete with #char-name or the
 * ability grid for attention.
 */
.roster-grid {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 8px;
}

.roster-card {
	background: rgba(255, 255, 255, 0.02);
	border: 1px solid var(--panel-border);
	border-radius: 6px;
	padding: 6px 8px;
	text-align: center;
	font-size: 11.5px;
}

.roster-name {
	font-weight: bold;
	font-size: 12.5px;
}

.roster-line {
	margin-top: 1px;
}

.roster-stats {
	margin-top: 3px;
	color: var(--muted);
}

.roster-status {
	margin-top: 3px;
	font-weight: bold;
	color: var(--muted);
	min-height: 1.2em; /* reserved up front so a card doesn't reflow when the end-state text lands */
}

.roster-card.roster-dead {
	border-color: var(--fatal);
	background: rgba(124, 29, 42, 0.18);
}

.roster-card.roster-dead .roster-status {
	color: #ff8f9a;
}

.beat-strip {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
	justify-content: center;
	margin: 12px 0;
}

.beat {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 2px;
	padding: 6px 8px;
	border-radius: 6px;
	background: rgba(255, 255, 255, 0.05);
	border: 1px solid var(--panel-border);
	font-size: 18px;
	cursor: default;
}

/* Check-beat states (checkState()) and combat states (combatState()) share
   this vocabulary with the timeline's .entry-* classes — see beatStateClass()
   in script.js. 'died'/'fatal' are the same fatal-combat concept under two
   names (the timeline calls it 'died'; nothing here calls a check beat
   'fatal'), so both selectors are kept for the class either can produce. */
.beat-cleared,
.beat-killed,
.beat-party-killed {
	border-color: var(--good);
}

.beat-failed,
.beat-fumbled {
	border-color: var(--bad);
}

.beat-fatal,
.beat-died {
	border-color: var(--fatal);
	background: rgba(124, 29, 42, 0.35);
}

/* `uninvolved` reads exactly like `survived` here on purpose — see the
   COMBAT_STATE_EMOJI note in script.js: the engine pays them the same
   quarter-value floor, so the strip shouldn't claim they were different
   results for this player. */
.beat-survived,
.beat-uninvolved {
	border-color: var(--accent);
}

.beat-unattempted {
	border-color: var(--unattempted);
	opacity: 0.55;
}

/* The not-yet-revealed placeholder (renderBeatStrip()'s `mask` path) — deliberately the same muted, dim treatment as .beat-unattempted so it reads as "nothing to see here yet," not as a fifth outcome. */
.beat-masked {
	border-color: var(--unattempted);
	opacity: 0.55;
}

.timeline {
	list-style: none;
	margin: 8px 0 14px;
	padding: 0;
	display: flex;
	flex-direction: column;
	/* Margin-based spacing, not a flex `gap` — a `gap` still reserves space
	   between children regardless of their own height, which would leave a
	   dead strip between every still-`.pending` entry. Margin collapses to
	   nothing along with the rest of a pending entry's box (see below). */
}

.entry {
	border: 1px solid var(--panel-border);
	border-left-width: 3px;
	border-radius: 6px;
	padding: 8px 10px;
	margin-bottom: 8px;
	background: rgba(255, 255, 255, 0.03);
}

.entry:last-child {
	margin-bottom: 0;
}

/* The press-to-reveal timeline (script.js's beginPressReveal()/revealNext())
   only ever toggles these two classes — opacity/transform/max-height,
   never `display`/`visibility` — so an entry's real text is still present
   for a screen reader or a plain text read the entire time, just visually
   collapsed. `.pending`: not yet reached — collapsed to nothing so five
   waiting entries don't leave a blank gap the height of the whole
   timeline before the player has pressed anything. `.landing`: reached
   and its roll is spinning, but its outcome/Renown haven't settled yet —
   already at full height, only outcome/renown are faded. Scoped to
   `no-preference` so prefers-reduced-motion gets an instant, un-animated
   reveal even in the (JS already skips this) case something still adds
   either class. */
@media (prefers-reduced-motion: no-preference) {
	.entry {
		max-height: 1000px;
		overflow: hidden;
		transition:
			opacity 0.3s ease,
			transform 0.3s ease,
			max-height 0.3s ease,
			margin-top 0.3s ease,
			margin-bottom 0.3s ease,
			padding 0.3s ease,
			border-width 0.3s ease;
	}

	/* `margin-top` is zeroed here as well as `margin-bottom` because
	   `.entry-finale` sets one — without this the day's climax would leave
	   a 14px gap floating in the timeline before it had been revealed,
	   which is the only part of that entry a pending box would otherwise
	   still be occupying. */
	.entry.pending {
		opacity: 0;
		transform: translateY(6px);
		max-height: 0;
		margin-top: 0;
		margin-bottom: 0;
		padding-top: 0;
		padding-bottom: 0;
		border-width: 0;
	}

	.entry-outcome,
	.entry-renown {
		transition: opacity 0.2s ease;
	}

	.entry.landing .entry-outcome,
	.entry.landing .entry-renown {
		opacity: 0;
	}
}

/* Visually hidden but present for assistive tech — the reveal announcer
   (script.js's announceEntry()) is the one thing on this page that must
   never be seen, only heard. */
.sr-only {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

/*
 * The list row the controls ride in as they follow the reveal down (see
 * script.js's moveControlsBelow()). It is a plain carrier: the timeline's
 * `list-style: none` already applies, and .entry's border and padding
 * deliberately do not, so the controls read as a control and not as a
 * sixth encounter.
 */
.reveal-controls-row {
	margin: 0;
	padding: 0;
	list-style: none;
}

.reveal-controls {
	display: flex;
	align-items: center;
	gap: 10px;
	margin: 4px 0 12px;
}

.reveal-controls button {
	width: auto;
	flex: 1 1 auto;
}

#reveal-all-btn {
	background: transparent;
	border: 1px solid var(--panel-border);
	color: var(--text);
	flex: 0 0 auto;
}

#reveal-progress {
	flex: 0 0 auto;
	white-space: nowrap;
}

.entry-cleared,
.entry-killed,
.entry-party-killed,
.entry-bonus {
	border-left-color: var(--good);
}

.entry-failed,
.entry-fumbled {
	border-left-color: var(--bad);
}

/* Combat that ended with neither side down — resolve.js's own outcome for
   this ("cleared" pre-fix, "survived" post-fix — see combatState()'s doc
   comment) isn't a clean win, so it isn't colored like one; amber says
   "notable, not resolved" rather than good or bad. */
.entry-survived,
.entry-uninvolved {
	border-left-color: var(--accent);
}

.entry-fatal,
.entry-died {
	border-left-color: var(--fatal);
	background: rgba(124, 29, 42, 0.2);
}

.entry-unattempted {
	border-left-color: var(--unattempted);
	opacity: 0.6;
}

/*
 * The day's climax (the pack's `beats[4].finale`). Given a rule above it
 * and a little more room so it reads as the end of the adventure rather
 * than the fifth of five identical rows — plus the word itself, since a
 * purely visual weight is silent to a screen reader. The state colours
 * above still own the left border, so marking the finale never overwrites
 * what happened in it.
 */
.entry-finale {
	margin-top: 14px;
	border-top-color: var(--accent);
}

.entry-finale-mark {
	font-size: 10.5px;
	font-weight: bold;
	letter-spacing: 1.2px;
	text-transform: uppercase;
	color: var(--accent);
	margin-bottom: 3px;
}

.entry-head {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 6px;
	font-size: 14px;
}

.entry-num {
	color: var(--muted);
	flex: 0 0 auto;
}

/*
 * The lead into a beat (script.js's buildLeads()) — the journey between the
 * cards, sitting at the top of the card it leads into.
 *
 * Inside the `<li>` rather than between them, deliberately: the `<li>` is
 * what `.pending` collapses, so a lead placed outside it would sit on the
 * page before its beat had been reached, and the finale's lead names the
 * objective it is about to be fought for. The cost is that it reads as the
 * card's first line rather than as the gap above it, which the subordinate
 * treatment here is doing the work of undoing — muted, italic, and a size
 * below the mechanical row, so nothing about it competes with the encounter
 * it introduces.
 */
.entry-lead {
	margin: 0 0 7px;
	font-size: 12.5px;
	font-style: italic;
	line-height: 1.45;
	color: var(--muted);
}

/*
 * What the check was for, in the SRD's own words (script.js's
 * beatExplanation()). A gloss on the mechanical row above it, so it is
 * marked as one: a quiet left rule and an indent, and the smallest type in
 * the card. It must not compete with the outcome on the head row or the
 * Renown figure below it, and it is the same sentence every time this skill
 * comes up, so it is reference text rather than news.
 *
 * Never on a combat beat — the authored line is the whole treatment there.
 */
.entry-explain {
	margin: 3px 0 0;
	padding-left: 8px;
	border-left: 2px solid var(--panel-border);
	font-size: 12px;
	line-height: 1.4;
	color: var(--muted);
}

/* The entry's headline, whichever kind of string it is: `.entry-line` is
   the pack's authored encounter, `.entry-label` this file's own synthetic
   text for the collapsed-unattempted and survival rows. Exactly one of the
   two is present per entry (script.js's renderTimelineEntry()). */
.entry-label,
.entry-line {
	font-weight: bold;
	/*
	 * A basis rather than `auto`, and this is load-bearing since the frames
	 * made the headline a sentence instead of a short authored line.
	 *
	 * With `auto` the headline's flex base size is its max-content width, so
	 * a long one exceeds the row and the flex algorithm moves it to a line of
	 * its own — leaving the beat number stranded above it, which reads as a
	 * mistake. With `0%` it shares the row with the outcome and gets squeezed
	 * into a ten-line ragged column (measured: the finale's head went to
	 * 160px tall). A fixed basis is the middle: a short headline still sits
	 * beside its outcome, and a long one takes the row and drops the outcome
	 * to the next line. Measured on the finale at 68px against 90 and 160.
	 */
	flex: 1 1 55%;
	min-width: 0;
}

/* The mechanical label — skill, ability and DC, or the monster's AC and HP
   — demoted to its own row under the encounter that heads the entry. Still
   every number it always carried, still unscrambled by the reveal spin
   (the DC and the AC are facts about the beat, not values that were
   rolled); just no longer the first thing the entry says. */
.entry-mech {
	margin-top: 2px;
	font-size: 12.5px;
	color: var(--muted);
}

.entry-outcome {
	flex: 0 0 auto;
	font-size: 12px;
	font-weight: bold;
	letter-spacing: 0.4px;
	color: var(--muted);
}

.entry-cleared .entry-outcome,
.entry-killed .entry-outcome,
.entry-party-killed .entry-outcome,
.entry-bonus .entry-outcome {
	color: var(--good);
}

.entry-failed .entry-outcome,
.entry-fumbled .entry-outcome {
	color: var(--bad);
}

.entry-survived .entry-outcome,
.entry-uninvolved .entry-outcome {
	color: var(--accent);
}

.entry-fatal .entry-outcome,
.entry-died .entry-outcome {
	color: #ff8f9a;
}

.entry-detail {
	display: flex;
	flex-wrap: wrap;
	justify-content: space-between;
	gap: 6px;
	margin-top: 4px;
	font-size: 13px;
	color: var(--muted);
}

.entry-renown {
	font-weight: bold;
	color: var(--accent);
}

.exchange-list {
	list-style: none;
	margin: 6px 0 0;
	padding: 6px 0 0;
	border-top: 1px dashed var(--panel-border);
	display: flex;
	flex-direction: column;
	gap: 3px;
	font-size: 12.5px;
	color: var(--muted);
}

/*
 * The companions' one-line tally (script.js's partyExchangeSummary()) and
 * the disclosure holding the full exchange log. Only the player's own
 * exchanges are open by default — 21% of fights ran to ten or more lines,
 * most of them other people missing — but the full log is still on the
 * page, one click away, because it's the audit trail that makes the
 * outcome checkable.
 */
.party-summary {
	margin-top: 4px;
	font-size: 12.5px;
	color: var(--muted);
}

.exchange-details {
	margin-top: 4px;
	font-size: 12.5px;
	color: var(--muted);
}

.exchange-details summary {
	cursor: pointer;
	list-style-position: inside;
}

/* The list inside the disclosure already sits under the summary's own
   line — a second dashed rule there would read as a third section. */
.exchange-details .exchange-list {
	border-top: 0;
	padding-top: 2px;
	margin-top: 2px;
}

.entry-hp {
	margin-top: 4px;
	font-size: 12.5px;
	color: var(--muted);
}

/*
 * What this character is trained in — the draw, without the arithmetic.
 * Names only: the modifier beside each is derived and lives in the fold.
 */
.proficient-list {
	margin: 0;
	font-size: 13.5px;
	overflow-wrap: anywhere;
}

/*
 * Everything the rules compute, folded away (script.js's
 * renderCharacterSheet()). Shut on load, because the point is a shorter sheet
 * and an open fold delivers none of it -- and nothing is lost, since every
 * number is one click from here.
 *
 * Styled as a quiet row rather than as one of the game's panels: it is a way
 * in, not a section of its own.
 */
.derived-fold {
	margin: 14px 0;
	border-top: 1px solid var(--panel-border);
	border-bottom: 1px solid var(--panel-border);
	padding: 10px 0;
}

.derived-fold-summary {
	cursor: pointer;
	list-style-position: inside;
	font-size: 12px;
	font-weight: bold;
	letter-spacing: 1px;
	text-transform: uppercase;
	color: var(--muted);
}

/* The first section inside the fold sits right under the summary and does not
   need the gap a sheet section normally carries above it. */
.derived-fold .sheet-section:first-of-type {
	margin-top: 8px;
}

.renown-line {
	font-size: 17px;
	font-weight: bold;
	text-align: center;
	margin: 0 0 4px;
	color: var(--accent);
}

/*
 * The two comparison lines under the score share one rule: they are the same
 * kind of statement about the same number and reading as a pair is the
 * point. A separate rule for the field line is how they drift apart -- and
 * the first draft of it did, centred against left-aligned, which made the
 * new line look like a stray caption rather than the sibling of the one
 * below it.
 */
#field-line,
#community-line {
	text-align: center;
	margin: 0 0 4px;
}

#community-line {
	margin-bottom: 8px;
}

.new-badge {
	text-align: center;
	font-weight: bold;
	color: var(--good);
	margin: 4px 0;
	font-size: 14px;
}

.share-row {
	display: flex;
	align-items: center;
	gap: 10px;
	margin-top: 10px;
}

.share-row button {
	width: auto;
	flex: 1;
}

#share-status {
	min-width: 60px;
}

.album-grid {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
}

.album-badge {
	background: rgba(209, 164, 65, 0.14);
	border: 1px solid var(--accent);
	color: var(--text);
	border-radius: 999px;
	padding: 5px 12px;
	font-size: 12.5px;
	cursor: default;
}

footer {
	padding: 16px;
}

.site-footer {
	max-width: 480px;
	margin: 0 auto;
	text-align: center;
	font-size: 11.5px;
	line-height: 1.5;
	color: var(--muted);
}

.site-footer a {
	color: var(--link);
}

@media (max-width: 400px) {
	/*
	 * Five across, on one row. As a wrapping flex row the five tiles do not
	 * fit 328px and break 4 + 1, leaving one orphan centred under the other
	 * four -- which read as a mistake anywhere and read as a worse one now
	 * that the strip sits directly above the Renown total. A grid of five
	 * equal columns shrinks them to fit instead of wrapping, which is the
	 * same call the ability dice already make one section up: a set that
	 * breaks across two lines stops reading as one set.
	 */
	.beat-strip {
		display: grid;
		grid-template-columns: repeat(5, 1fr);
		gap: 6px;
	}

	.beat {
		font-size: 16px;
		padding: 5px 4px;
	}

	.entry-head {
		font-size: 13px;
	}

	.entry-detail,
	.entry-mech,
	.entry-lead,
	.entry-explain,
	.exchange-list,
	.party-summary,
	.exchange-details,
	.entry-hp {
		font-size: 11.5px;
	}

	.ability-grid,
	.roll-strip {
		grid-template-columns: repeat(2, 1fr);
	}

	/* Four dice per ability at two abilities across: the faces shrink
	   rather than wrap, since a 4d6 roll that broke onto two lines would
	   stop reading as one throw. */
	.die {
		min-width: 15px;
		font-size: 10.5px;
		padding: 1px;
	}

	.stat-grid,
	.save-grid,
	.skill-grid,
	.roster-grid {
		grid-template-columns: 1fr;
	}

	.reveal-controls {
		flex-wrap: wrap;
	}

	#reveal-progress {
		flex-basis: 100%;
		text-align: center;
		order: 3;
	}
}
