/*
 * The board.
 *
 * A square grid that takes whatever width is going and never grows taller than
 * the space left for it, so the rack and the buttons stay on screen without
 * scrolling. That constraint is what makes it usable one-handed on a phone.
 *
 * Two elements, because the board zooms: the frame holds still and the field of
 * squares moves inside it. The frame is what the rest of the column is laid out
 * against and it keeps its box whatever the field is doing, so a close-up cannot
 * push the rack or the buttons off a phone.
 *
 * Nothing is drawn round the outside — no border, no rounded corners, no shadow.
 * The board runs to the edge of the window and every pixel of its width belongs
 * to the squares: a printed surround is a border-box border, so it comes off the
 * field, and on a phone that is most of a pixel off all fifteen squares. The edge
 * of the board is the outermost lattice line, which the field paints itself.
 */

.board {
	/*
	 * The white lattice between the squares, which is the gap and the padding
	 * showing through. A fine printed rule rather than a mortar course: 1.75px
	 * against a 30.4px square on a desktop board, about 6%.
	 * The 1px floor is the real constraint and must stay — below about 430px the
	 * middle term is already under a pixel, so a phone is held at the floor and
	 * one pixel is all the grid has to hold the board together with.
	 */
	--lattice: clamp(1px, 0.25vw, 1.75px);
	width: min(100%, var(--board-max, 40rem));
	aspect-ratio: 1;
	margin: 0 auto;
	/*
	 * Every square scales from this, so tile text stays proportional at any size.
	 * A close-up does not touch it: the field is scaled, so a magnified square's
	 * letter grows with the square instead of staying the size it was.
	 */
	container-type: inline-size;
	/*
	 * The frame is the window a close-up is seen through, so whatever the field
	 * does beyond it is not shown.
	 *
	 * Declared twice on purpose. clip is what is wanted, because it makes no
	 * scroll container: a hidden one can still be scrolled, and the browser would
	 * scroll it to reveal a focused square — sliding the field out from under its
	 * own transform, which nothing here would know about. hidden is what a Safari
	 * older than 16 understands, and clipping badly beats not clipping at all.
	 */
	overflow: hidden;
	overflow: clip;
}

/*
 * The field: the fifteen rows and the white lattice they sit in. This is what a
 * close-up moves, and zoom.js writes the transform, because where the player is
 * looking is not something a stylesheet can know.
 *
 * will-change must stay, for the reason it must stay on a rack tile: an element
 * that gains a transform from nothing has the paint property tree rebuilt around
 * it, and this board is a container query whose premium labels are then all
 * re-shaped to suit. Said up front, the first zoom costs nothing.
 */
.board__field {
	/* The badges saying what a play's words scored are laid out against this. */
	position: relative;
	display: grid;
	grid-template-rows: repeat(15, 1fr);
	gap: var(--lattice);
	width: 100%;
	height: 100%;
	padding: var(--lattice);
	background: var(--board-line);
	box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18);
	will-change: transform;
}

/* With a close-up up, a drag moves it, so the browser must not take the gesture
   for a scroll of the page. */
.board--zoomed {
	touch-action: none;
}

.board-row {
	display: grid;
	grid-template-columns: repeat(15, 1fr);
	gap: var(--lattice);
}

/*
 * A premium square is one flat colour, edge to edge, with its label printed on
 * it. The square itself is what carries the colour and nothing is drawn inside
 * it, so a premium costs no nodes at all — which matters, because 225 squares
 * are built for every game.
 *
 * A plain square leaves --premium unset and shows the green field. That is what
 * lets the highlight states below colour any square, premium or not, and it is
 * why the colour has to stay on the square rather than on a layer within it.
 */
/*
 * Nothing is clipped here, because what a tile paints outside its own square is
 * its thickness.
 *
 * A tile fills the square exactly, so its drop shadow — the outermost of the four
 * layers below, and half of what stands a tile off a lattice it is 1.07:1
 * against — falls entirely outside the padding box. Clipped, it never painted at
 * all, and the only thing left holding the tile off the board was the inset edge.
 *
 * What that asks of the animations is a rule rather than a clip: nothing may take
 * a tile further out of its square than its own resting shadow already does. A
 * square is a fifteenth of the board, so for a settled tile that is 8% of a square
 * below, 5% to the sides and 2% above, and for a pending one, whose shadow is
 * deeper, 20%, 12% and 4.5%. See animations.css, where each is measured against it.
 */
.square {
	position: relative;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 0;
	aspect-ratio: 1;
	background: var(--premium, var(--board));
	line-height: 1;
}

