/* Buttons, panels, the lobby form, the scoreboard, the blank sheet, toasts. */

/* --- buttons --- */

.button {
	min-height: 2.75rem;
	padding: 0 var(--space-3);
	border-radius: var(--radius);
	background: var(--surface-raised);
	color: var(--ink);
	font-weight: 600;
	box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.08);
	/*
	 * A transform that does nothing, so that :active below has one to change
	 * rather than one to create. Going from no transform to a transform rebuilds
	 * the paint property tree, and the board is a container query whose hundred
	 * premium labels are then re-shaped to suit: 19ms, measured, for a 1px nudge —
	 * paid on the press that starts a shuffle, and again on the release.
	 */
	transform: translateY(0);
}

/*
 * The one action the player came for. Taller, gold, and set in the game's own
 * face, so it is never mistaken for one of the four housekeeping buttons.
 *
 * The hue comes through --button-face so that a variant changes the colour and
 * nothing else: the weight of this button is the whole point of it, and a
 * destructive action wants that weight in a different colour, not less of it.
 */
.button--primary {
	--button-face: var(--accent);
	--button-ink: var(--accent-ink);
	min-height: 3rem;
	background: linear-gradient(180deg, color-mix(in srgb, var(--button-face) 82%, white), var(--button-face));
	color: var(--button-ink);
	font-family: var(--font-game);
	font-size: var(--step-1);
	font-weight: 700;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	box-shadow:
		inset 0 1px 0 rgb(255 255 255 / 0.45),
		inset 0 -2px 0 rgb(0 0 0 / 0.15),
		0 0.35rem 0.9rem -0.35rem rgb(0 0 0 / 0.6);
}

/*
 * Going ahead with something that cannot be taken back.
 *
 * Gold is what the game says yes with, and a sheet that ends the game should not
 * be answered in the same colour as playing a word. Worn with --primary rather
 * than instead of it: the button keeps its size and its face, and only the hue
 * carries the warning. Red is 5.1:1 against the panel it sits on and its ink is
 * 6.9:1 on the red, which is the gold pairing's 5.9 and 7.1 in another colour.
 */
.button--danger {
	--button-face: var(--danger);
	--button-ink: var(--danger-ink);
}

/*
 * Starting a table, and joining one. Worn with --primary like --danger is: the
 * size and the face are the button's, and only the hue is these. See tokens.css
 * for why the two hues are matched in luminance rather than chosen by eye.
 */
.button--start {
	--button-face: var(--start);
	--button-ink: var(--start-ink);
}

.button--join {
	--button-face: var(--join);
	--button-ink: var(--join-ink);
}

/*
 * A drawn mark on a button, in the ink the button is printed with.
 *
 * Drawn rather than a glyph, for the reason the teacher's face is: an emoji is
 * shaped and coloured by the operating system, inside a palette every ratio of
 * which is measured here. The stroke does not thin with the icon, because these
 * are read at one size.
 */
.button__icon {
	inline-size: 2rem;
	block-size: 2rem;
}

.button__icon * {
	fill: none;
	stroke: currentColor;
	stroke-width: 1.8;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.button__icon .button__icon-fill {
	fill: currentColor;
	stroke: none;
}

.button--quiet {
	background: rgb(255 255 255 / 0.03);
	box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.16);
}

.button--small {
	min-height: 2.25rem;
	padding: 0 var(--space-2);
	font-size: 0.875rem;
}

/*
 * A disabled Play is the commonest state of the whole interface — it is how the
 * player is told the tiles do not yet spell anything — so it has to look plainly
 * inert rather than merely faded.
 */
.button:disabled {
	opacity: 0.4;
	box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
}

/*
 * Gold at 40% over the dark table comes out olive, which reads as a mistake
 * rather than as unavailable. Play waits far more often than it is ready, so
 * when it is waiting it stops being gold at all.
 */
.button--primary:disabled {
	opacity: 1;
	background: rgb(255 255 255 / 0.06);
	color: var(--ink-dim);
	box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.1);
}

.button:not(:disabled):active {
	transform: translateY(1px);
}

/*
 * Every button carries the box its press flash is painted in.
 *
 * The flash is a layer inset to nothing over the button's own face — see
 * animations.css — and a layer inset to nothing needs the button to be what it is
 * inset to, or it fills the nearest positioned ancestor instead and washes a whole
 * panel. Three things about this one line are why it is one line.
 *
 * It is every button and not a list of the pressable classes, because press.js
 * asks nothing about classes either: a list is a second copy of the same set, and
 * the copy that drifts is the one that shows as a panel flashing white. The board's
 * squares and the rack's tiles are already positioned by their own rules, and
 * neither flashes in any case.
 *
 * It is the element and not a class, so it is the lowest specificity there is: a
 * pressable needing a position of its own has only to say so, in any rule, in any
 * order, and win.
 *
 * And it is declared at rest rather than on the flash class, which the same
 * argument rules out — a class that press.js never takes off would carry the
 * position for the rest of the page's life, and one that appears for 180ms is a
 * rule nothing would notice overriding.
 */
button {
	position: relative;
}

/* --- the ways out: a drawn cross, and the buttons it fills --- */

/*
 * Every cross the game draws, whatever it puts away.
 *
 * The stroke is the mark itself and is the same wherever it turns up — two lines
 * in a 24-unit box, so 2.2 units is 2.2px at the sizes below — and it is declared
 * once because five copies of it is five chances for one to be left behind. It is
 * `mark`, which ui/mark.js puts on every cross it builds alongside the panel's own
 * class; how big the mark is stays with the panel, since that is the part that
 * differs.
 */
.mark path {
	fill: none;
	stroke: currentColor;
	stroke-width: 2.2;
	stroke-linecap: round;
}

/*
 * The way out of a panel: 1.5rem of drawn cross in a 2.75rem target, which is the
 * touch floor and is what the word "Close" never reached.
 *
 * One rule for the four panels that have one, because a way out that is 44px on
 * three panels and 36px on the fourth is a rule a stylesheet loses quietly. Each
 * of them keeps its own class, because that is what the browser sweep and the
 * tests aim at, and because two of them still need a line of their own below.
 */
.callout__close,
.dictionary__close,
.lesson__dismiss,
.stats__close,
.stats__back {
	display: grid;
	place-items: center;
	inline-size: 2.75rem;
	block-size: 2.75rem;
	padding: 0;
	border-radius: var(--radius);
	color: var(--ink);
}

.callout__cross,
.dictionary__cross,
.lesson__cross,
.stats__cross,
.stats__chevron {
	inline-size: 1.5rem;
	block-size: 1.5rem;
}

/* Three of the four sit in a flex row and may not be squeezed by a long title
   beside them; the call-out's is in a row of its own and hangs from the top, so
   that it is not carried down by a gloss of several lines. */
.dictionary__close,
.lesson__dismiss,
.stats__close,
.stats__back {
	flex: none;
}

.callout__close {
	align-self: start;
}

/* --- lobby --- */

.lobby {
	display: grid;
	gap: var(--space-3);
	width: min(100%, 22rem);
	/* The plate below is positioned against this. */
	position: relative;
}

/*
 * A screen on its way out and the one replacing it, crossing.
 *
 * The lobby is a grid of one cell, so a second panel appended to it would sit in
 * a row below the one it is replacing and shove both up the window. Sharing the
 * cell is what lets the arriving screen travel to exactly where it will settle.
 * It is worn only while the two are in the air — see lobby.js — because a panel
 * left holding it would be one nothing can be laid beside.
 */
.lobby--crossing {
	grid-area: 1 / 1;
}

/* A flow's form carries the panel's own gap, so nesting it changes no spacing. */
.lobby__form {
	display: grid;
	gap: var(--space-3);
}

/*
 * Which of the two flows this is. It is a heading rather than decoration: it
 * names the region, so a screen reader entering the screen says where it is.
 */
.lobby__step {
	margin-bottom: calc(var(--space-1) * -1);
	font-family: var(--font-game);
	font-size: var(--step-1);
	font-weight: 700;
	text-align: center;
}

/* Who the seat is for, typed on the screen before and not editable here. */
.lobby__who {
	margin: 0;
	text-align: center;
	color: var(--ink-dim);
	font-size: 0.875rem;
}

/*
 * The way into a game is played out over the game's poster, and the poster
 * carries the name.
 *
 * It belongs to the screen and not to any panel on it, so that it holds still
 * while the panels slide past each other and stays up for every one of them: the
 * welcome screen, both flows, and the doorstep a knocking guest waits at. It goes
 * up over the room code screen for the same reason — the game has been made but
 * nobody is playing yet, and the board is not even drawn there — which is what
 * makes the artwork the whole of the way in and the board the whole of the game.
 * `screen--waiting` is the game screen's own account of that state, so there is no
 * second thing here to keep in step with it.
 *
 * Both take it away at the right moment with nothing to remember: the lobby is
 * hidden the instant a game is entered, `screen--waiting` comes off the moment
 * play begins, and neither a hidden element nor a class that has gone paints a
 * pseudo-element. Nothing here may be hung off a class of its own instead — one
 * has to be taken off again by whatever is drawn next, and the one that forgets
 * leaves the artwork behind a board.
 *
 * Fixed to the window rather than to the screen, since what it is is the wall
 * behind everything; nothing between it and the window may take a transform, a
 * filter or containment, or it is trapped in the screen's box. It also has to
 * stay behind the canvas rather than under it: `z-index: -1` paints below every
 * background in the page but above the one the body propagates to the canvas,
 * which is the only reason a poster shows through a screen at all. So neither
 * screen may take a z-index, and the game screen's `position: relative` is safe
 * only while it has none — either would make the screen the context this is
 * behind, and the poster would disappear under it.
 *
 * Nothing is washed over the artwork itself. What needs a darker ground brings
 * one: in the lobby that is the plate below, and on the room code screen it is
 * the cards, each of which carries an opaque fill of its own — the header
 * included, which is the next rule and had to be given one.
 */
.screen--lobby::before,
.screen--waiting::before {
	content: '';
	position: fixed;
	inset: 0;
	z-index: -1;
	background-color: var(--poster-mat);
	/*
	 * `contain`, so the whole poster is always shown and the mat takes up whatever
	 * is left over. Neither axis can be the one that always fits: a 428x926 phone
	 * is far taller in proportion than the 848x1264 artwork, so fitting the height
	 * would crop 96px off each side and cut the ends off the poster's own TILEZ,
	 * while a 1280x900 window fitting the width would blow it up to 1908 tall and
	 * show a band out of the middle. This asks for the smaller of the two scales
	 * and lets --poster-mat have the difference: 288px below and above on that
	 * phone, 338px either side in that window.
	 */
	background-image: url("../img/background.jpg");
	background-size: contain;
	background-position: center;
	background-repeat: no-repeat;
}

/*
 * The header is a card of its own while the poster is behind it.
 *
 * Everything on the room code screen is furniture built for the dark table, and
 * what makes the artwork safe behind it is that each piece brings an opaque fill
 * with it. The header did not: the bag pill is only as wide as its own words, so
 * the footer row was a full-width slot of poster with a pill floating in it — and
 * where that slot fell across the artwork's own TILEZ, which on a 320px phone is
 * exactly where it falls, it left the tops of four letters showing in a gap. A cut
 * word reads as a picture gone wrong rather than as a backdrop, and dimming the
 * wall does not cure it: at 80% the letters are still letters, and by then the
 * poster is grey everywhere it is not covered.
 *
 * So the header takes a ground the same way every other piece here has one, and it
 * is `--surface` — the table these inks were all measured against — so the Leave
 * button and the test mark are read over exactly what they were designed for
 * rather than over a photograph. The padding is affordable only because this is
 * the one screen where the board is not drawn: `--furniture` is not in play, so
 * two rows of 8px cost nothing anybody can see.
 */
.screen--waiting > .scoreboard {
	padding: var(--space-2);
	border-radius: var(--radius-lg) var(--radius-lg) 0 0;
	background: var(--surface);
}

/* The other half of the same card — see the seam in layout.css. */
.screen--waiting .admission {
	border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}

/*
 * The glass a panel is read through, and the one thing on this screen whose
 * contrast is set by a photograph rather than by the palette.
 *
 * Every mark here is meant to be seen through: the plate, the field and the two
 * buttons all let the burst behind them show. What that costs is measured against
 * the brightest thing the poster can put under a letter — a lit tile at a
 * luminance of 0.95. Measured over the poster's own pixels at every viewport, the
 * plate at 0.68 holds the field's label at 5.7:1 at worst, the name typed into the
 * field at 9.5:1, and each button's ink on its own 82% glass at 5.7:1. The plate
 * is near its floor and may not chase thinner: 0.62 puts the label at 4.7:1 and
 * 0.60 at 4.2:1, under the 4.5:1 this has to hold.
 *
 * The label is --ink rather than --ink-dim for the same reason, and the fault
 * carries a ground of its own — 14px of --danger cannot be printed on glass this
 * thin at any size, so the one line that only appears when something is wrong is
 * the one line that keeps a near-solid backing.
 *
 * The blur is on top of those ratios rather than part of them, because it is the
 * one thing here a browser may decline: it softens a lit tile under the plate, and
 * where it is not supported every number above still holds. Both spellings, since
 * the phones in use go back to Safari 15.
 *
 * It is the panel's own pseudo-element, so it travels with the panel it is
 * backing rather than staying put like the wall. It paints over the wall because
 * the panel is inside the screen: both are behind their own content and the
 * nearer element wins. Every lobby panel carries it, since every one of them is
 * read over the artwork.
 */
.lobby::after {
	content: '';
	position: absolute;
	/*
	 * One sheet across the poster on a phone, a card on anything wider. Between
	 * 320px and 428px the panel is nearly the whole window, so a card there is a
	 * card with a few pixels of poster beside it, which reads as a fault rather
	 * than as a margin; by 480px the same card has 48px either side.
	 */
	inset: calc(var(--space-3) * -1) -2.5rem;
	z-index: -1;
	border-radius: var(--radius-lg);
	background: rgb(9 33 27 / 0.68);
	box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.1);
	-webkit-backdrop-filter: blur(4px);
	backdrop-filter: blur(4px);
}

@media (min-width: 30rem) {
	.lobby::after {
		inset: calc(var(--space-3) * -1);
	}
}

/* Louder than --ink-dim, which the glass under it cannot carry. */
.lobby .field label {
	color: var(--ink);
}

/*
 * The field is glass too, and what is typed into it is what has to survive it.
 * Its own edge goes up with it: a hairline at 0.2 disappears against a lit
 * poster.
 */
.lobby .input {
	background: rgb(22 58 49 / 0.78);
	border-color: rgb(255 255 255 / 0.32);
}

/*
 * What is wrong with the field, on a ground of its own. It hugs its text rather
 * than filling the row, because a full-width bar under the field reads as part of
 * the field.
 */
.lobby .field__fault {
	justify-self: start;
	padding: 0.125rem var(--space-2);
	border-radius: var(--radius);
	background: rgb(9 33 27 / 0.92);
}

/*
 * The poster prints TILEZ across its top, so a panel's own wordmark would be the
 * name twice over — on every screen here, since the artwork is up for all of
 * them. The heading stays for a screen reader, which is told nothing by a
 * picture, and goes out of flow rather than to `display: none`: a 1px grid row
 * still takes the panel's gap.
 */
.lobby__title {
	position: absolute;
	inline-size: 1px;
	block-size: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/*
 * The two ways into a game, side by side and equally weighted.
 *
 * A row gives each 140px of the 288px the panel has on a 320px phone, which is
 * not enough for "START GAME" on one line at any size worth setting it in — it
 * measures 157.2px at --step-1 alone. So the label breaks into its two words, one
 * per line, which is what buys the size back: the widest word of the four is
 * START at 99.6px at --step-2, so the type is a third bigger than a primary
 * button's and still has 20px either side at the narrowest screen there is.
 */
.lobby__choices {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: var(--space-2);
	margin-top: var(--space-1);
}

/*
 * A mark over a label of two lines, centred, all of it the button's own ink.
 *
 * The break is in the markup rather than left to the width — see lobby.js — so
 * the two buttons are always the same shape as each other, whatever the label and
 * whatever the phone. Nothing here may go back to wrapping on its own: one button
 * of two lines beside one of three is not a pair of equal choices.
 */
.lobby__choice {
	display: grid;
	justify-items: center;
	align-content: center;
	gap: var(--space-1);
	padding: var(--space-2) var(--space-1);
	min-height: 4.75rem;
	line-height: 1.1;
	/*
	 * Glass, so the burst shows through the one thing on the screen that is meant
	 * to be pressed. The face is still the face: it is the same gradient the
	 * primary button paints, taken to 88% so that what is behind it is felt rather
	 * than read, which is what keeps the dark ink on it at better than 5:1.
	 */
	background-image: linear-gradient(
		180deg,
		color-mix(in srgb, color-mix(in srgb, var(--button-face) 82%, white) 82%, transparent),
		color-mix(in srgb, var(--button-face) 82%, transparent)
	);
}

.lobby__choice .button__label {
	display: grid;
	justify-items: center;
	font-size: var(--step-2);
	letter-spacing: 0.06em;
}

/*
 * With a game still going the left button says Continue Game rather than Start
 * Game, and the longer word is what the pair's type has to be sized to.
 *
 * CONTINUE measures 161.4px at --step-2 against the 132px a 320px phone's column
 * gives it, and a 1fr track will not shrink below its own content: the column
 * simply takes the width it needs and the other one gets what is left, which is
 * 169.4px against 110.6px there and 169.4 against 150.6 at 360px. Two buttons of
 * plainly different widths are not the pair of equal choices this screen is, so
 * the row steps down while that label is on it — measured, 140px apiece at 320px
 * and 160px at 360px. The whole row and not the one button, because two labels
 * set at two sizes are not a pair either, and one size at every width for the
 * reason the door's difficulty buttons keep one.
 *
 * --step-1 is what the narrowest screen will take, and the margin is the whole of
 * why it is not --step-2: measured at 320px across every face --font-game can
 * resolve to, CONTINUE is 126.1px in Iowan Old Style, 127.6px in Georgia and
 * 117.4px in the generic serif an Android falls back to, against 132px of button.
 */
.lobby__choices--resuming .lobby__choice .button__label {
	font-size: var(--step-1);
}

.lobby__choice .button__word {
	display: block;
}

.field {
	display: grid;
	gap: var(--space-1);
}

.field label {
	font-size: 0.875rem;
	color: var(--ink-dim);
}

/*
 * What is wrong with the field, under the field.
 *
 * A fault of a box belongs against that box rather than floating over the panel
 * in a toast that points at nothing. --danger is 6.6:1 on the table, which
 * carries at this size.
 */
.field__fault {
	margin: 0;
	font-size: 0.875rem;
	color: var(--danger);
}

.input {
	min-height: 2.75rem;
	padding: 0 var(--space-2);
	border: 1px solid rgb(255 255 255 / 0.2);
	border-radius: var(--radius);
	background: var(--surface-raised);
	color: var(--ink);
	/*
	 * An input's intrinsic width comes from its size attribute, twenty characters
	 * by default, and that minimum beats any cap put on an ancestor. The room code
	 * field at 1.6rem with letter-spacing demanded 335px inside a 320px screen and
	 * dragged the whole lobby into sideways scrolling. Nothing here needs to be
	 * wider than the space it is given.
	 */
	min-width: 0;
}

/*
 * A box the game draws, which neither of the two things typed into this game may
 * be a real `<input>`. See ui/textbox.js for the whole of why — in short, a text
 * field has a caret, a browser reveals a caret by scrolling to it, and installed
 * to the home screen that scroll parks an iPhone's whole document a status bar
 * out of true with what it answers to.
 *
 * It wears `.input` as well, so it is the same box as every real field beside it
 * and there is one description of what a field looks like rather than two.
 */
.textbox {
	display: flex;
	align-items: center;
	/* A word longer than the box is clipped rather than allowed to push the caret
	   out of it, which is what an input does with an overlong value. */
	overflow: hidden;
	/* It takes text, so it says so under a pointer even though it takes no
	   selection: what it does answer is a press, which raises the pad. */
	cursor: text;
	/* The whole box is one target and nothing in it is: a press anywhere on it
	   means the same thing. */
	user-select: none;
}

/* The value and the caret travel together, so the caret is always at the end of
   what has been typed and is centred with it where the box centres. */
.textbox__run {
	display: inline-flex;
	align-items: center;
	min-width: 0;
	white-space: pre;
}

.textbox__hint {
	color: var(--ink-dim);
	/* A placeholder is a hint and not a value, so it is spared whatever face the
	   box sets for what is typed into it. */
	font: var(--font);
	font-size: 1rem;
	letter-spacing: normal;
	text-transform: none;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/*
 * The caret, which is the box's own and not the browser's.
 *
 * It is a place marker before it is a movement, so it holds steady rather than
 * vanishing for a player who has asked for stillness — only the blink is behind
 * the query. 2px of --accent, which is the interface's own gold and is what every
 * other mark on this table is drawn in; 1.1em tall so it stands a little above
 * and below the letters, as a caret does.
 */
.textbox__caret {
	flex: none;
	inline-size: 2px;
	block-size: 1.1em;
	margin-inline-start: 1px;
	border-radius: 1px;
	background: var(--accent);
	/* No caret until the box is being typed into: one in a box nobody is filling
	   is a box claiming to be waiting for something. */
	visibility: hidden;
}

.textbox--typing .textbox__caret {
	visibility: visible;
}

@media (prefers-reduced-motion: no-preference) {
	.textbox--typing .textbox__caret {
		animation: caret-blink 1.06s steps(1, end) infinite;
	}
}

/*
 * A room code is read out and typed in, so it wants to be unmistakable.
 *
 * Centred as a whole rather than as a line of text: what is centred is the code
 * and its caret together, so the pair sits on the middle of the box and the caret
 * follows the last letter out from it. That is also what retires the indent the
 * old input needed — a letter-spaced line carries a trailing space after its last
 * character and centring counted it, putting the code half a space left of the
 * middle, where a flex row measures only what is actually in it.
 *
 * Empty, the run is the caret alone, so the caret lands on the middle of the box —
 * which is where a centred field puts an insertion point, and the reason this box
 * carries no placeholder: see ui/lobby.js. Neither of the margins below has
 * anything to stand off there, and both would take it off the middle.
 */
.input--code {
	justify-content: center;
}

.input--code .textbox__run {
	font-family: ui-monospace, "SF Mono", Menlo, monospace;
	font-size: var(--step-2);
	letter-spacing: 0.25em;
}

/* The last letter's trailing letter-space would sit between the code and its
   caret, which puts the caret a quarter of a character too far out. There is no
   such space to cancel while the box is empty, where it drags the caret a quarter
   of a character left of the middle instead. */
.input--code:not(.textbox--empty) .textbox__value {
	margin-inline-end: -0.25em;
}

/* The caret stands off the last letter, and an empty box has none to stand off. */
.input--code.textbox--empty .textbox__caret {
	margin-inline-start: 0;
}

/*
 * The cross that empties a field, and the box it sits in.
 *
 * Inside the field rather than beside it: a control alongside would be a second
 * column, and the field is the only thing on its row that wants the width.
 *
 * 1.25rem of mark in a 2.75rem target, which is the touch floor, and a field is
 * 2.75rem tall so the button fills it exactly. --ink-dim rather than --ink,
 * because this undoes what is in the box rather than putting a panel away, and it
 * is beside the text it is about — measured at 5.4:1 on the code field's glass
 * over the lightest the poster gets, and 6.3:1 on the dictionary's own fill.
 *
 * A field whose text could reach it keeps room for it — see .dictionary__field.
 * The room code cannot, and needs none: five monospaced characters measure 109px
 * centred in the 288px field a 320px phone gives it, so a full code stops 45.5px
 * short of the mark — and padding here would only take the code off its own
 * middle.
 */
.field__box,
.dictionary__box {
	position: relative;
	display: grid;
}

.field__clear,
.dictionary__clear {
	position: absolute;
	inset-block: 0;
	right: 0;
	display: grid;
	place-items: center;
	inline-size: 2.75rem;
	padding: 0;
	border-radius: var(--radius);
	color: var(--ink-dim);
}

.field__clear-mark,
.dictionary__clear-mark {
	inline-size: 1.25rem;
	block-size: 1.25rem;
}

/*
 * The pad both of the game's text fields are typed on, which is the game's own
 * and not the phone's. See ui/keys.js for why the platform keyboard is refused
 * on each of them.
 *
 * A QWERTY row is ten keys, and that is what decides the width rather than the
 * 44px floor: at the panel's 288px on a 320px phone a key is 25.2px, and 36.0px
 * on a 428px one. Those are the platform keyboard's own numbers — measured off
 * an iPhone at 428pt, its keys are ~36pt wide — so a thumb is being asked for
 * exactly the accuracy it already gives every day, and ours are 44px tall
 * against the platform's ~39pt. It is the third and last thing in the game that
 * cannot have a 44px target, and like the board square and the rack tile the
 * arithmetic says so rather than the taste.
 *
 * Three rows, not the five an alphabet needs at six columns: 140px against
 * 236px, which is 96px given back to the join screen and the room the word list
 * needs on the dictionary.
 *
 * The keys are the field's own glass, so the pad reads as part of the box it
 * fills rather than as a second kind of furniture on the poster: same fill, same
 * edge, and the same 9.5:1 the code typed into the field is measured at.
 */
.keypad {
	display: grid;
	gap: var(--space-1);
}

.keypad__row {
	display: flex;
	gap: var(--space-1);
}

/*
 * The rows step to the right, and by the amounts a typewriter steps them.
 *
 * A row of nine and a row of seven each leave a key's worth of slack against the
 * row of ten above, and where that slack goes is the whole of whether the block
 * reads as a keyboard. Split evenly it centres each row, which is what a phone's
 * own keyboard does and which comes out as a symmetrical taper — three rows of
 * buttons narrowing to a point, not a keyboard. A typewriter staggers them
 * instead: measured off a standard board in key widths from the left edge of the
 * Q row, A sits at +0.25 and Z at +0.75, so the left edges step right by a little
 * and the right edges are left ragged.
 *
 * So the middle row gives up a quarter of that slack on the left and
 * three-quarters on the right. The figures are exact rather than eyeballed: the
 * slack is a tenth of the row plus a tenth of a gap, which at 4px is 0.4px and is
 * left out, so a quarter of it is 2.5% and three-quarters 7.5%. That holds the
 * nine keys to exactly the width of the ten above them.
 */
.keypad__row:nth-child(2) {
	padding-left: 2.5%;
	padding-right: 7.5%;
}

.keypad__key {
	/* The base button's inline padding would push a 25.2px key past its share of
	   the row. A letter needs none of it. */
	flex: 1 1 0;
	min-inline-size: 0;
	block-size: 2.75rem;
	padding: 0;
	display: grid;
	place-items: center;
	background: rgb(22 58 49 / 0.78);
	box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.32);
	font-family: var(--font-tile);
	font-size: var(--step-0);
	font-weight: 700;
}

/*
 * The bottom row's two ends, carrying the same stagger as the rule above.
 *
 * Seven letters leave three keys of slack, and the typewriter's +0.75 is what
 * decides how it splits: a quarter of a key short of one goes in front of the Z
 * and the remaining two and a quarter behind the M. The empty one is not
 * decoration — it is what puts Z half a key right of A, which is the step the eye
 * reads the block by. Delete takes everything left over rather than a share of
 * it, so the row ends flush like the one at the top: it is the one key here that
 * gets pressed by mistake, a mis-tap wants the bigger target, and a big key at
 * the right of the bottom row is where a typewriter's shift is anyway. 56.7px on
 * a 320px phone against a letter's 25.2px.
 */
.keypad__gap {
	flex: 0.75 1 0;
}

.keypad__key--wide {
	flex: 2.25 1 0;
}

.keypad__key .button__icon {
	inline-size: 1.5rem;
	block-size: 1.5rem;
}

/*
 * The one key that is not a letter and not on a letter row: the dictionary's own
 * action. Full width, because it is what the panel is for.
 *
 * It stands off the letters by more than they stand off each other. At the pad's
 * own 4px it reads as a fourth row of keys — something to be typed rather than
 * the thing that asks the question — and it is the one control here that ends
 * what the other thirty start. The gap is the pad's 4px plus this, so 20px.
 */
.keypad__action {
	inline-size: 100%;
	margin-block-start: var(--space-3);
}
/* --- the chooser, which is our own dropdown --- */

/*
 * A native select's list is drawn by the platform: a grey system menu at whatever
 * size and place the OS likes, landing on top of the control it belongs to. This
 * is the same box as an .input with a list of our own under it, so wearing the
 * .input class is the point — a chooser and a text field beside it must not be
 * two opinions on what a field looks like.
 */
.chooser {
	position: relative;
	display: grid;
	min-width: 0;
}

.chooser__button {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--space-2);
	text-align: left;
}