.square--triple-word {
	--premium: var(--triple-word);
	--label-ink: var(--triple-word-ink);
}

.square--double-word {
	--premium: var(--double-word);
	--label-ink: var(--double-word-ink);
}

.square--triple-letter {
	--premium: var(--triple-letter);
	--label-ink: var(--triple-letter-ink);
}

.square--double-letter {
	--premium: var(--double-letter);
	--label-ink: var(--double-letter-ink);
}

.square--centre {
	--premium: var(--centre);
	--label-ink: var(--centre-ink);
}

/*
 * The premium's short name, sized from the board rather than the viewport so it
 * stays readable whether the board is 320px or 640px wide.
 *
 * Two letters, not "DOUBLE LETTER": a square is fourteen pixels across on a
 * small phone and the words cannot be set at any size a person could read. The
 * uppercase tracking is what makes them read as labels rather than as text.
 */
.square__hint {
	font-family: var(--font-tile);
	font-size: 1.7cqw;
	font-weight: 700;
	letter-spacing: 0.06em;
	color: var(--label-ink);
}

.square--centre .square__hint {
	font-size: 3.4cqw;
	letter-spacing: 0;
}

/*
 * An empty square is only a target when it is your move, and a square that is
 * not a target does not take the press either: the board does.
 *
 * That is what the close-up needs. A double-tap opens one from anywhere on the
 * board, including all the squares that are out of play while an opponent thinks,
 * and hit testing has to reach the board for it to be heard. Nothing is lost —
 * a disabled square is no more a target for a tile than for a finger.
 */
.square:disabled {
	pointer-events: none;
}

/*
 * A keyboard is the only way to play without a pointer, so the ring has to be
 * there. Gold is 3:1 on the field; the offset puts most of this one on the
 * white lattice, where dark violet is 7:1, and the violet fill does the rest.
 */
.square:focus-visible {
	outline-color: var(--board-accent);
}

/*
 * The square the keyboard is on is filled violet and ringed inside its own
 * edge, whatever colour it started as: the same mark in the same place on every
 * square, which is what lets it be recognised at 14px. Violet is the one hue
 * the board does not use, so it cannot be read as a premium.
 *
 * It takes the fill and the ring together, because no single colour is 3:1
 * against a green field, an ochre premium and a cream tile at once: the soft
 * fill carries on the field at 4.1:1 and the dark ring carries on the fill at
 * 7.8:1, so the pair reads wherever it lands.
 *
 * A pointer gets no such mark. A carried tile is already laid in the cell it is
 * over, so on the way to a drop the mark would be behind the tile; and with no
 * tile in hand there is nothing being aimed at, which leaves a square lighting
 * up under a passing cursor for no reason.
 */
.square:not(:disabled):not(.square--filled):focus-visible {
	--premium: var(--board-accent-soft);
	--label-ink: var(--board-accent);
	box-shadow: inset 0 0 0 max(1px, 0.4cqw) var(--board-accent);
}

/*
 * There is no separate mark for where a dragged tile will land. The square that
 * would take it is wearing the tile itself, at the square's own size and square
 * to the grid — anything painted underneath would be behind it, and anything
 * painted elsewhere would be a second answer to the same question.
 */

/* --- tiles --- */

.tile {
	position: relative;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 100%;
	height: 100%;
	background: linear-gradient(var(--tile-tilt, 160deg), var(--tile-light) 0%, var(--tile) 48%, var(--tile-deep) 100%);
	border-radius: 8%;
	/* The edge and the shadow are what a real tile's thickness does: the face is
	   1.07:1 against the lattice it sits in, so neither can be dropped. */
	box-shadow:
		inset 0 0 0 max(1px, 0.12cqw) var(--tile-edge),
		inset 0 0.2cqw 0 rgb(255 255 255 / 0.55),
		inset 0 -0.2cqw 0 rgb(0 0 0 / 0.12),
		0 max(1px, 0.2cqw) max(2px, 0.35cqw) rgb(0 0 0 / 0.45);
	color: var(--tile-ink);
}

/*
 * No two tiles catch the light quite the same way. Identical dimensions, four
 * gradient angles keyed on the parity of the row and the column, so no tile
 * shares an angle with the one beside or above it: enough to stop a finished
 * word looking stamped out, not enough to read as a pattern.
 */
.board-row:nth-child(odd) .square:nth-child(odd) .tile { --tile-tilt: 152deg; }
.board-row:nth-child(odd) .square:nth-child(even) .tile { --tile-tilt: 166deg; }
.board-row:nth-child(even) .square:nth-child(odd) .tile { --tile-tilt: 173deg; }
.board-row:nth-child(even) .square:nth-child(even) .tile { --tile-tilt: 158deg; }