/* The value has to give way rather than widen the control: a chooser sits in a
   row with a button beside it, on a screen 320px wide. */
.chooser__current {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* A chevron built from two borders of a rotated box: nothing to download, and it
   scales with the text like the rest of the control. */
.chooser__caret {
	flex: none;
	width: 0.45em;
	height: 0.45em;
	border-right: 2px solid var(--accent);
	border-bottom: 2px solid var(--accent);
	transform: translateY(-0.12em) rotate(45deg);
}

.chooser--open .chooser__caret {
	transform: translateY(0.12em) rotate(225deg);
}

.chooser__button:disabled {
	opacity: 0.4;
}

/*
 * The list, over whatever is under the control.
 *
 * Exactly the control's width, which is what keeps it inside the panel: sized to
 * its contents it would have to be measured against the right edge of the screen
 * as well, and there is nothing to gain from that. The height is capped and
 * scrolls because a list taller than the window cannot be reached otherwise —
 * the column it sits in is already as tall as the screen.
 */
.chooser__list {
	position: absolute;
	left: 0;
	right: 0;
	top: calc(100% + var(--space-1));
	z-index: var(--layer-menu);
	max-height: min(50svh, 18rem);
	overflow-y: auto;
	padding: var(--space-1);
	border-radius: var(--radius);
	background: var(--surface-raised);
	/* The border separates it from a panel of the same colour underneath; the
	   shadow is what says it is above rather than in the page. */
	box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.2), var(--shadow-panel);
}

/* Above the control when the list will not fit below it. Which way round is
   measured as it opens, in chooser.js. */
.chooser--above .chooser__list {
	top: auto;
	bottom: calc(100% + var(--space-1));
}

/*
 * The list holds the focus while it is up and shows the keyboard's position on an
 * option instead, which is what aria-activedescendant means. A ring round the
 * whole list as well would be two answers to the same question.
 */
.chooser__list:focus-visible {
	outline: none;
}

.chooser__option {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--space-2);
	/* A row of a list is a tap target like any other. */
	min-height: 2.75rem;
	padding: 0 var(--space-2);
	border-radius: var(--radius);
	cursor: pointer;
}

.chooser__option:hover {
	background: rgb(255 255 255 / 0.06);
}

/* Where the keyboard is. */
.chooser__option--active {
	background: rgb(255 255 255 / 0.08);
	outline: 2px solid var(--accent);
	outline-offset: -2px;
}

/* What is chosen, said twice — the gold the interface says yes in, and a tick —
   so it does not rest on colour alone. */
.chooser__option--chosen {
	color: var(--accent);
	font-weight: 600;
}

.chooser__option--chosen::after {
	content: "\2713";
}

/* --- the header: a card per player, and the bag --- */

.scoreboard {
	display: grid;
	gap: var(--space-2);
}

/*
 * A card each, side by side, rather than a row each. Two players is the common
 * case and they fit; four wrap into a pair of rows without any further rule.
 */
.scoreboard__seats {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr));
	gap: var(--space-2);
}

/*
 * Two rows: who and what they have scored, then what they are doing and how
 * many tiles they hold.
 *
 * min-width: 0 on the card and the name is load-bearing. A grid item's
 * automatic minimum is its min-content width, and a nowrap name never reports
 * anything smaller than itself however it is clipped — two cards side by side
 * then demanded 330px inside a 320px phone and took the whole column with them.
 */
.seat {
	position: relative;
	display: grid;
	grid-template-columns: auto minmax(0, 1fr) auto;
	align-items: center;
	gap: 0 var(--space-2);
	min-width: 0;
	padding: var(--space-2) var(--space-3);
	border-radius: var(--radius);
	background: linear-gradient(180deg, rgb(255 255 255 / 0.05), transparent 60%), var(--surface-raised);
	box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.06);
}

/*
 * Whose turn it is: the card is filled with the interface's gold and printed in
 * dark, rather than ringed or banner-ed. One card in the row going solid is seen
 * without being looked for, at a glance and from across a table, which a 2px
 * edge on a 194px card is not — and it needs no animation to be noticed, so
 * there is none.
 *
 * Filling it means every ink on the card changes with it, and that is not
 * optional: cream --ink is 1.89:1 on the gold and --ink-dim is 1.07:1, so a card
 * that only swapped its background would go blank. The rules below are that
 * swap, and each of them is measured against the flat gold — the lit top of the
 * gradient is the easier end for dark ink, so the flat colour is the floor.
 */
.seat--turn {
	background: linear-gradient(180deg, rgb(255 255 255 / 0.14), transparent 60%), var(--accent);
	color: var(--accent-ink);
	/* The fill is its own edge: gold is 7.1:1 on the table and 5.9:1 against a
	   neighbouring card, and the white hairline the dark cards use to lift
	   themselves is 1.2:1 here. */
	box-shadow: none;
}

.seat--turn .seat__score {
	color: var(--accent-ink);
}

/*
 * The card's second rank — what the seat is doing, and the icon saying what kind
 * of player it is — kept a step back from the name and the score, as --ink-dim
 * keeps them on a dark card.
 */
.seat--turn .seat__state,
.seat--turn .seat__who {
	color: var(--accent-ink-soft);
}

/* Tile marks are the tile's cream on a dark card and would be 1.9:1 here. */
.seat--turn .seat__pip {
	background: var(--accent-ink);
}

/* And the low-tile red the other way up with them, for the same reason. */
.seat--turn .seat__tiles--low .seat__pip {
	background: var(--danger-ink);
}

/*
 * A computer player's rating, with the light and the dark the other way up. Lit
 * against unlit is still the ratio that has to hold, and it comes out at 3.65:1
 * against the dark card's 3.15:1, with the unlit dot 1.95:1 on the gold so there
 * is still something to count the unlit ones by.
 */
.seat--turn .seat__dot {
	background: rgb(0 0 0 / 0.3);
}

.seat--turn .seat__dot--lit {
	background: var(--accent-ink);
}

.seat--resigned {
	opacity: 0.45;
	text-decoration: line-through;
}

/*
 * A seat with nobody connected behind it, faded but not struck through: that
 * player is still in the game and their score still counts, they are simply not
 * there. Lighter than a resignation for the same reason — this one can be undone
 * by their coming back, and usually is.
 *
 * It must not take the gold card away. A seat can be away and to move at once,
 * which is exactly when the table is about to pass for them, and that is the
 * moment the mark matters most.
 */
.seat--away {
	opacity: 0.6;
}

/*
 * A gold card fades less, because it has further to fall. Fading composites the
 * card and everything printed on it towards the dark table, and the two converge
 * from opposite sides: 0.6 leaves a dark card's name at 5.1:1 and its status at
 * 3.3:1, and the same 0.6 on the gold gives 3.5:1 and 2.8:1. 0.8 puts it back at
 * 5.1:1 and 3.7:1 — parity with the dark card — and the card is still 5.0:1
 * against the table where a card at full strength is 7.1:1, so it still reads as
 * faded.
 */
.seat--turn.seat--away {
	opacity: 0.8;
}

/*
 * Where a player stands, on a rosette centred on the corner of their card.
 *
 * The header says what everybody has scored and, without this, nothing about who
 * is ahead. Two numbers answer that on their own; four on a row of 114px cards do
 * not, and it is the four-player table this is for.
 *
 * It is out of flow, which is the whole reason there is room for it: --furniture
 * is a measured budget the board gets what is left of, and an absolutely
 * positioned mark takes none of it.
 *
 * 16px, and the size is arithmetic rather than taste. Centred on the corner it
 * overhangs 8px up and 8px to the left, and 8px is exactly what there is: the gap
 * between two seat cards is --space-2 both ways, so at 16px an inner card's mark
 * sits flush in that gap and touches nothing, and one pixel more is a mark sitting
 * on the neighbour. The cards do wrap — the grid is two tracks at 320px and three
 * from 390px up, so a fourth player is on a second row — which is why the row gap
 * counts as much as the column one. The leftmost card and the top row overhang
 * into `.screen`'s own 16px of padding instead, which `.screen--game`'s
 * `overflow: clip` does not cut, since a clip is at the padding box. 8px into the
 * card clears `.seat__who`, whose 14px column starts at the card's padding edge —
 * 16px, or 8px on the short screen below.
 *
 * Nothing moves. The gold turn card deliberately has no animation, for the reason
 * this has none: a mark this plain is seen without being looked for.
 */
.seat__rosette {
	position: absolute;
	top: 0;
	left: 0;
	display: grid;
	place-items: center;
	width: 16px;
	height: 16px;
	transform: translate(-50%, -50%);
	/* The card underneath opens that player's record, and the mark is over part of
	   it. A caption may not eat a press. */
	pointer-events: none;
}

/*
 * Most tables carry no mark at all for the first turn or two — nobody has scored,
 * so everybody is level — and this has to be said out loud: the display above
 * would otherwise beat the browser's own rule for a hidden element.
 */
.seat__rosette[hidden] {
	display: none;
}

.seat__rosette-shape {
	grid-area: 1 / 1;
	width: 100%;
	height: 100%;
}

/*
 * The ring is not decoration. First place is the interface's own gold and lands on
 * the gold card whenever the leader is to move, which is most of the time anybody
 * is looking at it — so on that card the ring is the disc's only edge, at 7.91:1.
 * It follows the notch as well as the disc, because a ribbon that is drawn round
 * three sides of itself is a disc with a bite out of it.
 */
.seat__rosette-shape path {
	fill: var(--rosette-fill);
	stroke: var(--surface-sunk);
	stroke-width: 2.5;
	stroke-linejoin: round;
}

.seat__rosette--1 { --rosette-fill: var(--accent); }
.seat__rosette--2 { --rosette-fill: var(--rosette-silver); }
.seat__rosette--3 { --rosette-fill: var(--rosette-bronze); }
.seat__rosette--4 { --rosette-fill: var(--rosette-lead); }

/*
 * The place itself, and it is what carries the rank — the four metals reinforce it
 * and cannot replace it, since bronze and lead are one colour in greyscale. See
 * the tokens for why they cannot be pulled apart.
 *
 * The grotesque and the 0.5625rem are the board's own score badge, which is the
 * other place the game prints a numeral this small and is the measured floor for
 * one. The serif every other figure on this card is set in does not survive 9px.
 *
 * Nudged up off the disc's middle, because the notch takes the foot of the disc
 * and a digit centred on the circle reads low in what is left of it. What it is
 * centred in is the fill above the notch rather than the box, and the 1.5px is
 * measured against the glyph's own ink rather than its line box — a line box is
 * the font's business and is nothing like the mark that gets painted. At 0.5px
 * the digit had 2.41px of fill over it and 0.33px under, which on a phone reads
 * as a numeral sitting on the apex; at 1.5px it is 1.41 and 1.33, and at 2.5px it
 * is 0.41 and 2.33 the other way about.
 */
.seat__place {
	grid-area: 1 / 1;
	transform: translateY(-1.5px);
	font-family: var(--font-tile);
	font-size: 0.5625rem;
	font-weight: 700;
	line-height: 1;
	color: var(--surface-sunk);
}

/*
 * What kind of player the seat is, down the left of the card: a person, or a
 * computer with its rating under it.
 *
 * A fixed width rather than the content's, so a card with a rating under the
 * icon and a card without are the same shape and every name in the row starts at
 * the same place. 14px is the icon's own width and the most this may take: the
 * column and its gap come out of the name, and on a 320px phone your own card
 * has 90px for the 82px "Mark (you)" wants.
 *
 * Nothing in here may grow past the two text rows beside it, which are 36px on
 * that phone, or the card gets taller and --furniture — the height budget the
 * board gets what is left of — moves. The icon and a three-dot rating come to
 * 19px.
 */
.seat__who {
	grid-row: 1 / 3;
	grid-column: 1;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 3px;
	width: 14px;
	color: var(--ink-dim);
}

/*
 * The icons are drawn here rather than fetched or set in a font: two shapes at
 * 14px need no file, no request and no glyph whose metrics are the platform's to
 * choose. Each is two blocks in currentColor — a head over shoulders, a screen
 * over a base — because a solid silhouette survives 14px where an outline turns
 * to mush.
 */
.seat__icon {
	position: relative;
	flex: 0 0 auto;
	width: 14px;
	height: 12px;
}

.seat__icon::before,
.seat__icon::after {
	content: "";
	position: absolute;
	background: currentColor;
}

.seat__icon--person::before {
	top: 0;
	left: 4px;
	width: 6px;
	height: 6px;
	border-radius: 50%;
}

.seat__icon--person::after {
	bottom: 0;
	left: 1px;
	width: 12px;
	height: 6px;
	border-radius: 6px 6px 1px 1px;
}

.seat__icon--computer::before {
	top: 1px;
	left: 1.5px;
	width: 11px;
	height: 8px;
	border-radius: 1px;
}

.seat__icon--computer::after {
	bottom: 0;
	left: 0;
	width: 14px;
	height: 2.5px;
	border-radius: 1px;
}

.seat__name {
	grid-area: 1 / 2;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	font-weight: 600;
}

/*
 * The score is the card's headline: the game's own face, and big.
 *
 * Centred in its column, as the tile marks under it are, so the two stack about
 * a shared middle whichever of them is the wider — the column is as wide as the
 * larger, and a score left in the corner of it sits off to one side of the marks.
 */
.seat__score {
	grid-area: 1 / 3;
	text-align: center;
	font-family: var(--font-game);
	font-size: var(--step-2);
	font-variant-numeric: tabular-nums;
	font-weight: 700;
	line-height: 1;
	color: var(--ink);
}

/*
 * What the seat is doing: thinking, waiting, resigned.
 *
 * It must stay clipped rather than allowed to report its own width: a nowrap
 * line in a grid item sets that item's minimum, which is how two cards once
 * demanded 330px inside a 320px phone.
 */
.seat__state {
	grid-area: 2 / 2;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	font-size: 0.6875rem;
	text-transform: uppercase;
	letter-spacing: 0.09em;
	color: var(--ink-dim);
}


/*
 * How hard a computer plays, under its icon: one dot per level, lit from the
 * left. Three dots are 14px across and 4px tall, which is the column exactly.
 *
 * Round, because the other row of small marks on a card is the tile count under
 * the score — 4px squares, a card's width away.
 * Two rows of marks that differ only in how many there are of them are one row
 * read twice, and a rating is not a count of anything the player holds. Shape is
 * the whole of what separates them at this size, so a dot must stay a circle and
 * a tile mark must stay square.
 *
 * The dots must stay the same size as each other. A set that steps up is a
 * signal-strength meter, which is a reading anybody who has held a phone has
 * already learnt, and it says how well something is connected — not how hard it
 * plays. How many are lit is the whole message.
 *
 * 4px is the largest the column allows — three of them and two 1px gaps come to
 * the 14px the icon sets — and the size is what a dot needs, since a circle
 * losing a px of diameter loses two of area. The 1px gap is why they may be this
 * big: circles meet at a tangent, so the white between them reads wider than the
 * same gap between the square marks under the score.
 *
 * Gold, because a seat card is on the dark table and not on the board — the
 * violet the board's own affordances need does not apply here.
 */
.seat__level {
	display: flex;
	align-items: center;
	gap: 1px;
}

.seat__dot {
	flex: 0 0 auto;
	width: 4px;
	height: 4px;
	border-radius: 50%;
	background: rgb(255 255 255 / 0.2);
}

/*
 * Lit against unlit is what a player counts, so that ratio is the one that has
 * to hold: 3.15:1, with the lit dot 5.85:1 against the card and the unlit one
 * 1.85:1 — enough to show how many dots there are to fill. Raising the unlit
 * alpha buys the unlit dot contrast and spends it on the count.
 */
.seat__dot--lit {
	background: var(--accent);
}

/*
 * How many tiles an opponent holds: one mark per tile, in the second row of the
 * card's right-hand column, centred under the score.
 *
 * Every size here is in px on purpose, and the marks are boxes rather than
 * characters: a character brings its font's advance width with it, and the fonts
 * disagree wildly at this size. `▪` is 4.6px a tile in Chrome and 15.5px on iOS,
 * which is 97px of a 194px card and leaves the status line beside it 57px for
 * the 61px "THINKING" needs. Seven marks are 33px on any engine — 7 × 3px and
 * six 2px gaps — and that line gets 121px.
 *
 * A mark is square, with a corner off it, because it stands for a tile and a
 * tile is square — anything much taller than it is wide reads as a dot or a bar
 * instead. Square is also what the width will pay for: height is free, since the
 * row is as tall as the status line beside it whatever the marks do, but every
 * px of width comes out of that line. 4px is what fits. Seven 4px marks and six
 * 1.5px gaps are 37px, which on a 320px phone leaves 65px for the 57.8px
 * "RESIGNED" — the longest status there is — measures. At 5px a mark they are
 * 44px and it has 58px, which is the same number twice.
 *
 * Cream is the tile's own colour and 11.1:1 on the card, where the dim ink the
 * status line uses is 6.3:1 — the marks are the smallest thing on a card and
 * must not also be the faintest.
 */
.seat__tiles {
	grid-area: 2 / 3;
	display: flex;
	justify-content: center;
	align-items: center;
	gap: 1.5px;
	overflow: hidden;
}

/* Your own card has no tile count: you can see your rack. */
.seat__tiles:empty {
	display: none;
}

.seat__pip {
	flex: 0 0 auto;
	width: 4px;
	height: 4px;
	border-radius: 1px;
	background: var(--tile);
}

/*
 * A player down to their last few tiles turns their marks red. See LOW_TILES in
 * ui/scoreboard.js for when, and animations.css for the pulse that goes with it.
 *
 * The colour rather than the pulse is what carries this to a player who has
 * asked for stillness, which is why it is here and not in there — and it is not
 * the only thing carrying it to anybody, since the count itself is the
 * information and is read out in words.
 *
 * It is a step lighter than the red the rest of the dark table warns in: see
 * --danger-light. The gold card takes the swap every ink on it takes, and there
 * it is the darkest red there is rather than the lightest, because a mark on
 * gold has to be dark to be seen at all — --danger-ink is 7.9:1 there against the
 * status line's 4.7:1, where the board's own red would be 3.3:1 and the table's
 * 1.2:1. What that costs is how plainly it reads as red on that one card: it is a
 * shift of 18 in dE from the dark ink the marks wear there, against 35 on a dark
 * card, so it reads as darker and redder rather than as red.
 */
.seat__tiles--low .seat__pip {
	background: var(--danger-light);
}

/*
 * The bag and the way out share a line.
 *
 * Three columns, not two, and the first one empty: the bag is centred under the
 * cards and stays centred whether or not there is a button beside it. A flex row
 * would swing it half a button's width to the left the moment the game starts.
 */
.scoreboard__footer {
	display: grid;
	grid-template-columns: 1fr auto 1fr;
	align-items: center;
	gap: var(--space-2);
}

/*
 * The bag, as a piece of game information rather than a stray line of text: the
 * count, and a ring that empties as the game goes on.
 */
.scoreboard__bag {
	grid-column: 2;
	display: flex;
	align-items: center;
	gap: var(--space-2);
	width: fit-content;
	margin-inline: auto;
	padding: var(--space-1) var(--space-3);
	border-radius: 999px;
	background: var(--surface-sunk);
	box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.06);
	font-size: 0.75rem;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--ink-dim);
}

.scoreboard__bag::before {
	content: "";
	inline-size: 0.85rem;
	block-size: 0.85rem;
	border-radius: 50%;
	background: conic-gradient(var(--accent) calc(var(--bag-fill, 1) * 360deg), rgb(255 255 255 / 0.15) 0);
	/* A ring, not a pie: the hole is what makes it read as a gauge. */
	-webkit-mask: radial-gradient(circle, transparent 42%, black 45%);
	mask: radial-gradient(circle, transparent 42%, black 45%);
}

/*
 * The mark saying every word is being accepted, in the column the bag's centring
 * leaves empty. It takes the bag's own pill so the line still reads as one row of
 * game information, and gold rather than the exit's red: this is a statement of
 * what the table is, not a way out of it, and two reds in a three-column row read
 * as two warnings. Gold is 5.9:1 on the table — the ban on it is only for things
 * drawn on the board, where it disappears into cream and wood.
 *
 * One short word, because the column is the exit's 79px on a 320px phone. What it
 * means is on the element, for a screen reader.
 */
.scoreboard__test {
	grid-column: 1;
	justify-self: start;
	padding: var(--space-1) var(--space-2);
	border-radius: 999px;
	box-shadow: inset 0 0 0 1px var(--accent);
	font-size: 0.6875rem;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--accent);
}

/*
 * Keeps the 44px height a thumb needs and gives back the width, which is the
 * scarce one: a 320px phone leaves 79px beside the bag, and a button with the
 * full padding at the interface's normal size wants 80px of it. Trimmed, it
 * measures 62x44 and clears the bag by 8px on that phone. Whatever this says has
 * to fit that, which is why it is one word.
 *
 * Red, because this is the one control on the screen that ends what you are in:
 * the room before the game starts, and your part in it afterwards.
 */
.scoreboard__exit {
	grid-column: 3;
	justify-self: end;
	padding-inline: var(--space-2);
	font-size: 0.875rem;
	color: var(--danger);
}

/*
 * Short screens give the height back to the board. This has to live after the
 * rules it trims, not in layout.css beside the height it saves: a media query
 * adds no specificity, so a later plain rule would simply win.
 */
@media (max-height: 40rem) {
	/*
	 * A tighter gap between the card's three columns as well as less padding,
	 * because this is the phone the card is tightest on and both of the things
	 * that can clip are in the middle column. At 8px a gap, a 320px phone gives
	 * your own name 82px for the 82.4px "Mark (you)" measures, and a bot's status
	 * line 57px for the 57.8px "RESIGNED" measures — both short by half a pixel.
	 * At 4px they get 90px and 65px, so each has about 7px in hand.
	 */
	.seat {
		padding: var(--space-1) var(--space-2);
		column-gap: var(--space-1);
	}

	.seat__score {
		font-size: var(--step-1);
	}

	.seat__state {
		font-size: 0.625rem;
	}

	.scoreboard__bag {
		padding-block: 0;
	}

	/*
	 * The footer is a 44px control row here rather than a line of text, and it
	 * separates itself from the cards without help. The 4px this returns is 4px
	 * the board keeps, which at this size is most of a square.
	 */
	.scoreboard {
		gap: var(--space-1);
	}
}

/* --- the door --- */

.admission {
	display: grid;
	gap: var(--space-3);
	padding: var(--space-3);
	border-radius: var(--radius-lg);
	background: var(--surface-raised);
}

.admission__label {
	font-size: 0.75rem;
	text-transform: uppercase;
	letter-spacing: 0.1em;
	color: var(--ink-dim);
}

/* The tiles carry their own shadow, so the label and the line under them need
   air or they sit on the edges of it. */
/*
 * The code is the point of this panel, so it is centred and the label and the
 * line under it are centred with it — the tiles alone, under a label ranged
 * left, read as a mistake. The tiles carry their own shadow and need the air.
 */
.admission__code {
	display: grid;
	justify-items: center;
	gap: var(--space-2);
	text-align: center;
}

/*
 * The room code, printed on tiles. It is the first thing a player sees of the
 * game, and it is the game's own alphabet, so it is set in the game's own type.
 *
 * The tile size is worked out rather than guessed at, the way the rack's is: five
 * characters, four gaps of --space-1, the screen's padding and the panel's, which
 * comes to 5rem of furniture. That leaves 48px a tile on a 320px phone, so the
 * cap is what decides it at every width. Capped at 2.75rem, past which a code
 * that cannot be picked up is shouting.
 *
 * --rack-tile is what the tile face and its letter are built from, so overriding
 * that one property here sizes the whole thing.
 */
.admission__tiles {
	--rack-tile: clamp(1.75rem, (100vw - 5rem) / 5, 2.75rem);
	display: flex;
	gap: var(--space-1);
}

.admission__help,
.admission__empty {
	font-size: 0.875rem;
	color: var(--ink-dim);
}

.admission__seated {
	display: grid;
	gap: var(--space-1);
}

.admission__players {
	display: grid;
	gap: var(--space-1);
}

/*
 * A name and, for a computer player, what it plays like. minmax(0, auto) on the
 * name is load-bearing: a nowrap name reports its own width as the column's
 * minimum, and a twenty-character nickname beside a badge would otherwise push
 * the panel wider than a 320px phone.
 */
.admission__player {
	display: grid;
	grid-template-columns: minmax(0, auto) auto;
	justify-content: start;
	align-items: center;
	gap: var(--space-2);
	font-size: 0.875rem;
}

.admission__badge {
	padding: 0 var(--space-2);
	border-radius: 999px;
	background: var(--surface-sunk);
	box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
	font-size: 0.6875rem;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	white-space: nowrap;
	color: var(--ink-dim);
}

.admission__waiting {
	display: grid;
	gap: var(--space-2);
}

.admission__knock {
	display: grid;
	grid-template-columns: 1fr auto auto;
	align-items: center;
	gap: var(--space-2);
}

.admission__name {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	font-weight: 600;
}

.admission__controls {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--space-2);
}

/*
 * The three difficulties, side by side under one heading.
 *
 * They are one choice asked three ways, so they read as a set: three equal
 * columns, and what a press does is said once above them rather than three times
 * in them. Three whole sentences cannot sit in a row at any width the game runs
 * at — "Add an intermediate computer player" alone measures 281.4px, so the set
 * wants 900px of panel against the 256px a 320px phone gives it and the 992px of
 * the widest screen there is.
 */
.admission__bots {
	flex: 1 0 100%;
	display: grid;
	gap: var(--space-1);
}

/*
 * Three equal columns at every width, because they are three equal choices:
 * nothing about intermediate is worth more of the row than expert.
 *
 * What has to fit a column is the longest of the three words, and the narrowest
 * screen is what decides the type it is set in. A column is a third of the panel:
 * 80px on a 320px phone, 93.3px at 360px, 103.3px at 390px and 120px at 440px.
 * `Intermediate` is 97px at the button's ordinary 1rem, which fits none of the
 * phones. At 0.75rem it is 75.5px, which leaves 17.8px in a 360px phone's column
 * and 4.5px in the 320px phone's — too little to trust to a font this game does
 * not ship, since `system-ui` is whatever the phone has and a word that outgrows
 * its button here spills into the gap beside it. So the floor takes the step
 * below, at which it is 69.2px with 10.8px to spare, and that is the whole of the
 * difference: one step of type on the one screen whose column is 13px narrower
 * than the next one up. Stepping it back up again above 360px would buy a single
 * pixel and cost two sizes of the same button on two phones a player might hold
 * side by side.
 *
 * The floor is a 44px button 80px wide with one familiar word in it, which is a
 * tab bar's shape and is read the same way. What a press does is said once above
 * the row, and each button keeps the whole sentence in its label for anything
 * that speaks rather than shows it.
 *
 * minmax(0, 1fr), not 1fr: a button's own text is its minimum width, and three
 * of those would push the panel wider than the phone rather than sit inside it.
 */
.admission__levels {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: var(--space-2);
}

.admission__bot {
	/* No padding of its own. The word is centred in a column that is already
	   wider than it, so the air either side is the column's — a padding here only
	   takes room off the longest of the three, which is what has to fit. */
	padding: 0;
	font-size: 0.6875rem;
	/* One word, and never two lines in a 44px button: what does not fit is
	   answered by the size, which is measured against the narrowest column. */
	white-space: nowrap;
}

@media (min-width: 22.5rem) {
	.admission__bot {
		font-size: 0.75rem;
	}
}

/*
 * Starting the game is a row of its own, whatever the width. It is the one thing
 * on this screen that cannot be undone — every other control here adds a player
 * or changes a setting — so it does not sit at the end of a line a host is
 * scanning across, next to the button that adds another computer player. A basis
 * of the whole line is what breaks it out: the controls before it keep their
 * line, and this takes the next one entire.
 */
.admission__start {
	flex: 1 0 100%;
}