/*
 * The letter fills the tile the way it does on a printed one: a grotesque at this
 * size lands a cap height of a little over half the square, where the serif it
 * replaced sat at well under half and read as a small letter on a big tile.
 *
 * It is as big as the corner value lets it be and must not be grown. A letter
 * this size fills the square from side to side, so what keeps the two apart is
 * the value sitting below the letter's baseline rather than beside it — and Q
 * and Z are the only tiles worth ten, which is the one value wide enough to
 * reach back under the letter. Bigger and their ink meets the digits.
 */
.tile__letter {
	font-family: var(--font-tile);
	font-size: 4.3cqw;
	font-weight: 700;
	letter-spacing: -0.01em;
}

/*
 * The face value, tucked into the corner as it is on a real tile. Small enough
 * to sit clear of the letter's baseline in both directions: a step larger and
 * the ten under a Q or a Z is printed on the letter, since a two-digit value is
 * nearly a third of the square wide. Measured against every letter's own ink at
 * every board size, the tail of the Q is the closest thing on the board to it.
 */
.tile__value {
	position: absolute;
	right: 0.6cqw;
	bottom: 0.35cqw;
	font-family: var(--font-tile);
	font-size: 1.3cqw;
	font-weight: 700;
	opacity: 0.85;
}

/*
 * A blank is plainly a blank: no value, a paler face, and its chosen letter in
 * red. Three signals rather than one, and the square's own aria-label says
 * "(blank)" besides, because colour on its own tells a colour-blind player
 * nothing.
 */
/* Both hosts are named because a blank looks the same on a square as it does in
   the air, and a carried tile is a .rack-tile — whose own ink is declared in a
   later stylesheet and would otherwise win on load order alone. The paler face is
   the three face tokens reassigned, not a second gradient: what a tile is made of
   is stated once, on .tile and on .rack-tile, and anything that recolours a face
   goes through the tokens so it reaches every kind of tile. */
.tile.tile--blank,
.rack-tile.tile--blank {
	--tile-light: #ffffff;
	--tile: #fbf9f2;
	--tile-deep: #efece0;
	color: var(--tile-blank-ink);
}

/* A tile still being placed sits proud of the board and can be picked back up. */
.tile--pending {
	box-shadow:
		inset 0 0 0 max(1px, 0.12cqw) var(--tile-edge),
		inset 0 0.2cqw 0 rgb(255 255 255 / 0.6),
		0 0.5cqw 0.8cqw rgb(0 0 0 / 0.5);
	outline: 0.25cqw solid var(--board-accent);
	outline-offset: -0.25cqw;
}

/*
 * A word the last play scored: every tile that spells it is washed green, the
 * letters already on the board included, because what was played is the word and
 * not only the tiles that were added to it.
 *
 * The counterpart of the refusal below, and built the same way — the three face
 * tokens reassigned, so a blank is washed along with the rest, which is why it
 * must stay after the blank's own rule. A refusal wins over it on source order
 * for the same reason: the two never coincide today, since a play clears the
 * refused squares, but red is the one that must survive if they ever do.
 *
 * It lasts as long as the badges beside it and comes off with them, because both
 * are one caption on one position: what was played, and what it scored.
 *
 * Colour is not the only signal. The toast names the words, the badges say what
 * each was worth, and the square's aria-label says "just played", because a pale
 * green face reads as nothing to a screen reader and as very little to a
 * colour-blind player against a cream one.
 */
.square--scored .tile {
	--tile-light: var(--tile-scored-light);
	--tile: var(--tile-scored);
	--tile-deep: var(--tile-scored-deep);
	/* The cream edge is 3.0:1 on this face and 2.5:1 at the deep end of the
	   gradient, so it stops doing its job. */
	--tile-edge: var(--board);
}

/*
 * A word the lexicon refused: every tile that spells it is washed red, the
 * letters already on the board included, because what was refused is the word and
 * not the one tile that finished it.
 *
 * The wash is the face tokens reassigned, so a blank is washed along with the
 * rest. It must stay after the blank's own rule to do that — both are two classes
 * deep, so the later one wins.
 *
 * Colour is the least of the signals: the tiles shake, the toast names the words,
 * and the square's aria-label says "not in the word list", because a pink face
 * reads as nothing to a screen reader and as very little to a colour-blind player.
 */