/* --- the blank sheet --- */

.sheet {
	position: fixed;
	inset: 0;
	z-index: var(--layer-sheet);
	display: grid;
	align-items: end;
	background: rgb(0 0 0 / 0.6);
}

.sheet__panel {
	display: grid;
	gap: var(--space-3);
	padding: var(--space-4) var(--space-3);
	padding-bottom: max(var(--space-4), env(safe-area-inset-bottom));
	border-radius: var(--radius-lg) var(--radius-lg) 0 0;
	background: var(--surface-raised);
}

.sheet__title {
	font-family: var(--font-game);
	font-size: var(--step-1);
	text-align: center;
}

/* What an action will cost, under the question asking about it. Quieter than
   the title but not faint: it is the whole reason the sheet is up. */
.sheet__detail {
	margin: calc(var(--space-3) * -0.5) 0 0;
	color: var(--ink-dim);
	font-size: 0.875rem;
	line-height: 1.4;
	text-align: center;
	text-wrap: pretty;
}

/*
 * What a word means.
 *
 * The word carries the bubble, so it takes a title's face and size rather than a
 * title being printed over the top of it: "What this word means" above CHIN is a
 * heading saying what the next line is about, on a panel with only one line on
 * it — and the bubble is already named that for a screen reader. Left-aligned,
 * unlike the sheets that ask something, because this is a thing to read rather
 * than a row of choices to aim at.
 */
.meaning {
	display: grid;
	gap: var(--space-1);
	margin: 0;
}

.meaning__word {
	font-family: var(--font-game);
	font-size: var(--step-1);
	letter-spacing: 0.02em;
}

/* A step back from the word, as --ink-dim is everywhere a second line explains
   the first. A definition runs to a couple of lines on a 320px phone, so it
   wraps rather than being held to one. */
.meaning__sense {
	margin: 0 0 var(--space-2);
	color: var(--ink-dim);
	font-size: 0.875rem;
	line-height: 1.4;
	text-wrap: pretty;
}

.meaning__sense:last-of-type {
	margin-bottom: 0;
}

/* --- the call-out --- */

/*
 * The layer a bubble lives in, and the box it has to stay inside.
 *
 * Its own rect is what ui/callout.js clamps against, which is the whole reason it
 * has one: the safe areas and the margin off the edge are declared here, once,
 * and nothing in the script has to know what a home indicator costs. It takes no
 * pointer events, because it covers the board and the only thing on it that
 * answers is the bubble.
 */
.callout-layer {
	position: fixed;
	inset-inline: var(--space-2);
	top: max(var(--space-2), env(safe-area-inset-top));
	bottom: max(var(--space-2), env(safe-area-inset-bottom));
	z-index: var(--layer-callout);
	pointer-events: none;
}

/*
 * The bubble itself: what was pressed, answered beside the thing that was
 * pressed.
 *
 * Fixed, so the numbers the script works out are the numbers it is placed at, and
 * so it is not clipped by the board's frame or by the report's own scroll. It
 * follows that nothing between it and the window may take a transform, a filter
 * or containment — that would make the layer the containing block and every
 * coordinate wrong by the layer's own offset.
 *
 * 20rem wide, which is 320px: the widest phone there is takes it with room either
 * side, and the narrowest gives it the whole column less the margin, at 304px.
 * Two lines of a definition is what it is sized for, and a long one scrolls
 * inside it rather than growing past the window — --callout-most is the layer's
 * measured height, written before the bubble is measured.
 *
 * A pale ring and a shadow, as the toast has, and for the same reason: it lands
 * on the green field, where --surface-raised is 1.01:1 and no fill in this
 * palette stands off. The ring is 3:1 or better against all three fills and 6:1
 * against the table.
 */
.callout {
	--callout-ring: color-mix(in srgb, var(--ink) 60%, transparent);

	position: fixed;
	left: var(--callout-left, 0);
	top: var(--callout-top, 0);
	width: min(20rem, calc(100vw - 2 * var(--space-2)));
	max-height: var(--callout-most, 100svh);
	display: grid;
	grid-template-columns: minmax(0, 1fr) 2.75rem;
	border-radius: var(--radius-lg);
	background: var(--surface-raised);
	box-shadow:
		inset 0 0 0 2px var(--callout-ring),
		0 0.75rem 1.5rem rgb(0 0 0 / 0.45);
	font-size: 0.875rem;
	pointer-events: auto;
}

/* The scroller, and the padding, so that a bubble taller than the window gives
   way inside itself and the ring around it holds still. Nothing at the far end:
   the cross's own 2.75rem box is the air on that side. */
.callout__body {
	min-height: 0;
	padding: var(--space-3);
	padding-inline-end: 0;
	overflow-y: auto;
	overscroll-behavior: contain;
	-webkit-overflow-scrolling: touch;
}

/*
 * The tail, which is the whole difference between this and a dialog.
 *
 * Buried 2px in the bubble, which is the thickness of the ring: an inset shadow
 * paints under an element's children, so the ring line the tail crosses is
 * covered by the tail's own fill and the two read as one shape. How far along it
 * sits is the script's, from where the anchor's middle came out.
 */
.callout__tail {
	position: absolute;
	left: var(--callout-tail, 50%);
	inline-size: 22px;
	block-size: 12px;
	margin-inline-start: -11px;
	fill: var(--surface-raised);
	stroke: var(--callout-ring);
	stroke-width: 2;
	stroke-linejoin: round;
}

.callout--above .callout__tail { top: calc(100% - 2px); }

.callout--below .callout__tail {
	bottom: calc(100% - 2px);
	transform: rotate(180deg);
}

/* Clamped away from what it was pointing at — a short window with no room either
   side of the anchor — and a tail that points at the wrong thing is worse than
   none. */
.callout--blunt .callout__tail { display: none; }

.sheet__grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(2.75rem, 1fr));
	gap: var(--space-2);
}

/* Whatever a sheet asks, it asks with tiles: the letter a blank is about to
   become, or the tiles about to go back in the bag. */
.sheet__option,
.sheet__tile {
	min-height: 2.75rem;
	border-radius: 3px;
	background: linear-gradient(160deg, var(--tile-light) 0%, var(--tile) 48%, var(--tile-deep) 100%);
	box-shadow:
		inset 0 0 0 1px var(--tile-edge),
		inset 0 2px 0 rgb(255 255 255 / 0.6),
		0 2px 4px rgb(0 0 0 / 0.4);
	color: var(--tile-ink);
	font-family: var(--font-tile);
	font-size: var(--step-1);
	font-weight: 700;
}

/*
 * The letter a blank already stands for, when it is being asked again. Ringed in
 * gold like any other chosen tile on the dark panel, so the sheet says what the
 * answer is now rather than only offering 26 alternatives.
 */
.sheet__option--current {
	box-shadow:
		inset 0 0 0 1px var(--tile-edge),
		inset 0 2px 0 rgb(255 255 255 / 0.7),
		0 0 0 3px var(--accent),
		0 8px 12px rgb(0 0 0 / 0.5);
}

/*
 * A tile offered for swapping, which shows its value: what a player is weighing
 * up is what they can afford to lose.
 */
.sheet__tile {
	position: relative;
	display: flex;
	align-items: center;
	justify-content: center;
	aspect-ratio: 1;
}

.sheet__tile .tile__letter {
	font-family: var(--font-tile);
	font-weight: 700;
	line-height: 1;
}

.sheet__tile .tile__value {
	position: absolute;
	right: 0.2rem;
	bottom: 0.1rem;
	font-family: var(--font-tile);
	font-size: 0.625rem;
	font-weight: 600;
	line-height: 1;
	opacity: 0.7;
}

/*
 * Chosen for the bag: ringed, as a chosen tile is anywhere else in the game —
 * in gold rather than the board's violet, because this ring is drawn on the dark
 * panel, where the violet all but disappears. Not faded: a dimmed tile reads as
 * one that cannot be picked, and the letter and its value are the whole of what a
 * player is weighing up.
 */
.sheet__tile--chosen {
	box-shadow:
		inset 0 0 0 1px var(--tile-edge),
		inset 0 2px 0 rgb(255 255 255 / 0.7),
		0 0 0 3px var(--accent),
		0 8px 12px rgb(0 0 0 / 0.5);
}

/*
 * Swapping is the action and gets a row to itself; choosing all and backing out
 * share the row beneath. Three buttons of equal weight do not fit across a
 * phone, and "Swap 2 tiles" has to have the width to sit on one line.
 */
.sheet__actions {
	display: grid;
	gap: var(--space-2);
}

.sheet__minor {
	display: flex;
	gap: var(--space-2);
}

.sheet__minor .button {
	flex: 1 1 0;
}

@media (min-width: 30rem) {
	.sheet { align-items: center; justify-items: center; }
	.sheet__panel { width: min(100%, 26rem); border-radius: var(--radius-lg); }
}

/* --- the dictionary --- */

/*
 * The one panel in the game with a text field on it, and that is what decides
 * its box.
 *
 * It takes the whole window rather than sitting at the foot of it like every
 * other sheet, because two things want the room: 127 two-letter words, and the
 * pad that takes their place while somebody is typing. No phone keyboard ever
 * appears over it — see ui/keys.js — so what is on the screen is all ours and
 * every part of it can be measured.
 *
 * No radius, because a full-height card's rounded corners are corners cut out of
 * the screen. From 30rem up the sheet centres its panel and caps its width, so
 * this becomes a tall card with the sheet's own radius and room round it.
 *
 * The reading half is the only row that flexes, so the field, its answer and the
 * tiles hold still while a hundred and twenty-seven words go past: they are what
 * the list is being read against. The pad is the row after it and is sized by its
 * own keys, so hiding the list leaves the `1fr` empty and parks the pad at the
 * foot of the panel, where a keyboard belongs. The height has to be the window's
 * in svh and not a stretch or a percentage, because the sheet's own grid row is
 * auto-sized: given 127 words at the 44px they need to be pressable, the row
 * grows to 1453px and takes the panel 896px below the fold with it — measured, on
 * a 320px phone, which is how this was found. A definite height is what makes the
 * `1fr` a share of something rather than of its own contents.
 */
.dictionary {
	block-size: 100svh;
	grid-template-rows: auto auto auto minmax(0, 1fr) auto;
	padding-top: max(var(--space-3), env(safe-area-inset-top));
	border-radius: 0;
}

/*
 * The tabs and the list together: what there is to read when nobody is typing.
 * One element because they are one thing, and because the pad takes the room of
 * both — the tabs are no use while the list they switch is hidden.
 */
.dictionary__browse {
	grid-row: 4;
	display: grid;
	grid-template-rows: auto minmax(0, 1fr);
	gap: var(--space-3);
	min-block-size: 0;
}

/*
 * Both rows are named, and that is not tidiness — it is the whole of what makes
 * the swap work. A hidden element is no longer a grid item at all, so left to
 * auto-placement the pad slides up into the `1fr` the list had and stretches to
 * fill it: measured before this rule, a 44px key row came out 425px tall on a
 * 390px phone. Named, the `1fr` is simply left empty and the pad keeps its own
 * height at the foot of the panel.
 */
/*
 * The padding and the negative margin that cancels it are the pad's buffer: a
 * band round the keys that belongs to the pad and answers to nothing. A thumb
 * reaching for the top row and coming up short lands in the void above it, and
 * without this that void is "somewhere else on the panel" and takes the pad away
 * mid-word. The two must stay equal and opposite — the padding grows the box the
 * panel tests a press against, the margin puts the keys back where they were — so
 * nothing moves and the `auto` track is still the pad's own 208px.
 *
 * It reaches the panel's own edges sideways and below and 24px above the keys,
 * which is all the room there is to take: above is the empty `1fr`, below and
 * either side is the panel's padding, and there is no control anywhere in it.
 * That is why this is the dictionary's alone — on the join screen the same pad
 * has "Ask to join" directly under it, and a buffer there would swallow presses
 * meant for the one button that screen exists for.
 */
.dictionary .keypad {
	grid-row: 5;
	padding: var(--space-4) var(--space-3);
	margin: calc(var(--space-4) * -1) calc(var(--space-3) * -1);
}

@media (min-width: 30rem) {
	.dictionary {
		block-size: min(100svh, 44rem);
		border-radius: var(--radius-lg);
	}

	/*
	 * The one width at which the copy is not the same size as the tile in the
	 * trough: the panel is capped at the sheet's 26rem here while the window is
	 * not, so seven of the window's own rack tiles no longer fit — 410px of them
	 * in 384px of panel on a 1280px desktop. Taken off the panel's width instead,
	 * less its padding and the six gaps.
	 */
	.dictionary__tiles {
		--rack-tile: calc((26rem - 3.125rem) / 7);
	}
}

.dictionary__head {
	display: flex;
	align-items: center;
	gap: var(--space-2);
}

.dictionary__title {
	flex: 1 1 auto;
	min-width: 0;
	margin: 0;
	font-family: var(--font-game);
	font-size: var(--step-1);
}

.dictionary__search {
	display: grid;
	gap: var(--space-1);
}

/* The box the field and the cross that empties it share, and the cross itself,
   are the field's own — see .field__clear. */
/* The letters are the box's own, not the hint's: the hint is a sentence and is
   spared the case a word is typed in, which .textbox__hint already declares —
   "TYPE A WORD" in a box reads as its label rather than as an invitation. */
.dictionary__field .textbox__run {
	text-transform: uppercase;
	letter-spacing: 0.04em;
}



/*
 * Room for the cross is kept only while the cross is there, so a fifteen-letter
 * word does not run underneath it and an empty box still has the whole of its
 * width for the hint — which needs it: the sentence measures 246px against the
 * 234px a padded box leaves on a 320px phone, and 270px without the padding.
 * `textbox--empty` is what the box wears when it holds nothing, which is exactly
 * when the cross is hidden, so the two cannot drift out of step. Nothing moves as
 * the first letter is typed: the text is left-aligned, so what changes is where
 * the box ends, at the moment the cross appears to fill it.
 */
.dictionary__field:not(.textbox--empty) {
	padding-right: 2.75rem;
}