.square--invalid .tile {
	--tile-light: var(--tile-refused-light);
	--tile: var(--tile-refused);
	--tile-deep: var(--tile-refused-deep);
	/* The cream edge is 2.7:1 on the washed face and stops doing its job. */
	--tile-edge: var(--board-danger);
	outline-color: var(--board-danger);
}

/*
 * The play the teacher is suggesting, laid out where it would have gone.
 *
 * Pencilled in rather than played: one flat violet face with a dark violet ring
 * and no thickness at all, so it cannot be read as a tile somebody has put down.
 * Violet because it is the one hue the board does not use, and it takes the fill
 * and the ring together for the reason the keyboard's own mark does — the soft
 * fill is 4.15:1 on the green field but only 1.02:1 on the pale aqua double
 * letter, so the ring is what gives the tile an edge on a premium: 3.0:1 on the
 * rust triple word, 3.2:1 on the teal triple letter and better everywhere else.
 * The letter is 7.8:1 on the fill.
 *
 * The face tokens are reassigned rather than a background set, so a blank the
 * suggestion spends is washed with the rest. It must stay after the blank's own
 * rule to do that — both are two classes deep, so the later one wins.
 *
 * The ink goes through the token for the opposite reason: a blank declares its
 * red directly and has to keep it, since which tile a play spends is part of
 * what is being suggested. Red on this face is 4.6:1.
 */
.square--suggested .tile {
	--tile-light: var(--board-accent-soft);
	--tile: var(--board-accent-soft);
	--tile-deep: var(--board-accent-soft);
	--tile-ink: var(--board-accent);
	box-shadow: inset 0 0 0 max(1px, 0.35cqw) var(--board-accent);
}

/*
 * A tile that has been picked up and is in the air. Faded rather than removed:
 * the element a touch started on has to outlive the gesture, or the browser
 * cancels the drag along with it.
 */
.square--lifted .tile {
	opacity: 0.2;
	outline-color: transparent;
}

/* A tile still being placed is the one thing on the board that can be moved,
   so it is the one thing that says so. */
.square--pending {
	cursor: grab;
	touch-action: none;
}

/* --- what a play's words scored --- */

/*
 * The badges sit in the field rather than over the board, so a close-up carries
 * them with the squares they belong to.
 *
 * Which means they are laid out over the grid rather than in it, and the
 * arithmetic has to match what the grid itself does: fifteen tracks with a
 * lattice line between each pair and one round the outside, which is sixteen
 * lines. Get that wrong and a badge drifts a square away from the word it is
 * about by the far edge of the board.
 *
 * The percentage in --cell is resolved where it is used, against this box, which
 * is square — so the same expression serves a column and a row.
 */
.board__badges {
	--cell: calc((100% - 16 * var(--lattice)) / 15);
	position: absolute;
	inset: 0;
	/* The board underneath still takes every press: a close-up is opened from
	   anywhere on it, badges included. */
	pointer-events: none;
}

/*
 * One box exactly over the square a word finished on, holding the pill that says
 * what it scored. Two elements because the box is what says which square, and
 * the pill is what moves and what can be turned round to the other side of it.
 */
.score-badge {
	position: absolute;
	left: calc(var(--lattice) + var(--col) * (var(--cell) + var(--lattice)));
	top: calc(var(--lattice) + var(--row) * (var(--cell) + var(--lattice)));
	width: var(--cell);
	height: var(--cell);
	/*
	 * How far this badge has been moved down off another that was already there,
	 * in squares, which badges.js works out by measuring the set.
	 *
	 * It has to be on this box and not on the pill. --cell holds a percentage, and
	 * a percentage is resolved against the containing block of whatever uses it:
	 * this box's is the badge layer, which is the board, and the pill's is this
	 * box, which is one square. On the pill it would come out fifteen times too
	 * small. In squares rather than pixels so that a board changing size carries
	 * the badges with it, as the rest of this geometry already does.
	 */
	margin-top: calc(var(--nudge, 0) * var(--cell));
	/*
	 * And how far it has been slid left to keep it inside the frame, in the same
	 * squares and on the same box for the same reasons. A badge by the right-hand
	 * edge is slid rather than turned round: only as far as it must be, so it stays
	 * hard against the end of the word it is about.
	 */
	margin-left: calc(-1 * var(--tuck, 0) * var(--cell));
}