/*
 * The answer, sunk into the panel.
 *
 * Sunk rather than printed straight onto it, and that is measured rather than
 * chosen: --good is 4.3:1 on the raised fill this panel is and 5.8:1 on the sunk
 * one, and the verdict is the whole reason the panel is up.
 *
 * The room is held whether or not there is anything in it, and only the fill
 * comes and goes. Everything below the answer is furniture a player is reading —
 * their own seven tiles, the two tabs, and the word list — and a panel that
 * shunts all of it down a row the first time somebody asks about a word is one
 * that moves under the thumb aiming at it. Holding the room costs the word list
 * 46px, which is measured rather than guessed: at the 320px floor it takes the
 * list from 279px to 233px against the 233.3px the two-letter words need, so that
 * phone sits 0.3px over at rest instead of only once a verdict is up. It is a
 * scroll container, so 0.3px is not reachable and nothing else on the sweep
 * moves.
 *
 * An empty sunk rectangle under the field still reads as a panel half built, so
 * an empty box paints nothing at all: the room is kept, the fill is not.
 *
 * The height is fixed at one line of the verdict, and that is not the same as one
 * line of whatever is in it: "Looking up GADJOS…" is a note, a step smaller than
 * the verdict it is about to be replaced by, so a box that took its content's
 * height shrank while the request was in flight and grew back when it answered —
 * pulling the tiles, the tabs and the whole word list up the panel and down again,
 * twice per lookup. The arithmetic is the verdict's own size and line-height,
 * which is why both are declared here rather than left to be inherited. A
 * fifteen-letter word still wraps it to two lines; that is a floor, not a cap.
 */
.dictionary__answer {
	min-height: calc(var(--step-1) * 1.3 + var(--space-2) * 2);
	display: grid;
	align-content: center;
	gap: var(--space-1);
	padding: var(--space-2);
	border-radius: var(--radius);
	background: var(--surface-sunk);
}

.dictionary__answer:empty {
	background: none;
}

.dictionary__verdict {
	margin: 0;
	font-family: var(--font-game);
	font-size: var(--step-1);
	/* Said, because the box above is sized off it. */
	line-height: 1.3;
}

.dictionary__verdict--good { color: var(--good); }
.dictionary__verdict--bad { color: var(--danger); }

.dictionary__note {
	margin: 0;
	color: var(--ink-dim);
	font-size: 0.875rem;
	line-height: 1.4;
	text-wrap: pretty;
}

/*
 * The tiles in hand: the trough's own tiles, at the trough's own size and spacing.
 *
 * The face is rack.css's one declaration rather than a copy of it — see the note
 * there. What a player is weighing up is the letters and what each is worth, and
 * a copy of their rack that is not quite the same tile is a second rack. Values
 * shown for the swap sheet's reason. A copy to read and not a control, so these
 * are spans and take no press: the rack itself is where a tile is moved.
 */
.dictionary__tiles {
	display: flex;
	justify-content: center;
	gap: var(--rack-gap);
}

/* Two tabs in equal halves, so neither reads as the main one: the words are what
   this panel is for and the moves are what the table has done, and a player
   arrives wanting either. */
.dictionary__tabs {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: var(--space-2);
}

.dictionary__tab {
	min-height: 2.75rem;
	padding: 0 var(--space-2);
	border-radius: var(--radius);
	background: var(--surface-sunk);
	color: var(--ink-dim);
	font-size: 0.875rem;
	font-weight: 600;
}

/*
 * The one showing, said twice — filled gold and printed dark — for the reason the
 * turn card is: a 2px edge is a detail to be looked for and a filled tab is seen
 * at a glance. The ink has to turn over with it, since cream is 1.89:1 on the
 * gold and would go blank.
 */
.dictionary__tab--chosen {
	background: var(--accent);
	color: var(--accent-ink);
}

.dictionary__list {
	overflow-y: auto;
	padding-right: var(--space-1);
}

/*
 * The two-letter words: all 127 at once, in one run, and nothing to scroll.
 *
 * Words and not tiles, and not controls. Every target in the game keeps the 44px
 * floor, and 127 of those are the best part of a thousand pixels — three screens
 * deep on the phone this is measured on. As text they fit the panel whole, and
 * what is given up is a press for what one means: the field two rows above
 * answers exactly that, and the word is two letters to type.
 *
 * One run and not grouped by first letter, which was tried. Keeping a group whole
 * means breaking the line wherever the next one will not fit, and 25 groups in a
 * column this narrow is a block of short ragged lines with a two-word line under
 * each of the big ones. Run together every line fills, and that is what pays for
 * the size: grouped, the narrowest phone could only afford 0.6875rem, the
 * smallest type in the game.
 *
 * Centred lines rather than a left edge, as the printed lists are set: a block of
 * pairs has no first column worth aligning to.
 */
.dictionary__words {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-content: flex-start;
	gap: 2px var(--space-1);
}

/*
 * How big they are is what the panel can afford, and it is measured at every
 * width rather than chosen once.
 *
 * Fluid and not a step at a breakpoint, because a step would put two plainly
 * different sizes on two phones a player might hold side by side. It runs from
 * 0.8125rem at the 320px floor to 1.125rem at 440px, the widest phone there is.
 * The floor is the one figure here that is not the ramp's own: 0.875rem there is
 * 257.1px of words against 245px of list, one row too many.
 *
 * The cap is not decoration and it is the desktop card that sets it. Past 30rem
 * the panel is capped at 26rem wide and 44rem tall while the window is not, so
 * 3.33vw would ask for 43px of type in a 384px panel — and that panel is the
 * tightest box at the top of the range, not the widest phone: 362px of list
 * against 358px of words at 1.25rem, which is 4px of margin. At 1.125rem it is
 * 297px.
 *
 * Measured as the block against the room the list has with a verdict up, which is
 * the tighter of the two states. The row gap is 2px and cannot grow: at 4px the
 * narrowest phone gains 28px it does not have. line-height is said rather than
 * inherited, since the row height is the other half of what decides the fit.
 */
/*
 * The first letter of every word in gold, which is how the printed lists set them
 * and what the two tones are for: in one run of 127 pairs the eye has to know
 * where each word starts, and a block all in one ink is a strip of letters that
 * has to be counted in twos. Two tones and it reads as pairs. Every word and not
 * only the first of each letter's run — that would be marking the groups, which is
 * a different job and one this list has already decided not to do; see above.
 *
 * Measured at 5.85:1 on this panel, which is the pairing the swap sheet's own gold
 * is measured at.
 */
.dictionary__initial {
	color: var(--accent);
}

.dictionary__word {
	font-family: var(--font-tile);
	font-size: clamp(0.8125rem, 0.2rem + 3.33vw, 1.125rem);
	line-height: 1.4;
	font-weight: 600;
	letter-spacing: 0.04em;
	color: var(--ink);
}

/*
 * The turns in rounds, newest round first.
 *
 * A round is a block with its own heading rather than a rule between blocks: the
 * number is the thing a player is looking for — "what did they play last round" —
 * and a rule says only that something changed. The heading is quiet and small,
 * because the rows under it are what is being read, and it is set in the game's
 * own face like every other title here.
 */
.dictionary__rounds {
	display: grid;
	gap: var(--space-3);
}

.dictionary__round {
	display: grid;
	gap: var(--space-1);
}

.dictionary__round-name {
	margin: 0;
	font-family: var(--font-game);
	font-size: 0.875rem;
	font-weight: 400;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--ink-dim);
}

/* Newest first, because the question is nearly always about the turn that has
   just gone. */
.dictionary__moves {
	display: grid;
	gap: var(--space-1);
	margin: 0;
	padding: 0;
	list-style: none;
}

.dictionary__move {
	display: grid;
	grid-template-columns: minmax(0, 6rem) minmax(0, 1fr) auto;
	gap: var(--space-2);
	align-items: baseline;
	padding: var(--space-2);
	border-radius: var(--radius);
	background: var(--surface-sunk);
	font-size: 0.875rem;
}

.dictionary__who {
	color: var(--ink-dim);
	overflow-wrap: anywhere;
}

.dictionary__what {
	font-family: var(--font-tile);
	font-weight: 600;
	letter-spacing: 0.02em;
	overflow-wrap: anywhere;
}

/*
 * A play that used the whole rack, said as a mark beside the words.
 *
 * Gold filled and printed dark, which is the pairing the game says yes with and
 * the one a bingo's own celebration is titled in — so a bingo looks like a bingo
 * wherever it is said. It is also the only warm hue in a panel of greens, which is
 * what makes a lozenge this small carry at all. Measured: the fill is 7.91:1 on
 * the sunk row it sits on and its ink 7.1:1 on the fill.
 *
 * A pill and not a word in the sentence: the row is three columns and this rides
 * in the middle one, so it has to read as something other than the word that was
 * played. Its own line-height and a full radius, so it keeps its shape at the one
 * size it is ever set in.
 */
.dictionary__bingo {
	margin-left: var(--space-1);
	padding: 0.1em 0.45em;
	border-radius: 999px;
	background: var(--accent);
	color: var(--accent-ink);
	font-family: var(--font-game);
	font-size: 0.6875rem;
	font-weight: 700;
	line-height: 1.5;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	white-space: nowrap;
}

/* In its own column, so a run of turns is read down rather than across. */
.dictionary__score {
	font-family: var(--font-game);
	font-variant-numeric: tabular-nums;
	text-align: right;
}

/* --- toasts --- */

/*
 * Across the top, coming down from the edge of the screen.
 *
 * Most of these say why a play was refused, and that is the one message a
 * player must not miss. It costs the scoreboard: a toast covers the scores and
 * whose turn it is while it is up, which is the price of it being read at all.
 * The stack never takes clicks, so the board underneath stays playable.
 */
.toasts {
	position: fixed;
	inset-inline: 0;
	top: max(var(--space-2), env(safe-area-inset-top));
	z-index: var(--layer-toast);
	display: grid;
	gap: var(--space-2);
	justify-items: center;
	padding-inline: var(--space-2);
	pointer-events: none;
}

.toast {
	max-width: min(100%, 26rem);
	padding: var(--space-3) var(--space-4);
	border-radius: var(--radius-lg);
	background: var(--surface-raised);
	/*
	 * A pale edge and a shadow, as a tile has, because a toast this size reaches
	 * past the scoreboard onto the board itself and the green fill is 1.01:1 on
	 * the green field. The edge is 3:1 or better against all three fills and
	 * 6:1 against the table it usually lands on; the shadow carries the rest.
	 */
	box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--ink) 60%, transparent),
		0 0.75rem 1.5rem rgb(0 0 0 / 0.45);
	font-size: var(--step-1);
	line-height: 1.3;
	font-weight: 600;
	text-align: center;
	text-wrap: balance;
}

.toast--good { background: #1a6b51; }
.toast--bad { background: #8a2f26; }

/* --- the bingo cheer --- */

/*
 * A card in the middle of the window with confetti coming down over the whole of
 * it, for the two seconds after the player empties their rack.
 *
 * Nothing in here takes a press, so the celebration cannot be in the way of
 * anything: a tap during it lands the movement — which takes the card and the
 * confetti off, because the resting style of both is invisible — and goes on to
 * whatever is underneath. That is why there is no way to dismiss it and no need
 * of one.
 *
 * It clips, so a piece that has fallen past the bottom edge is gone rather than
 * painted below the window. `clip` after `hidden` for Safari 15, and it is `clip`
 * that is wanted: a hidden box is still a scroll container, and the game is
 * exactly one screen.
 */
.cheer {
	position: fixed;
	inset: 0;
	z-index: var(--layer-cheer);
	display: grid;
	place-items: center;
	overflow: hidden;
	overflow: clip;
	pointer-events: none;
}

/*
 * The card. Sunk rather than raised, which is what buys the type: the gold title
 * is 7.9:1 on --surface-sunk and the green figure 5.8:1, where on the raised fill
 * every panel elsewhere uses that green is 4.3:1 and under the floor.
 *
 * It takes the toast's edge and for the toast's reason — it is the other thing in
 * the game that lands in the middle of the board, where no fill in this palette
 * stands off the green field: the darkest one there is comes to 2.6:1 against it.
 * So the pale ring inside the card is what draws its boundary, at 6.1:1 against
 * its own fill, and the shadow carries the rest.
 *
 * Invisible until something animates it, and that is load-bearing: the whole life
 * of the card is one animation in ui/cheer.js, so a player who touches the screen
 * mid-celebration is left with a clear board rather than a card no press can
 * shift.
 */
.cheer__panel {
	/* Positioned, so it paints over the confetti rather than under it: the pieces
	   are positioned and would otherwise take the front whatever the order they
	   were built in, which puts paper across the one figure being read. */
	position: relative;
	width: min(100% - 2rem, 20rem);
	padding: var(--space-4) var(--space-3);
	border-radius: var(--radius-lg);
	background: var(--surface-sunk);
	box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--ink) 60%, transparent),
		0 1rem 3rem rgb(0 0 0 / 0.6);
	text-align: center;
	opacity: 0;
	/* Declared, or the element gains a transform from nothing when the animation
	   starts and the paint property tree is rebuilt around it mid-celebration. */
	transform: scale(0.2);
}

.cheer__title {
	margin: 0;
	color: var(--accent);
	font-family: var(--font-game);
	font-size: var(--step-2);
	letter-spacing: 0.02em;
}

/*
 * The number is the whole point of the card, so it is the biggest type in the
 * game while it is up. --good rather than the welcome screen's --start, which is
 * that screen's alone.
 *
 * The top of the scale and no further, measured: "+50 points" is 187.5px of ink
 * in the 256px the card gives it on a 320px phone, where the next size up would
 * leave 22px — too little to trust to a font the game does not ship, since
 * --font-game is Iowan Old Style on a phone that has it and Georgia or Palatino
 * on one that does not. One size at every width and never a clamp, for the reason
 * the door's difficulty buttons keep one: two sizes of the same card on two phones
 * a player might hold side by side.
 */
.cheer__bonus {
	margin: var(--space-1) 0 0;
	color: var(--good);
	font-family: var(--font-game);
	font-size: var(--step-3);
	font-weight: 700;
	line-height: 1.1;
}

/*
 * One piece of paper on its way down. Dropped from above the top edge, so it
 * arrives in the window rather than appearing in it, and invisible at rest for
 * the reason the card is.
 */
.cheer__piece {
	position: absolute;
	top: -2rem;
	width: var(--cheer-piece, 0.375rem);
	height: calc(var(--cheer-piece, 0.375rem) * var(--cheer-taller, 1.8));
	border-radius: 1px;
	opacity: 0;
	transform: none;
}

/* --- the teacher --- */

/*
 * The frame the report is seen through: the whole of the game's column, from the
 * safe area at the top of the window to the safe area at the foot of it.
 *
 * The report is the tallest thing the game puts on the screen and the board's own
 * box cannot hold it: 487px of report into the 238px a 320px phone gives the
 * board, so half the lesson — the reason, the tip and the standing — was below a
 * fold with nothing to say it was there. What it may have instead is the window,
 * because by the time it is up there is nothing else on the screen to protect.
 * The board behind it is covered whatever this box is, which is why showing the
 * suggested play folds the report away; the rack and the buttons under it are
 * dimmed and out of reach; and the header is the last thing to give, since the
 * panel is bottom-anchored and only a report too tall to clear it reaches it at
 * all — 38px of the cards on a 320px phone, and none from 390px up.
 *
 * Fixed rather than laid out in the column, so it takes its width from --column
 * rather than from a second figure kept by hand, and it escapes
 * .screen--game's `overflow: clip` because nothing between the two establishes a
 * containing block for it. Anything gaining a transform, a filter or containment
 * on that path would trap it back inside the column.
 *
 * The frame takes no presses. It is a box, not the dialog: what is in it is one
 * card at the foot of the window, and the empty air above that card belongs to
 * the board.
 */
.teacher {
	position: fixed;
	inset-inline: var(--space-3);
	top: max(var(--space-3), env(safe-area-inset-top));
	bottom: max(var(--space-3), env(safe-area-inset-bottom));
	max-width: calc(var(--column, 40rem) - 2 * var(--space-3));
	margin-inline: auto;
	z-index: var(--layer-lesson);
	display: grid;
	align-items: end;
	pointer-events: none;
}

/*
 * A coach's reaction, and the shape of it is a height budget. It is as tall as it
 * needs to be, up to the frame above — 536px on a 320px phone, 632px on a 390px
 * one — and the row order in ui/teacher.js is what decides the fold on the rare
 * report that still outgrows that. Nothing here may grow a row without shoot.mjs
 * being run again.
 *
 * The panel is the scroll container and not the frame around it, which is what
 * keeps the edge below the whole way round: scrolled inside a frame, the bottom
 * of the ring goes under the fold with the last row and the card is left looking
 * cut off rather than scrollable.
 *
 * --lesson-tone is the verdict, and every mark that carries it reads it from
 * here: the face and the edge, which are the only two things in the panel that
 * are a judgement rather than a measurement. Set in one place so a fifth verdict
 * cannot end up half-coloured. All three tones clear 4.3:1 on this panel — good
 * 4.32, accent 5.85, danger 5.10 — of which the drawn face needs 3:1.
 *
 * The edge is the whole way round, so the report reads as a card of its own over
 * a board it does not belong to rather than as a stripe down one side of the
 * page. It is a border-box border, so the 4px comes out of the panel and not out
 * of the frame it sits in: 8px of the text's width and 8px of the height budget.
 */
.lesson {
	display: grid;
	gap: var(--space-2);
	padding: var(--space-3);
	border-radius: var(--radius-lg);
	background: var(--surface-raised);
	box-shadow: var(--shadow-panel);
	border: 4px solid var(--lesson-tone);
	font-size: 0.875rem;
	max-height: 100%;
	overflow-y: auto;
	-webkit-overflow-scrolling: touch;
	pointer-events: auto;
}

.lesson--good { --lesson-tone: var(--good); }
.lesson--warn { --lesson-tone: var(--accent); }
.lesson--bad { --lesson-tone: var(--danger); }

/*
 * What is left of the report while the play it suggests is on the board: one
 * row, saying which play that is and offering the way back.
 *
 * Violet round it rather than the verdict's tone, because the tiles it is
 * captioning are wearing violet. Which of the two it takes is not a choice: the
 * dark --board-accent is 1.04:1 on this panel and would not be there at all,
 * where the soft fill the suggested tiles are painted in is 8.1:1. That is the
 * same reason the board's own red is no use on a sheet.
 */
.lesson--bar {
	grid-auto-flow: column;
	grid-template-columns: 1fr auto;
	align-items: center;
	padding: var(--space-2) var(--space-3);
	--lesson-tone: var(--board-accent-soft);
}

/* The way back, and the whole row but the Close button is it: 44px is the floor
   for a thumb and this is the only thing on the screen a thumb is aiming at. */
.lesson__back {
	display: flex;
	align-items: baseline;
	flex-wrap: wrap;
	gap: 0 var(--space-2);
	min-height: 2.75rem;
	min-inline-size: 0;
	padding: 0;
	text-align: start;
	color: var(--ink);
}

.lesson__head {
	display: flex;
	align-items: center;
	gap: var(--space-2);
}

/* Drawn, not an emoji: see face() in ui/teacher.js. The stroke is scaled by the
   viewBox, so 1.6 units is 2.4px at this size and holds up on the 36px the head
   row gives it. */
.lesson__face {
	flex: none;
	inline-size: 2.25rem;
	block-size: 2.25rem;
	color: var(--lesson-tone);
}

.lesson__face circle {
	fill: none;
	stroke: currentColor;
	stroke-width: 1.6;
}

.lesson__face .lesson__eye {
	fill: currentColor;
	stroke: none;
}

.lesson__face path {
	fill: none;
	stroke: currentColor;
	stroke-width: 1.8;
	stroke-linecap: round;
}

/*
 * The standing and the shout share a line wherever there is room for it, the
 * shout being the thing that is read and the figure the thing that is felt.
 * Where there is not, the shout drops below the figure and the head grows from
 * 36px to 58.1px — measured, and the width is what decides it: once the face and
 * the way out have taken theirs, the pair is left 217.6px on a 390px phone and
 * 147.6px on a 320px one, so a 320px phone stacks them whatever the verdict.
 * That is what labelling the figure costs, and the label is what stops a bare
 * number being read as the opening of the shout. The longest shout wraps again
 * at 88.1px there. The panel scrolls, so what all of this moves is the fold, and
 * the row order in ui/teacher.js is what decides what falls below it.
 *
 * Everything in the head is centred on one line and nothing in it shares a
 * baseline. Four marks of four sizes sit on that line — the face, the figure and
 * its label, the dot between the two claims, and the cross — and only two of them
 * are text, so a baseline is a rule half the row cannot obey: the face and the
 * cross are already centred by .lesson__head, which left the dot hanging a
 * separator's descender below them. Centring boxes centres the type in them
 * because half-leading is symmetric, so a box's middle stands the same way over
 * its baseline whatever size is set in it — measured, the dot's middle and the
 * shout's are 3.4px apart on a baseline and level once centred.
 */
.lesson__result {
	flex: 1;
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 0 var(--space-2);
	min-inline-size: 0;
}

/*
 * The standing is cream and must not take --lesson-tone. It is a place in the
 * field, where the tone is a gap behind the best play in the ranking's Value, so
 * the two disagree by design: a play can beat 91% of the board and still be 38
 * behind the best of it, because most legal plays on a board are dreadful. In
 * the verdict's red, the one number in the panel that is not a judgement
 * contradicts the footer sentence quoting the same figure as reassurance. Its
 * own value cannot colour it either — a green figure beside a rueful red face is
 * the same contradiction the other way about. --ink is 11.0:1 on this panel.
 *
 * The figure itself needs no rule beyond its place in the line: it takes this
 * size and this ink, and only the label steps back from them.
 *
 * The label is centred on the figure, as everything in the head is centred on
 * everything else. Sharing one baseline with something twice its size sits the
 * small word at the foot of the big one, which reads as a subscript rather than
 * as a label on it; centred, the pair is one mark.
 */
.lesson__percentile {
	display: flex;
	align-items: center;
	font-family: var(--font-game);
	font-size: var(--step-2);
	line-height: 1.1;
	color: var(--ink);
}

/*
 * A step back from the figure, as a label is: the figure is what is read, and
 * the word only has to stop it being taken for a grade or for the first word of
 * the shout. --ink-dim is 6.3:1 here.
 *
 * The gap between the two is this margin and not a space in the text, because
 * the label and the figure are separate elements so that they can be set at
 * separate sizes, and nothing in the head is read aloud anyway — see #head.
 */
.lesson__beat {
	margin-inline-end: 0.25em;
	font-size: var(--step-0);
	color: var(--ink-dim);
}

.lesson__dot { color: var(--ink-dim); }

.lesson__shout {
	font-family: var(--font-game);
	font-size: var(--step-1);
	color: var(--ink);
}

/*
 * The way out, and the only square target in the panel: 44px is the floor for a
 * thumb, and a cross has no word beside it to be aimed at instead. It costs the
 * head 8px over the 36px the face gives it, which the panel takes out of the
 * scroll it already has.
 *
 * Cream, at 11.0:1 on this panel. The word it replaces could afford --ink-dim
 * because a word is read rather than recognised; a mark this small has to be
 * unmistakable at a glance, and it is the one thing on the screen that answers
 * while the report is up.
 */
.lesson__say { color: var(--ink); }

.lesson__warning {
	padding: var(--space-2);
	border-radius: var(--radius);
	background: color-mix(in srgb, var(--danger) 18%, transparent);
}

/*
 * The two plays, one above the other so the scores line up and the difference
 * is read rather than remembered. Sunk into the panel at 1.35:1, which is
 * enough to read as a block of its own — nothing in here depends on that
 * contrast, since every label says what it is.
 */
.lesson__compare {
	display: grid;
	border-radius: var(--radius);
	background: var(--surface-sunk);
	overflow: clip;
}

.lesson__play {
	display: grid;
	gap: 0;
	padding: var(--space-2);
	text-align: start;
	color: var(--ink);
}

/*
 * The better play's row is two targets, split down the middle: what the word
 * means on the left, where it would have gone on the right. The halves carry the
 * padding rather than the row, so each is pressable right out to the row's own
 * edges and the score in the right half lands in the same column as the played
 * row's above it.
 *
 * Both halves are three lines of the same three sizes, which is what keeps them
 * reading as one row: the gloss opposite the gap, the word opposite the score,
 * the label opposite the action. So .lesson__ask and .lesson__delta must stay
 * the same size, or the two bottom lines sit a pixel apart.
 */
.lesson__play--better {
	grid-template-columns: minmax(0, 1fr) auto;
	padding: 0;
	border-block-start: 1px solid rgb(255 255 255 / 0.08);
}

.lesson__define,
.lesson__show {
	display: grid;
	align-content: start;
	gap: 0;
	min-height: 2.75rem;
	padding: var(--space-2);
	color: var(--ink);
}

/* The block clips, so a ring outside the half is a ring nobody sees. */
.lesson__define:focus-visible,
.lesson__show:focus-visible { outline-offset: -3px; }

.lesson__define {
	min-inline-size: 0;
	text-align: start;
}

/* A hairline, because the two halves answer different questions and nothing else
   says the row is two targets rather than one. */
.lesson__show {
	justify-items: end;
	text-align: end;
	border-inline-start: 1px solid rgb(255 255 255 / 0.08);
}

.lesson__role {
	font-size: 0.75rem;
	color: var(--ink-dim);
}

/* Says what pressing that half does, and is part of its own text so that what a
   screen reader announces is the score and the action together. */
.lesson__action {
	font-size: 0.75rem;
	color: var(--accent);
}

.lesson__line {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--space-2);
}

.lesson__word {
	font-family: var(--font-game);
	font-size: var(--step-1);
	font-weight: 700;
	letter-spacing: 0.02em;
	overflow-wrap: anywhere;
}

.lesson__points {
	flex: none;
	font-family: var(--font-game);
	font-size: var(--step-1);
	color: var(--ink);
}

/* The gap, and it is not always a gain: the suggestion is the best play by the
   ranking, which weighs the rack kept and the board left as well as the score,
   so it sometimes scores less. Green is only ever for a real gain in points —
   see deltaOf — because green over a smaller number claims a win that is not
   there. */
.lesson__delta {
	justify-self: end;
	font-weight: 700;
	font-size: 0.75rem;
}

.lesson__delta--good { color: var(--good); }
.lesson__delta--quiet { color: var(--ink-dim); }

.lesson__why,
.lesson__footer {
	color: var(--ink-dim);
	font-size: 0.8125rem;
}

/* The one thing to carry into the next turn. Earned by the position — see
   tipOf — never sprinkled on.

   The label is a column beside the advice rather than the first word of it: set
   inline, it indents the first line alone, so a tip long enough to wrap has its
   second line starting further left than its first and the two do not read as
   one block. The label's baseline still sits on the first line, which is what
   keeps it a heading on the sentence rather than a chip above it. */
.lesson__tip {
	display: flex;
	align-items: baseline;
	gap: var(--space-1);
	padding: var(--space-2);
	border-radius: var(--radius);
	background: color-mix(in srgb, var(--accent) 12%, transparent);
	font-size: 0.8125rem;
}

.lesson__tip-label {
	flex: none;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	font-size: 0.6875rem;
	color: var(--accent);
}

.lesson__tip-body { min-inline-size: 0; }

/* A gloss on show every turn is a paragraph nobody reads sitting where the
   advice should be, so it is behind a press that puts it in a call-out beside
   this row. Underlined because inside a half of the comparison row, which is
   pressable whatever this says, the underline is the only thing naming which
   press it is. */