/*
 * Hung off the bottom-right corner of the square, clear of the letter and its
 * value: the number is beside the word rather than on top of it.
 *
 * Nearly the whole square out, so that what it lands on is the square
 * diagonally next — which for a word running either way is off the end of it
 * and empty. Any less and a badge sits over the next letter of its own word,
 * which for a word running down is the one directly beneath it, and these stay
 * up until somebody else moves.
 *
 * White on a royal blue for your own score and on a crimson for somebody else's,
 * lit from the same side a tile is and with nothing printed round it. The fill is
 * what gives the pill its edge everywhere except the green field — 10.8:1 on a
 * cream tile, 3.1:1 on the rust triple word, 3.3:1 on the teal triple letter —
 * which is also what caps how bright either may be, and white on them is 12.2:1,
 * so a three-figure score is readable at the size a phone's board gives it. The
 * two are matched stop for stop in luminance, which is why one set of ratios
 * covers both: see tokens.css.
 *
 * On the field the fill manages 1.9:1 and cannot do better: 3:1 under that green
 * needs a luminance of 0.005, which is black, and white on black is not a
 * coloured pill. So the shadow is load-bearing here rather than decoration — it
 * is the only thing separating a pill from the field it is lying on, and must not
 * be dropped or lightened without putting a ring back.
 *
 * A two-figure score is smaller than a square, in both directions, and that is
 * the size rule rather than a matter of taste. Two badges are moved off each
 * other only when they overlap, and two pills on neighbouring squares overlap by
 * exactly as much as a pill exceeds a square — so a pill any bigger than one
 * guarantees the moving, and a badge moved is a number sitting somewhere down the
 * board with nothing to say which word it is about. In Helvetica a digit is
 * 0.556em, so "16" comes to 1.112 × the font size plus the padding: at 3.4cqw and
 * 0.75cqw either side that is 5.28cqw against the 6.33cqw a square measures, and
 * the height 1.2 × 3.4 plus 0.45cqw either side is 4.98cqw. Both clear a square
 * by more than the 2px of air badges.js keeps between two pills, which is what
 * makes the neighbouring cases — a word and the cross-word off its last tile —
 * come out unmoved.
 *
 * The number is a good deal smaller than the pill it sits in, and the padding is
 * what it gave up: the pill is the size it has always been — 16.1 × 15.2 against
 * a 19.2px square on a 390px phone — with the digits set a step down inside it,
 * because a figure filling its own bubble edge to edge reads as cramped at every
 * width and the pill cannot grow to answer it without guaranteeing the moving.
 *
 * The floor is legibility on a 320px phone, where the board is 240px across; it
 * must not be raised, since a floor that binds any harder is the size rule broken
 * on the narrowest screen there is. It does bind there — 3.4cqw is 8.2px — so
 * that phone keeps a 9px digit and takes the whole of the change as air: 13.6 ×
 * 13.0 against a 15.2px square, still clear in both directions.
 *
 * This is where a badge rests, and it must stay a resting style rather than the
 * end of a fill: a badge stays up until somebody else takes their turn, and an
 * animation called off mid-arrival leaves the element on nothing but its own
 * style. Written as a fill instead, a tap during the stagger would take half a
 * play's numbers off the board.
 */
.score-badge__pill {
	position: absolute;
	left: 88%;
	top: 88%;
	padding: 0.45cqw 0.75cqw;
	border-radius: 999px;
	background: linear-gradient(160deg, var(--badge-light) 0%, var(--badge) 48%, var(--badge-deep) 100%);
	box-shadow: 0 0.35cqw 0.9cqw rgb(0 0 0 / 0.55);
	color: var(--badge-ink);
	font-family: var(--font-tile);
	font-size: clamp(0.5625rem, 3.4cqw, 1.3125rem);
	font-weight: 700;
	line-height: 1.2;
	white-space: nowrap;
}

/*
 * Above the square instead of below it, for a badge on the bottom row that would
 * otherwise hang off the board and be cut in half by the frame. A mirror of the
 * same offset about the square's own centre, which is what lets the script decide
 * by measuring one side and assuming the other.
 *
 * The vertical case is the one that has to be turned round. A pill hangs nearly a
 * whole square out, so one slid up far enough to fit back inside the frame would
 * be sitting on its own last letter — where sideways it can simply be slid, and
 * is, because there it only gives up the corner it was hanging off.
 */
.score-badge--up .score-badge__pill {
	top: auto;
	bottom: 88%;
}

/*
 * Somebody else's play. Blue is what you scored or are about to score, red is
 * what was scored against you, and that is the whole of what the colour says —
 * so it goes on a play that has landed and never on the one being built, which
 * can only ever be your own.
 */
.score-badge--rival .score-badge__pill {
	background: linear-gradient(160deg, var(--badge-rival-light) 0%, var(--badge-rival) 48%, var(--badge-rival-deep) 100%);
}