.lesson__ask {
	color: var(--accent);
	font-size: 0.75rem;
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

/* The played word has no half of a row to ride in — see #coach — so asking about
   it is a target of its own and pays the height for one. */
.lesson__ask--alone {
	justify-self: start;
	min-height: 2.75rem;
	padding: 0;
	text-align: start;
	font-size: 0.8125rem;
}

.lesson__footer { font-size: 0.75rem; }

/* --- the final scores, and how to play --- */

.result,
.help {
	position: fixed;
	inset: 0;
	z-index: var(--layer-panel);
	display: grid;
	place-items: center;
	padding: var(--space-3);
	background: rgb(0 0 0 / 0.7);
}

.result__panel,
.help__panel {
	display: grid;
	gap: var(--space-3);
	width: min(100%, 24rem);
	max-height: 85svh;
	overflow-y: auto;
	padding: var(--space-4) var(--space-3);
	border-radius: var(--radius-lg);
	background: var(--surface-raised);
	box-shadow: 0 1rem 3rem rgb(0 0 0 / 0.6);
}

.result__title,
.help__title {
	font-family: var(--font-game);
	font-size: var(--step-2);
	text-align: center;
	color: var(--accent);
}

.result__list {
	display: grid;
	gap: var(--space-2);
}

.result__row {
	display: grid;
	grid-template-columns: 1fr auto;
	gap: var(--space-1) var(--space-2);
	padding: var(--space-2);
	border-radius: var(--radius);
	background: rgb(255 255 255 / 0.05);
}

.result__row--winner {
	box-shadow: inset 0 0 0 2px var(--accent);
}

.result__name { font-weight: 600; }

/* The rack adjustment sits under the name, since it explains the total above it. */
.result__adjustment {
	grid-column: 1;
	font-size: 0.75rem;
	color: var(--ink-dim);
}

.result__score {
	grid-row: 1 / span 2;
	grid-column: 2;
	align-self: center;
	font-family: var(--font-game);
	font-size: var(--step-2);
	font-variant-numeric: tabular-nums;
	font-weight: 700;
}

.result__footer {
	display: grid;
	gap: var(--space-2);
	justify-items: center;
}

/*
 * Centred because it wraps: a conceded game names who quit and where the game
 * went, which is two lines in the 256px a 320px phone gives the panel, under a
 * heading and a list that are centred themselves.
 */
.result__reason {
	font-size: 0.875rem;
	text-align: center;
	color: var(--ink-dim);
}

/*
 * The guide is the one panel whose contents can be taller than the height it is
 * allowed: on a 320px phone the steps come to 83px more than the 85svh above.
 *
 * So the steps are what scrolls, not the panel. The title stays at the top and
 * Got It stays at the bottom, where a scrolling panel puts one or the other out
 * of sight — and a modal whose only way out is below the fold is a trap.
 */
.help__panel {
	grid-template-rows: auto minmax(0, 1fr) auto;
}

.help__steps {
	display: grid;
	gap: var(--space-2);
	padding-inline-start: var(--space-4);
	overflow-y: auto;
	-webkit-overflow-scrolling: touch;
	list-style: decimal;
}

.help__steps li { padding-inline-start: var(--space-1); }

/* --- chat --- */

.chat {
	display: flex;
	justify-content: center;
}

.chat__toggle {
	min-height: 2.25rem;
	padding: 0 var(--space-3);
	border-radius: var(--radius);
	font-size: 0.875rem;
	color: var(--ink-dim);
}

.chat__panel {
	display: grid;
	gap: var(--space-2);
	width: 100%;
	padding: var(--space-2);
	border-radius: var(--radius-lg);
	background: var(--surface-raised);
}

.chat__header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	font-size: 0.75rem;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--ink-dim);
}

.chat__close {
	min-height: 2rem;
	padding: 0 var(--space-2);
	color: var(--ink-dim);
}

.chat__log {
	display: grid;
	gap: var(--space-1);
	max-height: 8rem;
	overflow-y: auto;
	font-size: 0.875rem;
}

.chat__empty { color: var(--ink-dim); }

.chat__form {
	display: grid;
	grid-template-columns: 1fr auto;
	gap: var(--space-2);
}

/* --- scrolling inside a panel --- */

/*
 * A flick that reaches the end of one of these stops there. Left to chain, it
 * carries on into the page and slides the game up behind the panel that is being
 * read — and in a modal, up behind the modal. Every box in the client that
 * scrolls its own contents is listed here; add to it when another appears.
 */
.chooser__list,
.dictionary__list,
.teacher,
.result__panel,
.help__panel,
.help__steps,
.chat__log {
	overscroll-behavior: contain;
}

/*
 * A player's record, opened by pressing their card in the header.
 *
 * A short panel at the foot of the screen, unlike the dictionary: there is no text
 * field on it, so the phone keyboard never comes up and there is nothing that has
 * to be kept clear of it. It is the sheet's ordinary shape, and the sheet's own
 * rule caps and centres it from 30rem up.
 */
.stats {
	gap: var(--space-3);
}

.stats__head {
	display: flex;
	align-items: center;
	gap: var(--space-2);
}

.stats__title {
	flex: 1 1 auto;
	min-width: 0;
	margin: 0;
	font-family: var(--font-game);
	font-size: var(--step-1);
	/* A nickname is twenty characters and the panel is 288px on a 320px phone, so
	   a long one has to give way rather than push the way out off the row. */
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/*
 * The body holds its height across the three states it has — nothing, a note, and
 * the record — so that the panel does not grow and shrink under the reader's
 * thumb while the answer is in flight. The floor is the record's own height at its
 * shortest, which is the rating and the tally with no bests under them.
 */
.stats__body {
	display: grid;
	gap: var(--space-3);
	min-block-size: 8.5rem;
	align-content: start;
}

.stats__note {
	margin: 0;
	align-self: center;
	text-align: center;
	color: var(--ink-dim);
}

/* The rating is the headline and is set as one: the top of the type scale, with
   its caption under it rather than beside it, so the figure is what the eye lands
   on first. */
.stats__rating {
	display: grid;
	justify-items: center;
	gap: var(--space-1);
	padding: var(--space-3);
	border-radius: var(--radius);
	background: var(--surface-sunk);
}

.stats__figure {
	margin: 0;
	font-family: var(--font-game);
	font-size: var(--step-4);
	font-weight: 700;
	line-height: 1;
	color: var(--accent);
}

.stats__label {
	margin: 0;
	font-size: var(--step--1);
	color: var(--ink-dim);
}

/* Three equal columns, because three equal figures read as a set that way and as
   three separate claims stacked. */
.stats__tally {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: var(--space-2);
}

.stats__bests {
	display: grid;
	gap: var(--space-2);
}

.stats__cell {
	display: grid;
	justify-items: center;
	gap: 2px;
	padding: var(--space-2);
	border-radius: var(--radius);
	background: var(--surface-sunk);
}

.stats__cell-figure {
	margin: 0;
	font-family: var(--font-game);
	font-size: var(--step-1);
	font-weight: 700;
	line-height: 1.1;
	color: var(--ink);
}

/* A best word is set in the tile face's grotesque, because that is what a word on
   this board is printed in, and it may be fifteen letters on a 288px panel. */
.stats__word {
	font-family: var(--font-tile);
	font-size: var(--step-0);
	letter-spacing: 0.04em;
	overflow-wrap: anywhere;
	color: var(--accent);
}

.stats__cell-label {
	margin: 0;
	font-size: var(--step--1);
	color: var(--ink-dim);
	text-align: center;
}

/* The way into the games, under the record it belongs to. Quiet, because the
   record above it is what the press was for and this is the offer of more. The
   height is `.button`'s own 2.75rem and may not be restated here: two rules
   naming the touch floor is one of them left behind when it moves. */
.stats__more {
	inline-size: 100%;
}

/*
 * The games view.
 *
 * It takes the height the dictionary takes and for the same reason: fifty rows
 * is far more than the sheet's ordinary shape at the foot of the screen can hold,
 * and a panel that grows with its contents would put its own head off the top of
 * the window. The record view is untouched and stays the short card it was — the
 * class is only worn while the games are up.
 */
.stats--games {
	block-size: 100svh;
	grid-template-rows: auto minmax(0, 1fr);
	padding-top: max(var(--space-3), env(safe-area-inset-top));
	border-radius: 0;
}

/* The body stops holding a floor and starts being the room the list has: the
   floor is there to stop the record's three states changing the panel's height,
   and here the panel's height is fixed by the rule above. */
.stats--games .stats__body {
	min-block-size: 0;
	align-content: stretch;
	grid-template-rows: minmax(0, 1fr) auto;
}

/*
 * The list scrolls and the panel does not, which is the dictionary's rule and its
 * reason: what has to hold still is what the scrolling thing is read against, and
 * that is the head saying whose games these are. Scrolled as a whole panel
 * instead, the name goes off the top the moment anybody looks past the first few.
 */
.stats__games {
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	align-content: start;
	gap: var(--space-2);
	overflow-y: auto;
	overscroll-behavior: contain;
	min-block-size: 0;
}

.stats__game {
	display: grid;
	gap: 2px;
	padding: var(--space-2);
	border-radius: var(--radius);
	background: var(--surface-sunk);
}

/* The date at one end and what the rating did at the other: two facts about the
   whole game, over the scores it came from. */
.stats__game-when {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--space-2);
	font-size: var(--step--1);
	color: var(--ink-dim);
}

.stats__game-date {
	min-inline-size: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.stats__game-rating {
	flex: none;
	font-family: var(--font-game);
	font-weight: 700;
	font-variant-numeric: tabular-nums;
}

.stats__game-rating--up { color: var(--good); }
.stats__game-rating--down { color: var(--danger); }
.stats__game-rating--level { color: var(--ink-dim); }

/* The player's own score and how the game went, on one baseline: the figure is
   what the row is read for and the verdict is what qualifies it. */
.stats__game-score {
	display: flex;
	align-items: baseline;
	gap: var(--space-2);
}

.stats__game-figure {
	margin: 0;
	font-family: var(--font-game);
	font-size: var(--step-1);
	font-weight: 700;
	line-height: 1.1;
	font-variant-numeric: tabular-nums;
	color: var(--ink);
}

/* The top score of the game, wherever it fell. It is deliberately not the same
   mark as the verdict beside it: a resignation is a loss whatever the figures
   say, so a row can read Lost with the best score on it, and the panel may not
   reconcile the two. */
.stats__game-figure--best,
.stats__game-other--best {
	color: var(--accent);
	font-weight: 700;
}

.stats__game-verdict {
	margin: 0;
	font-size: var(--step--1);
	font-weight: 700;
	letter-spacing: 0.06em;
	text-transform: uppercase;
}

.stats__game-verdict--won { color: var(--good); }
.stats__game-verdict--lost { color: var(--ink-dim); }
.stats__game-verdict--drew { color: var(--ink-dim); }

/* Everybody else, run together on as many lines as they need. Each name and its
   score is one span, so a line breaks between opponents rather than between
   somebody's name and their number. */
.stats__game-others {
	margin: 0;
	font-size: var(--step--1);
	color: var(--ink);
	line-height: 1.5;
}

.stats__game-others--none {
	color: var(--ink-dim);
	font-style: italic;
}

.stats__game-gap {
	color: var(--ink-dim);
}

/* Set in the tile face's grotesque, because that is what a word on this board is
   printed in — the same rule the lifetime best word above is set by. */
.stats__game-word {
	margin: 0;
	font-family: var(--font-tile);
	font-size: var(--step--1);
	letter-spacing: 0.03em;
	overflow-wrap: anywhere;
	color: var(--ink-dim);
}

/* What the list is not showing, said only when there is something it is not. */
.stats__more-note {
	margin: 0;
	font-size: var(--step--1);
	color: var(--ink-dim);
	text-align: center;
}

@media (min-width: 30rem) {
	/* The sheet centres and caps its panel from here up, so the games view
	   becomes a tall card with room round it rather than a full-height sheet —
	   the dictionary's own treatment at the same breakpoint. */
	.stats--games {
		block-size: min(100svh, 44rem);
		border-radius: var(--radius-lg);
	}
}

/*
 * A card that opens a record takes a press over the whole of itself.
 *
 * The button is stretched over the card rather than replacing it: a card is a grid
 * of five things, and making the card itself a button would put a grid container
 * inside a control that not every browser lays out the same way. Inset to the
 * card's own box, so the whole card is the target and nothing about the layout
 * moves.
 *
 * It has no appearance of its own. The card is already a card, and a second
 * treatment on the cards that happen to have a record behind them would read as
 * those players being marked out.
 *
 * What it is inset to is `.seat`'s own `position: relative`, which every card
 * carries because the place rosette hangs off the corner of every card. It must
 * stay there rather than coming back here: a press positioned against whatever
 * ancestor happened to be relative would be a press the size of the header.
 */
.seat__press {
	position: absolute;
	inset: 0;
	padding: 0;
	border: 0;
	border-radius: inherit;
	background: none;
	cursor: pointer;
}

/*
 * A button that reads as a sentence rather than as a control.
 *
 * For the two places where the action is secondary to the screen it is on: signing
 * in from the welcome screen, and reaching the invite form from the sign-in one.
 * It keeps the 44px target every other control has — what is given up is the fill,
 * not the size — so it is no harder to hit for being quieter.
 */
.button--link {
	min-height: 2.75rem;
	padding: 0 var(--space-2);
	background: none;
	box-shadow: none;
	color: var(--ink);
	font-size: var(--step--1);
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

/*
 * The line under the two choices saying who this browser is.
 *
 * It sits below the pair rather than above the name field, because it is about
 * what pressing Start will need rather than about who is playing: a player joining
 * a game never reads it, and one starting a game reads it on the way to the button.
 */
.lobby__account {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: center;
	gap: var(--space-1);
	margin: 0;
	font-size: var(--step--1);
	color: var(--ink-dim);
}

.lobby__signed-in {
	/* On the poster, and so held to the plate's own contrast: --ink-dim is what the
	   label beside it is measured at, and this is the same ink on the same glass. */
	color: var(--ink-dim);
}

/* Said before the password is typed rather than after it is refused, which is the
   only place a rule about a password is any use. */
.lobby__hint {
	margin: 0;
	font-size: var(--step--1);
	color: var(--ink-dim);
	text-align: center;
}

/*
 * Handing the room code to somebody who is not in the room.
 *
 * Quiet rather than primary: the host's decision on this screen is Start the game,
 * and a second filled button beside it is two things asking to be pressed. It is
 * still a full 44px target — what a quiet button gives up is the fill, not the
 * size.
 *
 * It takes the code block's own width rather than the panel's, so it reads as
 * belonging to the code above it and not as a third control in the row below.
 */
.admission__share {
	justify-self: stretch;
}

/*
 * The link itself, shown only where the browser has neither a share sheet nor a
 * clipboard — which over plain HTTP is every time, since both need a secure
 * context and a LAN address is not one.
 *
 * A URL is longer than the box at every width the game runs at and must not be
 * allowed to widen the panel, so it scrolls inside itself. It is left-aligned
 * against the centred block above it because a URL is read from its start.
 */
.admission__link {
	justify-self: stretch;
	inline-size: 100%;
	min-width: 0;
	font-size: 0.8125rem;
	text-align: left;
	text-overflow: ellipsis;
}
