:root {
    /* The X's box, named because three rules depend on it agreeing: the button itself, the row's
       minimum height, and the padding a left-aligned title reserves so it wraps before reaching
       it. Sized for FINGERS - see .modalClose. */
    --modal-close-size: clamp(34px, 18px + 1.8vw, 52px);
    --game-accent: #759B67;
}

/* Dimensions consolidate as: top right bottom left. */
body {
    text-align: center;
    font-family: sans-serif;
    font-size: calc(4px + .5vw); /* Results in .5vw with minimum of 4px */
    /* Named, so the site's header and footer (base.css, body.gi-compact) can claim the margin back
       with negative margins of exactly this much. Capped by the height too, so a short, wide window
       does not spend its few rows on margin. */
    --page-margin: min(2vw, 4vh);
    margin: var(--page-margin);
    background-image: url("../image/PubDomain_FadedRock.jpg");
}

h1 {
    font-family: var(--mono);
    font-size: calc(6px + 4vw);
    /* Was -1vw top: a tuck up into the body margin, whose space the shared header
       (views/partials/header.html) occupies - the negative margin collapsed past it and overran it by ~5px. */
    margin: 0 0 1vw 0;
}

h3 {
    font-family: var(--mono);
    font-size: calc(6px + 1vw);
    margin: 0;
}

/* The board. layout.js writes its size and float into this rule (updateStylesheet), by this
   selector. */
#hangmanCanvas {
    float: left;
    width: inherit;
    height: inherit;
    margin: 1vw 0 0 0;
    background-color: #e8e8e8;
}

.hangmanWord {
    font-family: var(--mono);
    letter-spacing: .5vw;
    font-size: calc(12px + 8vw);
    font-weight: 500;
    text-align: center;
    margin: -2vw 2vw 2vw 2vw;
    /* The theme's accent, a CSS variable: the stylesheet's own default is on :root (the one place
       the value is written) and applyThemeStyle() overrides it for a skin. #759B67 is the green
       the game has always used and what every look uses until a skin says otherwise. */
    color: var(--game-accent);
}

.letterBtn {
    font-family: var(--mono);
    font-size: calc(8px + 3vw);
    font-weight: normal;
    width: 100%;
    text-align: center;
    padding: 0;

}

select {
    text-align: left;
    font-family: var(--mono);
    font-size: calc(4px + .5vw);
    margin: 1vw auto auto auto;
}

/* Body text in the Help and About dialogs. Sized from vmin, like the headings above it, rather
   than vw: on a tall narrow window the old calc(6px + .75vw) came out SMALLER than the section
   headings it sat under (Jeff, 2026-08-21: "Make the 'About' text bigger. Is smaller than
   subheadings now"). */
.helpText {
    text-align: left;
    font-family: sans-serif;
    font-size: calc(13px + 0.55vmin);
    line-height: 1.5;
    margin: 1vw auto auto auto;
}

.helpEnd {
    margin-bottom: 2.5vh;
}

.modalText {
    text-align: left;
    font-family: var(--mono);
    font-size: calc(6px + .75vw);
    margin: 1vw auto auto auto;
}
.modalTextCentered { text-align: center; }

/* The game shows and hides these with the hidden attribute, and each one's class sets a display
   that would otherwise win over it. */
.finishVerdicts[hidden],
.finishNoteRow[hidden],
.classRosterArea[hidden],
.standingLive[hidden],
.prefChoiceCol[hidden],
.modalClose[hidden] { display: none; }

.indent {
    margin-left: 2vw;
}

.modal {
    display: none;
    position: fixed;
    z-index: 1;
    padding-top: 10vw;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    overflow: auto;
    /* RESERVE THE SCROLLBAR'S WIDTH WHETHER OR NOT IT IS THERE. Jeff, 2026-09-03: "The advanced
       filter is (unnecessarily) triggering a scroll bar. Some screens might need that, so don't
       suppress it. But center left-to-right as though the scroll bar is there, so the screen
       doesn't shift as you toggle between advanced and normal."
       Advanced is taller than the window where Normal is not, so the bar appears on the toggle,
       the scrollable area narrows by its width, and a centred dialog slides left by half that -
       a horizontal jump caused entirely by a vertical control. `stable` keeps the gutter there
       always, which costs a strip of background nobody looks at and buys a dialog that does not
       move. Suppressing the bar was never an option: on a short screen it is the only way out. */
    scrollbar-gutter: stable;
    background-color: rgb(0, 0, 0); /* Color for browsers not supporting opacity */
    background-color: rgba(0, 0, 0, 0.4);
}

/* THE MODALS ARE <dialog> ELEMENTS (code review C3, 2026-09-10). The element itself is the
   full-screen sheet the div used to be - same fixed box, same padding-top, same tinted
   background, so a click on the sheet outside the content still targets the dialog and the
   "click outside to close" handlers keep working - and the browser's own backdrop is left
   transparent so the tint is not applied twice. showModal() puts an open dialog in the top
   layer, so the per-pair z-index rules that stacked one div over another no longer decide
   anything: the dialog opened last is on top, which is what every pair wanted. */
dialog.modal {
    margin: 0;
    inset: 0;
    border: 0;
    max-width: none;
    max-height: none;
    padding: 10vw 0 0 0;
    color: inherit;
}
dialog.modal[open] { display: block; }
dialog.modal::backdrop { background: transparent; }

.modal-content {
    text-align: left;
    background-color: #fefefe;
    margin: auto auto 20vh;
    padding: 1vw;
    border: 1px solid #888;
    width: 80vw;
}

/* ---------------------------------------------------------------------------------------------
   THE DIALOG HEADER BAR. Jeff, 2026-08-23: "We need to develop and use a large informational
   modal dialog that has a space carved out and protected for the close 'X'... It's been a
   continual issue that layout trips on this... an optional title sits left. The 'X' sits right
   and tight to the top... And all text starts below that line."

   The old X was `float: right` with `top: -1.7vw`, which is exactly why layout kept tripping: a
   float is OUT OF FLOW, so every dialog had to dodge it by hand (the preferences title carried a
   `padding-right: calc(12px + 5vw)`) and any
   dialog that forgot - or whose text was rendered by JS - ran straight under it.

   This bar is IN FLOW. It is the first child of every .modal-content, it is as tall as the X,
   and everything after it therefore starts below it. There is nothing left to dodge, which is
   the whole point: the protection is structural, not a per-dialog correction.

   `margin-left: auto` on the button rather than `justify-content: space-between` so the X stays
   hard right whether or not there is a title - space-between with a lone child puts it LEFT.
   --------------------------------------------------------------------------------------------- */
/* THE X IS OUT OF FLOW (2026-08-26). It used to be a flex sibling of the title, which meant the
   title's box was the row MINUS the button - so `text-align: center` on a title centred it in the
   leftover space beside the X rather than on the dialog, and it sat visibly off-centre from the
   text below it. Jeff: "any title centered there needs to be centered within the same space as
   the text below it. I.e. the full horizontal wall-to-wall space" - and, on the collision that
   allows: "I do want a title to be able to be centered there at the top, horizontally conflicting
   with the 'X' at the far right."

   So the button is absolutely positioned and the title spans the whole row. A LEFT-aligned title
   still reserves the button's width as padding, because left-aligned text grows rightward into
   the X and wrapping early is the correct behaviour there. A CENTRED one drops that padding: it
   is centred on the dialog, which is the whole point, and it is allowed to run under the X. */
.modalHead {
    display: flex;
    align-items: center;
    gap: 1em;
    position: relative;
    /* Enough height for the absolutely-positioned X, which no longer contributes any. */
    min-height: var(--modal-close-size);
    /* No bottom margin when the bar is empty of a title: a title-less dialog should open as
       tight to the top as it can while still clearing the X (Jeff: "Possibly very tight to the
       top boundary if there is no title, but still below the 'X'"). */
    margin: 0 0 0.6vmin 0;
}

.modalHeadTitle {
    margin: 0;
    flex: 1 1 auto;
    min-width: 0;                    /* a long title wraps instead of shoving the X off the edge */
    padding-right: calc(var(--modal-close-size) + 1em);
    text-align: left;
    font-family: var(--mono);
    font-size: calc(12.5px + 0.6vmin);
    font-weight: 700;
    line-height: 1.25;
    color: #333333;
}

/* THE X IS DRAWN, NOT TYPED. Jeff, 2026-08-23, on the per-dialog nudges the old glyph needed:
   "can you just adjust the glyph? (chop whitespace off I think, or scale to a symmetrical 'x')"
   - which is the right instinct, and the way to do it properly is to stop using a font glyph at
   all. `&times;` sits in a box whose padding is the type designer's decision, differs between
   the fonts on Windows, macOS and Android, and cannot be trimmed by CSS; every "nudge it up a
   bit" was really compensating for that box on one machine.

   Two lines in an SVG viewBox instead: the cross is exactly centred and exactly symmetrical by
   construction, on every platform, at every size. currentColor keeps the hover state working
   from CSS. The square button box then sits equidistant from the dialog's top and right padding
   with no correction at all - "needs to move up, equal distant to the side and top".

   Much smaller than the old calc(12px + 5vw), which was 76px on a laptop - the size in Jeff's
   screenshot. "Maybe the 'X' is a tiny bit smaller so it looks better without the optional
   title." */
/* Sized for FINGERS, not for looks. Jeff, 2026-08-23: "please make it extra-bold and a bit
   larger font. Part of the glyph choice was to make it easier for people with poor fine motor
   skills... A bigger target." The whole square is the target, not just the strokes, and it never
   drops below 34px - a shade over the 9mm that reads as comfortable on a phone. */
.modalClose {
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--modal-close-size);
    height: var(--modal-close-size);
    color: #8A8A8A;
    border-radius: 4px;
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
}
/* 58% of the button box, so the cross stays clear of the corners while the hit area around it
   stays generous. stroke-width is on the 24-unit viewBox, so 3.5 is genuinely heavy - the
   "extra-bold" Jeff asked for, and thicker strokes are easier to aim at as well as to see. */
.modalClose svg {
    width: 58%;
    height: 58%;
    display: block;
    stroke-width: 3.5;
}

.modalClose:hover,
.modalClose:focus {
    color: #000;
    text-decoration: none;
    background: #F0F0F0;
}

/* JEFF'S NUMBER, and the reasoning that got near it. He was comparing the icons to the title's
   letters - "I'm looking at the 'H' in Hangman (or B, D in Bloom Doom)... still slightly less
   than the height of those, making it look proper" - and then gave the figure directly: "Maybe 22
   or 23px if current is 16." Tried at 23, settled at 20: "They are a tiny bit big. But I think
   the exact measure may make them a bit small by optical illusion." He is right, and it is worth
   naming - a round or irregular mark has to be drawn slightly larger than its geometric match to
   READ as the same size, which is why the arithmetically exact 18 would have looked undersized.
   max() so that landscape and desktop, where 3vw is already larger and which he said "look and
   work well", are untouched.

   Two earlier stabs missed by reasoning about the wrong quantity, and both are worth remembering.
   16px was a floor under a 12px icon: three pixels, invisible, which is exactly the "they look
   the same" that came back. 32px then solved a different problem - a good thumb target, but 243%
   of the cap height of "B", which would have looked absurd beside the title.

   One measurement is worth keeping whatever the final number: every icon PNG is 256x256 with only
   192x192 of ink, so 25% of the box is transparent padding and the mark a person SEES is 75% of
   whatever this rule says. That gap between the box CSS sets and the ink the eye judges is why
   estimates from either end kept disagreeing. At 23px the ink reads about 17px against a measured
   cap height near 14px on a 430px viewport - a shade taller than the letters, which is what the
   eye wants rather than the arithmetic match. */
.iconButtonImage {
    width: max(20px, 3vw);
    padding: 0;
    border: thin solid transparent; /* Transparent non-hover border prevents shifting icons on hover.  */
    vertical-align: -.09em;
}

.iconButtonImage:hover {
    background-color: lightgray;
    border-color: black;
}

/* THE FOCUS RING, which tabindex makes necessary rather than optional (TODO 492). These icons are
   reachable from the keyboard now; a player tabbing to one has to be able to SEE where they are,
   or the reachability is theoretical. :focus-visible rather than :focus so a mouse click does not
   leave a ring behind, matching .poolBallDrag and the pool grid headings. */
.iconButtonImage:focus-visible {
    outline: 2px solid #566E3B;
    outline-offset: 2px;
    background-color: lightgray;
}

/* A 34-PIXEL TOUCH TARGET ON A TOUCH DEVICE, matching the dialog X's floor. The drawn size stays
   exactly as tuned above - the padding grows the box a finger has to hit, not the ink, which is
   what the long comment on .iconButtonImage is about. Scoped to a coarse pointer so the desktop
   layout that comment describes is untouched. */
@media (pointer: coarse) {
    .iconButtonImage {
        box-sizing: content-box;
        padding: max(0px, calc((34px - max(20px, 3vw)) / 2));
    }
}

/* The gaps between icon groups scale with the icons, or the row bunches up once the icons grow. */
.iconButtonSpacer {
    width: max(10px, 1.5vw);
    padding: 0;
}

/* Below 480px the gaps close steadily with the width (10px spacers and 7px tap padding at 480,
   2px each at 320), so the icons and the speaker stay on the title's line on a 320px phone. */
@media (max-width: 480px) {
    .iconButtonSpacer { width: clamp(2px, calc(5vw - 14px), 10px); }
}
@media (pointer: coarse) and (max-width: 480px) {
    .iconButtonImage { padding: clamp(2px, calc(3.125vw - 8px), 7px); }
}
/* A phone on its side: the heading shares the width with the drawing, so the gaps close the same way. */
@media (orientation: landscape) and (max-height: 480px) {
    .iconButtonSpacer { width: 2px; }
}
@media (pointer: coarse) and (orientation: landscape) and (max-height: 480px) {
    .iconButtonImage { padding: 2px; }
}

img {
    margin: auto;
}

.letterBtnActive {
    /* background-color: lightcyan;     /*  Original */
    background-color: #5EBFB6; /*  Alternate Blue: #1B8996 or #0BA3D2   */
    color: black;
}

/* COLOUR IS NOW THE THIRD WAY OF SAYING IT, NOT THE ONLY ONE (TODO 501). A hit and a miss were a
   green key and a red one and nothing else - which is no information at all to a player who cannot
   separate those two hues, and none to a screen reader either (play.js carries the words). The
   marks are shape. Both keys are disabled, so neither can be confused with a link. */
.letterBtnHit {
    /* background-color: darkgreen;      /*  Original */
    background-color: var(--game-accent); /*  Alternate Green: #B9CE2B    */
    color: white;
    text-decoration: underline;
    text-decoration-thickness: 0.12em;
    text-underline-offset: 0.1em;
}

/* A MISS IS CROSSED OUT, NOT STRUCK THROUGH (Jeff, 2026-09-16). The first version of this used
   text-decoration: line-through, which draws its line at the x-height - straight across the one
   thing on the key worth reading. Jeff: "the strikethrough... makes seeing the letters difficult
   ... instead we should use a semi-transparent, fairly thin, letter-box sized 'X' drawn over the
   letter box."

   So: two thin diagonals corner to corner, as background gradients rather than as a border, a
   pseudo-element or an image. Background-image paints ABOVE background-color and BELOW the glyph,
   which is exactly the stacking this wants - the X crosses the key while the letter stays on top
   of it, crisp and fully legible. It costs no element, no markup and no request, and it scales
   with the key because the stops are percentages of the box.

   Each gradient's colour band is 1.8px measured across the diagonal, and 0.42 alpha lets the key's
   own red through - "semi-transparent, fairly thin", not a black cross. */
.letterBtnMiss {
    /* background-color: darkred;       /*  Original */
    background-color: #A82E5D; /*  Alternate Red: #914318      */
    color: black;
    background-image:
        linear-gradient(to bottom right, transparent calc(50% - 0.9px), rgba(0, 0, 0, 0.42) calc(50% - 0.9px),
                        rgba(0, 0, 0, 0.42) calc(50% + 0.9px), transparent calc(50% + 0.9px)),
        linear-gradient(to bottom left, transparent calc(50% - 0.9px), rgba(0, 0, 0, 0.42) calc(50% - 0.9px),
                        rgba(0, 0, 0, 0.42) calc(50% + 0.9px), transparent calc(50% + 0.9px));
}

.missedLetter {
    /* For non-button letters... No background! */
    color: #A82E5D; /*  Alternate Red: #914318      */
}

/* Pressed, waiting for the server's answer. */
.letterBtnPending { opacity: 0.5; }


/* The Word Pool table: a live count as the merged banner row, then the difficulty-x-censorship
   matrix of real example words. The count is the headline; the matrix is the explanation. */
/* NO BORDERS anywhere on this table except the example-word cells themselves (Jeff, 2026-08-21:
   "Make all borders in the table disappear, except for the actual example words cells"). The
   sliders, rungs, axis labels and count are controls and captions around a grid; boxing them
   made the whole thing read as one big spreadsheet. */
.poolTable {
    width: 100%;
    /* Fixed, so the colgroup's widths govern rather than the words in the cells. */
    table-layout: fixed;
    border-collapse: collapse;
    margin: 1.2vmin 0 0 0;
    background: #FAFAFA;
    border: 0;
}

/* The count and its unit sit on one baseline-free line: a flex row, centred both ways, so the
   small "Words" hangs at the vertical middle of the big number rather than at its baseline
   (Jeff: "the word 'Words' is tiny and not vertically centered"). */
.poolHead {
    padding: 1vmin 0 0.6vmin 0;
    border: 0;
}
/* On the inner div, not the td - see renderPoolTable for why a flex td loses its colspan. */
.poolHeadRow {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.9vmin;
}


.poolCount {
    font-family: var(--mono);
    font-size: calc(26px + 2.4vmin);
    font-weight: 700;
    line-height: 1.1;
    color: #4A6B3D;
}


/* The headings ARE the preference controls now - there are no dropdowns above the table. Both
   axes get the same treatment so it reads as one grid of buttons rather than two widgets. */
.poolColHead,
.poolRowLabel {
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(9.5px + 0.35vmin);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: #888888;
    background: #F6F6F6;
    text-align: center;
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
    border: 0;
    transition: background .12s, color .12s;
}

/* The rungs are the thin strip between each slider and the word cells. Jeff, 2026-08-21: "Make
   the empty cells between the sliders and the word examples very narrow, and a bit darker when
   selected. Invisible when not." So: a few pixels wide, no background until lit, and lit one
   shade deeper than the cells so the band still frames the selection. Still clickable - click,
   shift-click and sweep all land on them - just a thinner target, which the slider balls beside
   them make up for. */
.poolColHead {
    padding: 0;
    height: calc(3px + 0.3vmin);
}

/* The grade names WRAP. They used to be nowrap, which meant "College and Career" and "Upper
   Elementary" held the whole table open at narrow widths instead of folding onto two lines.
   The trade is that a wrapped label is taller, so the row gets an explicit minimum height sized
   for two lines and the vertical padding comes down to keep ten of them from turning the table
   into a column. In a table cell `height` behaves as a minimum, which is exactly what is wanted:
   short labels sit at the floor, a wrapped one grows past it, and the drag handles follow either
   way because they are positioned from measured geometry rather than assumed row heights. */
/* The grade names are gone: only the two ends of the scale are named now, and the eight rungs
   between them carry no text. They stay clickable (click, shift-click and sweep all still work
   on them) and keep a real accessible name through aria-label, so losing the visible text costs
   a screen-reader user nothing. The minimum height still holds - it is what keeps a rung a
   usable tap target and stops the rows collapsing to hairlines now that most are empty. */
/* The rungs carry no text now, so the two-line minimum that used to hold "College and Career"
   is gone. What stays is a tap target: narrow, but tall enough to press. */
.poolRowLabel {
    padding: 0;
    height: calc(1.5em + 0.4vmin);
}

/* The unit beside the count. Quieter than the number, but no longer tiny. */
.poolCountUnit {
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(14px + 0.8vmin);
    font-weight: 600;
    color: #777777;
    letter-spacing: .04em;
    line-height: 1;
}

/* ---------------------------------------------------------------------------------------------
   The two axes. Each names only its ends, with the axis itself between them and an arrow pointing
   at each end. Naming every step is what put religion on the same line as methamphetamine; naming
   only the ends says which way the scale runs and claims nothing about the steps.
   --------------------------------------------------------------------------------------------- */
.poolAxisEnd {
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(8.5px + 0.3vmin);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: #8A8A8A;
    white-space: nowrap;
}

/* ONE rule for both axis names. The horizontal "Content Filter" and the rotated "Difficulty"
   used to have separate rules that had drifted apart in colour and size (Jeff: "The labels and
   arrows of each slider should be formatted the same"). The rotation is the only difference
   between them now, and that lives in its own rule below. */
.poolAxisName,
.poolAxisLabel {
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(9px + 0.32vmin);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .1em;
    color: #6E8B4E;
    white-space: nowrap;
}

/* The arrows are OUTSIDE the rotated label on purpose. writing-mode turns glyphs 90 degrees, so
   an arrow inside it points somewhere other than where it was written - the mistake this file has
   already made once. Kept upright, they mean what they look like.
   They are inline SVGs now (see axisArrow() in prefs.js): the typed arrow characters were
   hairlines at this size and bold did not help. Sized a little larger than the axis text, in the
   axis colour, so they carry the same weight as the words on either side of them. */
.poolAxisArrow {
    color: #6E8B4E;
    display: block;
    flex: 0 0 auto;
}
/* THE VERTICAL ARROWS FILL THE GAPS, and that is what stopped MAX drifting below the slider.
   Measured 2026-08-27: the stack's five children came to 279.7px inside a 229.1px cell, because
   each arrow claimed 74.7px of intrinsic height from its aspect ratio and `flex: 0 0 auto` let
   none of it give. space-between cannot make negative space, so the overflow all landed past the
   bottom - MAX sat 50.6px below its handle, which is the "quarter-line low" Jeff was still seeing
   after the first fix (that one corrected the label's own half-height and was right as far as it
   went). Growing from a zero basis, the arrows take exactly what is left between the two end words
   and the rotated name, which is also what the comment above says they are for. */
.poolAxisStack .poolAxisArrow { flex: 1 1 0; min-height: 0; height: auto; }
/* The name never shrinks: it is words, and clipping it would be a worse answer than a short
   arrow. */
.poolAxisStack .poolAxisLabel { flex: 0 0 auto; }
/* Only the cross-axis dimension is set; the viewBox's 4.5:1 aspect supplies the length. */
.poolAxisArrowH { height: calc(13px + 0.5vmin); width: auto; }
.poolAxisArrowV { width: calc(13px + 0.5vmin); height: auto; }

/* Both strips are laid out the same way: the two end words pinned to the far ends of the scale,
   the axis name dead centre, and each arrow centred in the gap between an end and the name
   (Jeff, 2026-08-21). space-between on five items does exactly that as long as the two end words
   are the same width - which "Min" and "Max" are, and which is the real reason the ends are
   those words and not "Kindergarten"/"Max". */
.poolAxisTop {
    padding: 0.2vmin 0 0.5vmin 0;
    border: 0;
    white-space: nowrap;
}
.poolAxisRow {
    display: flex;
    align-items: center;
    justify-content: space-between;
}

/* Difficulty: the same idea stood on end, and stretched to the full height of the slider beside
   it so Min and Max land level with the first and last rows. A rowspan cell has no definite
   height for height:100% to resolve against (the trap that collapsed the slider track earlier
   in this file), so the stack is positioned absolutely against the cell instead, the same way
   the vertical track is. */
.poolAxisStack {
    position: absolute;
    top: 0; bottom: 0; left: 0; right: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: space-between;
}
/* MIN AND MAX SIT ON THE HANDLES (Jeff, 2026-08-27: "the vertical Min - Max slider label starts
   (Min) a little low and ends a bit more low. It would be great if the MIN and MAX labels were
   horizontally lined up with the handles when at those far extents of their range").
   space-between puts the first label's TOP EDGE at 0 and the last label's BOTTOM EDGE at 100%,
   while a handle at either extreme is CENTRED on 0 and 100% - so each label sat half its own
   height out of true. Pulling each back by half its height puts the two centres on one line, at
   any font size, with nothing measured. */
.poolAxisStack .poolAxisEnd:first-child { transform: translateY(-50%); }
.poolAxisStack .poolAxisEnd:last-child { transform: translateY(50%); }

/* An unnamed rung: nothing at all until its row or column is selected. */
.poolScaleRung {
    background: transparent;
}
.poolScaleRung.poolHeadOn { background: #C3D4AE; }

/* The axis name, once, down the side. writing-mode turns it on its edge; sideways-lr keeps it
   reading bottom-to-top like an ordinary chart axis, and vertical-rl is the fallback where that
   is unsupported. NOTE, as elsewhere in this file: this rotation turns glyphs 90 degrees, which
   is why arrows have to be written pointing the "wrong" way - text is unaffected. */
/* Column widths, declared because table-layout: fixed will not derive them from content. The
   axis column is sized from the font its end words use: "Min"/"Max" run about 2.6 times the
   end-label font size, and the rotated axis name is narrower than that, so the column is exactly
   that wide with no padding ("Make that column zero whitespace"). Change the font and the column
   follows; change the end words to anything longer and this needs revisiting. */
.poolColAxis  { width: calc((8.5px + 0.3vmin) * 2.6); }
/* THE NAMES COLUMN (2026-08-30). Wide enough for "Specialized Language", the longest of the
   seven, at the size the names actually render - `ch` was the wrong unit here because a <col>
   inherits the TABLE's font, not the smaller one the names use, so 13ch measured the wrong
   characters and clipped every long name. The names may wrap as a safety valve; the row bands
   are measured after layout, so a two-line row costs nothing but height. */
.poolColName  { width: calc(106px + 4vmin); }
/* Wider than the ball, not equal to it. The drag handles grew when they gained grip dots
   (2026-09-03) and the vertical track ended up shoulder to shoulder with the grade names.
   Jeff: "The new handles are larger, so are now a bit crowded." The extra width is the gap. */
.poolColTrack { width: calc(28px + 1vmin); }
.poolColRung  { width: calc(3px + 0.3vmin); }

.poolAxisCell {
    white-space: nowrap;
    padding: 0;
    border: 0;
    text-align: center;
    vertical-align: middle;
    position: relative;          /* the absolutely-positioned stack measures against this */
}

.poolAxisLabel {
    display: inline-block;
    writing-mode: vertical-rl;
    writing-mode: sideways-lr;
}

.poolColHead:hover,
.poolRowLabel:hover { background: #ECECEC; color: #555555; }

.poolColHead:focus-visible,
.poolRowLabel:focus-visible { outline: 2px solid #6E8B4E; outline-offset: -2px; }

/* Selected heading. Same green as a live cell, one shade deeper, so the lit row band and lit
   column band visibly frame the lit region of the grid. */
.poolHeadOn {
    background: #DCE7CE;
    color: #3C4A2C;
}

.poolCorner {
    border: 0;
    padding: 0;
}

/* ---------------------------------------------------------------------------------------------
   The two sliders. They live INSIDE the table - a cell spanning the censorship columns for the
   horizontal one, a cell spanning all ten body rows for the vertical one - because a slider
   floating beside the grid would not line its handles up with the rows and columns it controls,
   and that alignment is the entire point of the control.
   --------------------------------------------------------------------------------------------- */
.poolTrackCell {
    padding: 0;
    border: 0;
}

/* position:relative + inset:0 on the track, NOT height:100%. A rowspan cell has no definite
   height for a percentage to resolve against, so height:100% collapsed the vertical track to
   zero and stranded both grade handles at the top of the table. Caught by measuring the rendered
   page; the vm harness could not see it, because it tests the math and this was a layout fact. */
.poolTrackCellV {
    position: relative;
}

.poolTrack {
    position: relative;
}

/* The horizontal track gets real air under it for the same reason: 0.2vmin put the ball's
   bottom edge against the column headings it points at. */
.poolTrackH { height: calc(14px + 1vmin); margin: 0 0 calc(6px + 0.6vmin) 0; }

/* THE CUSTOM STATE. A selection made in Advanced usually matches none of the four steps, and the
   handle then sits on the widest step the mix covers - honest about safety, silent about being an
   approximation. Same treatment as the Advanced master's "Mixed": the VALUE is what changes
   appearance, never the control, because the control is still what resolves the state. Dragging it
   replaces the mix with that step's exact preset, which is why nothing here needs to be disabled
   or explained further. */
.poolTrackCustom .poolFill { opacity: 0.45; }
.poolTrackCustom .poolBallDrag { filter: saturate(0.25); }
.poolAxisCustom { font-style: italic; font-weight: 400; opacity: 0.7; }
/* Inset on the right so the vertical rail keeps its distance from the first column of the grid;
   the balls are centred on the rail, so this is what actually moves them off the names. */
.poolTrackV { position: absolute; top: 0; bottom: 0; left: 0; right: calc(8px + 0.5vmin); }

/* The rail is inset by half a ball at each end so the balls never hang off the track, and the
   fill is measured against the FULL track, so the two agree at 0% and 100%. */
.poolRail {
    position: absolute;
    background: #E2E2E2;
    border-radius: 999px;
}
.poolTrackH .poolRail { left: 0; right: 0; top: 50%; height: 4px; transform: translateY(-50%); }
.poolTrackV .poolRail { top: 0; bottom: 0; left: 50%; width: 4px; transform: translateX(-50%); }

.poolFill {
    position: absolute;
    background: #9BB37A;
    border-radius: 999px;
}
.poolTrackH .poolFill { top: 50%; height: 4px; transform: translateY(-50%); }
.poolTrackV .poolFill { left: 50%; width: 4px; transform: translateX(-50%); }

/* A THUMB, NOT A DOT. These were circles and users did not read them as grabbable (Jeff,
   2026-09-02: "They don't recognize them as handles"). Three things were wrong and the symbol
   was only the third.
   Shape: a circle centred ON a track reads as an INDICATOR - something the system set to show
     you where you are. A rounded rectangle standing proud of the rail reads as a physical thumb
     you could pinch. Same pixels, opposite meaning.
   Size: these were ~13px. Under about 44px a target reads as decorative no matter what is drawn
     on it, so the drag handles are now nearer that while staying small enough not to hide the
     row they point at.
   Grip: dots. Notion, Trello, Jira and every mobile reorder list use a dot grid, which makes it
     the single most-recognised "drag me" mark there is - ahead of grip lines, which collide
     with the hamburger-menu meaning. */
.poolBall {
    position: absolute;
    width: calc(15px + 0.55vmin);
    height: calc(24px + 0.8vmin);
    border-radius: 5px;
    background: #FFFFFF;
    border: 2px solid #6E8B4E;
    box-sizing: border-box;
    box-shadow: 0 1px 3px rgba(0, 0, 0, .28);
}
/* The vertical track's handle slides up and down, so it lies the other way about. */
.poolTrackV .poolBall {
    width: calc(24px + 0.8vmin);
    height: calc(15px + 0.55vmin);
}
.poolTrackH .poolBall { top: 50%; transform: translate(-50%, -50%); }
.poolTrackV .poolBall { left: 50%; transform: translate(-50%, -50%); }

/* Six dots in a 2x3 grid, drawn with one gradient rather than six elements. Only on the
   draggable handles - the fixed one must keep looking immovable. */
.poolBallDrag { position: absolute; }
.poolBallDrag::after {
    content: '';
    position: absolute;
    inset: 0;
    margin: auto;
    width: 8px;
    height: 12px;
    background-image: radial-gradient(circle at 2px 2px, #6E8B4E 1.15px, transparent 1.25px);
    background-size: 4px 4px;
    pointer-events: none;      /* the dots must never swallow the drag */
}
.poolTrackV .poolBallDrag::after { width: 12px; height: 8px; }

/* The fixed ball is why there is no way to switch Fully Censored off, so it has to LOOK
   immovable: filled in, no shadow, no grab cursor. */
.poolBallFixed {
    background: #6E8B4E;
    cursor: default;
    box-shadow: none;
}

.poolBallDrag {
    cursor: grab;
    touch-action: none;          /* or the browser scrolls the page instead of moving the handle */
}
.poolBallDrag:active { cursor: grabbing; }
.poolBallDrag:hover { border-color: #566E3B; }
.poolBallDrag:focus-visible { outline: 2px solid #566E3B; outline-offset: 2px; }

.poolCell {
    font-family: var(--mono);
    font-size: calc(9.5px + 0.4vmin);
    text-align: center;
    padding: 0.55vmin 0.4vmin;
    border: 1px solid #E4E4E4;
    color: #AAAAAA;          /* Cells OUTSIDE the current selection read as muted... */
    overflow-wrap: break-word;
}

.poolCellIn {
    color: #333333;          /* ...and the ones your settings actually serve read as live. */
    background: #EFF4EA;
}

/* Shown but not served: the content filter admits this column, so the words are safe to read,
   but the reading level excludes the row. Same treatment the Advanced control uses - medium dark
   grey on white - so "you could have this" and "you have this" are told apart at a glance
   (Jeff, 2026-08-30). */
.poolCellDim {
    color: #6B6B6B;
    background: #FFFFFF;
}

.poolNa {
    color: #CCCCCC;
    font-style: italic;
}

/* The notice dialog's button. Everything else it needs it inherits from .modal / .modal-content. */
.noticeOk {
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(11px + 0.45vmin);
    padding: 0.7vmin 3vmin;
    border: 1px solid #6E8B4E;
    border-radius: 4px;
    background: #DCE7CE;
    color: #3C4A2C;
    cursor: pointer;
}
.noticeOk:hover { background: #CBDBB8; }
/* The person check, inside a notice (showChallenge). */
#turnstileBox { margin: 0.8em 0 0.2em; }

/* The preferences dialog gets real side margins inside the box: a share of the width when there
   is room, a few pixels on a phone, never a lot (Jeff, 2026-08-21: "a percentage of the
   available width, reducing to a minimal amount for tiny screens... maybe 5% to 10% on each
   side"). clamp() is exactly that sentence. The other dialogs keep the 1vw they had. */
#divPreferenceDialog {
    position: relative;                      /* the close button anchors to the box, not the padding */
    padding: clamp(4px, 3.5%, 36px) clamp(4px, 3.5%, 36px) 1.6vmin clamp(4px, 3.5%, 36px);
}

/* ---------------------------------------------------------------------------------------------
   The score band, 2026-08-21. "Win now for 280": a quiet label and a loud number, so the number
   is read as what a win is worth this instant rather than as a score already earned.
   --------------------------------------------------------------------------------------------- */
/* The number is set exactly like the title - same face, same size - and takes the theme's
   accent (see applyThemeStyle() in look.js for where --game-accent comes from). */
.scoreNum {
    font-family: var(--mono);
    font-size: calc(6px + 4vw);
    font-weight: 700;
    color: var(--game-accent);
}

/* The small high-scores link under the score band - a text link on purpose; the icon row is
   full and an icon would claim more than this deserves. */
.hsLink {
    display: inline-block;
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(9.5px + 0.35vmin);
    color: #6E8B4E;
    text-decoration: none;
    letter-spacing: .05em;
    text-transform: uppercase;
    margin: -0.6vmin 0 0.8vmin 0;
}
.hsLink:hover { text-decoration: underline; }

/* Two questions side by side, each a stack (Jeff, 2026-08-27). The gap above is what stopped
   "Look:" colliding with the difficulty slider's MAX label - they were adjacent full-width rows
   and the first sat right under the scale. */
/* LEFT JUSTIFIED since 2026-08-30 (Jeff: "In prefs, let's move keyboard and game look to left
   justified (works better with the other changes)"). Flush with the section headings and their
   rules, same as #wordPoolMatrix and the filter explanation below - a centered pair of columns
   above three left-aligned sections read as a different screen. */
.prefChoiceCols {
    display: flex;
    justify-content: flex-start;
    gap: 3.5vmin;
    margin: 1.6vmin 0 0.4vmin 0;
    flex-wrap: wrap;
    text-align: left;
}
.prefChoiceCol {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.25em;
    font-size: calc(9px + 0.32vmin);
    font-family: Arial, Helvetica, sans-serif;
}
/* One choice per line. The Look radios are emitted by renderThemeRadios() into a span, so they
   need to be block-level themselves - a column container does not break inline children, which is
   why Hangman and Bloom Doom stayed side by side after the columns landed. */
.prefChoiceCol label, .prefChoiceItem { display: block; margin: 0; cursor: pointer; white-space: nowrap; }
.prefChoiceCol > span { display: block; }
.prefChoiceCol input { vertical-align: -1px; margin-right: 0.35em; }
.prefThemeLabel { font-weight: 700; color: #6E8B4E; text-transform: uppercase; letter-spacing: .06em; font-size: 0.9em; }

/* The transit dialog after a game. Narrower than the other dialogs: it is a card, not a page. */
/* Wide enough that the five verdict buttons sit on one row when the screen allows it. */
.finishDialog { width: min(92vw, 640px); }
.finishPool { color: #9B4623; }
.finishPool:empty { display: none; }

.finishVerdicts {
    margin: 1.4vmin 0 0 2vw;
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(10.5px + 0.4vmin);
    color: #555555;
    display: flex;
    flex-wrap: wrap;
    gap: 0.8vmin;
    align-items: center;
}
/* A group never breaks internally, so the row either fits whole or breaks between the groups -
   i.e. after 'Inappropriate'. */
.verdictGroup { display: inline-flex; gap: 0.8vmin; flex-wrap: nowrap; }
.finishWord { font-weight: 700; color: #4A6B3D; font-family: var(--mono); }
.verdictBtn {
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(10px + 0.35vmin);
    padding: 0.4vmin 1.2vmin;
    border: 1px solid #C3D4AE;
    border-radius: 999px;
    background: #F6F6F6;
    color: #3C4A2C;
    cursor: pointer;
}
.verdictBtn:hover { background: #EFF4EA; }
.verdictBtn.verdictChosen { background: #6E8B4E; border-color: #6E8B4E; color: #FFFFFF; }
/* The two thumbs (sql/171): black-on-transparent inline SVGs, no button chrome (Jeff,
   2026-08-30: "transparent button outlines and make them black and white"). Chosen keeps the
   icon black and puts a quiet tint behind it, since the outline can no longer carry the state. */
.verdictFace {
    background: transparent; border-color: transparent;
    padding: 0.3vmin 1vmin; line-height: 1; color: #1F2430;
}
.verdictFace svg { width: calc(16px + 1.2vmin); height: calc(16px + 1.2vmin); display: block; }
.verdictFace:hover { background: #EFF4EA; border-color: transparent; }
.verdictFace.verdictChosen { background: #CFE0BC; border-color: transparent; color: #1F2430; }
/* Report: quieter than the thumbs, never hidden, vertically centered on their line with a
   little extra distance from the dislike thumb (Jeff, 2026-08-30). Chosen state borrows the
   pill look so the toggle reads. */
.reportWordLink {
    font-size: calc(9.5px + 0.3vmin);
    /* Vertically centered on the thumbs' line (Jeff's screenshot showed it riding high): the
       base .hsLink carries a NEGATIVE top margin for its usual under-the-score placement, so
       this zeroes the vertical margins outright and lets flex centering do the job. */
    margin: 0 0 0 3vmin;
    align-self: center;
    line-height: 1;
}
.reportWordLink.verdictChosen {
    background: #6E8B4E; color: #FFFFFF; border-radius: 999px; padding: 0.2em 0.8em;
    text-decoration: none;
}

/* "We'll review this word." above the comment box - the promise, then the (optional) box. */
.finishNoteLead { flex: 0 0 100%; font-size: calc(10px + 0.35vmin); color: #4a4238; }
.finishNoteRow {
    flex-wrap: wrap;
    margin: 1vmin 0 0 2vw;
    display: flex;
    gap: 0.8vmin;
    align-items: center;
    flex-wrap: wrap;
}
.finishNote {
    flex: 1 1 14em;
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(10.5px + 0.4vmin);
    padding: 0.5vmin 0.8vmin;
    border: 1px solid #CCCCCC;
    border-radius: 4px;
}
.finishThanks { font-family: Arial, Helvetica, sans-serif; font-size: calc(10px + 0.35vmin); color: #6E8B4E; }

/* The definition block in the finish dialog (sql/130). Quiet type under the result line; the
   'say it' control is a small round button so it reads as a speaker, not a form. */
.finishDefinition { margin-top: 0.6vmin; }
.finishDefinition:empty { display: none; }

/* THE QUIZ ENDING (sql/229). It stands where the definition would have been, and is the one place
   on this dialog that asks the player for prose - so it gets the room to write it in, and the
   lead line sits above the box rather than beside it. */
.finishQuiz { margin: 1.2vmin 2vw 0 2vw; }
.finishQuizLead {
    display: block;
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(11px + 0.4vmin);
    color: #4a4238;
    margin-bottom: 0.6vmin;
}
.finishQuizAnswer {
    display: block;
    width: 100%;
    box-sizing: border-box;
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(10.5px + 0.4vmin);
    padding: 0.6vmin 0.8vmin;
    border: 1px solid #CCCCCC;
    border-radius: 4px;
    resize: vertical;
}
.defLine { margin: 0; }
.defLine .finishWord { font-size: 1.05em; }
.defMore { margin-top: 0.3em; padding-left: 1.2em; }
.defSep { margin: 0 0.45em 0 0.1em; color: #7a7062; }
.defPos { font-style: italic; color: #7a7062; }
.defAs { color: #7a7062; }
.sayItBtn {
    display: inline-block;
    margin-left: 1.2em;
    padding: 0 0.55em;
    border: 1px solid #C3D4AE;
    border-radius: 999px;
    background: #F6F6F6;
    color: #3C4A2C;
    font-family: Arial, Helvetica, sans-serif;
    font-size: 0.85em;
    line-height: 1.7;
    cursor: pointer;
    vertical-align: 1px;
}
.sayItBtn:hover { background: #EFF4EA; }
.sayWrongBtn { margin-left: 0.8em; padding: 0; border: 0; background: none; color: #6E8B4E; font-family: Arial, Helvetica, sans-serif;
    font-size: 0.8em; cursor: pointer; white-space: nowrap; text-decoration: underline; }
.sayWrongDone { color: #7a7062; cursor: default; text-decoration: none; }
.defAttribution { display: block; margin-top: 0.3vmin; font-family: Arial, Helvetica, sans-serif; font-size: calc(8.5px + 0.3vmin); color: #9a9084; }
/* The word's reading level, stated on every summary (Jeff, 2026-08-30). Quieter than the
   definition it follows - it is a fact line, not the payoff. */
/* Below the citation lines, "with a little space for paragraph separation" (Jeff, 2026-08-30). */
.defLevelLine { margin: 1.4vmin 0 0; font-family: Arial, Helvetica, sans-serif; font-size: calc(10px + 0.35vmin); color: #6f675c; }
/* A recent-words row links back to that game's summary box; dressed as a link so it looks
   clickable, in the words' own color so the table stays calm. */
.statsWordLink { color: inherit; text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }

/* The mode switch sits above Next word, close enough to read as "what that button will start". */
.finishActions { text-align: center; margin-top: 1.2vmin; }
.finishNext { font-size: calc(13px + 0.55vmin); padding: 0.9vmin 4vmin; }
.finishScoresLink { display: block; margin: 1.2vmin 0 0 0; }

/* ---------------------------------------------------------------------------------------------
   HIGH SCORES, as an early-80s cabinet. Jeff, 2026-08-23: "It needs to be modeled after an early
   1980's arcade game. Not exactly necessarily, but less words, more (bigger) scores... Relative
   to the viewport of course."

   Three streaks across, wrapping to a column on a phone. Everything is monospace and the numbers
   are the largest thing on the screen, because on a cabinet the score WAS the screen. No row
   striping, no highlight for your own line - you found yourself by recognising your initials.
   --------------------------------------------------------------------------------------------- */
.scoreBoards {
    display: flex;
    flex-wrap: wrap;
    gap: 3vmin;
    justify-content: space-around;
    margin: 0.6vmin 0 1vmin 0;
}
.scoreBoard { flex: 1 1 12em; min-width: 8em; }
/* A phone held upright: the three boards stack, and the title's Nice | Mean | Evil takes a line of
   its own instead of crowding the close X (Jeff, 2026-09-26). */
@media (max-width: 600px) {
    .scoreBoards { flex-direction: column; align-items: stretch; }
    .scoreBoard, body.classroom .scoreBoard { flex: none; width: 100%; max-width: none; }
    #scoresTitle .scoresModeSwitch { display: flex; width: max-content; margin: 0 auto 0.4em; }
}
/* Courier, like the dialog titles and the rows - Jeff, 2026-08-25: "lets use courier New or
   Courier to match the titles". This was the one Arial thing on the board, and it read as a
   different component sitting on top of the scores rather than a label belonging to them. */
.scoreBoardTitle {
    margin: 0 0 0.8vmin 0;
    text-align: center;
    font-family: var(--mono);
    font-size: calc(9.5px + 0.4vmin);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .08em;
    color: #8A8A8A;
}
.scoreBoardEmpty {
    text-align: center;
    font-family: var(--mono);
    font-size: calc(9.5px + 0.35vmin);
    font-style: italic;
    color: #AAAAAA;
    margin: 0;
}
/* The two columns hug each other under the title rather than being flung to the edges of the
   board. width:100% did the flinging - on a wide dialog each board is a third of ~800px, so
   initials sat far left and the score far right with a gulf between (Jeff, 2026-08-24: "We need
   the initials and the score to be much closer together, right under the 5-Streak column
   label"). Shrink-to-fit and centred instead, with the gap between the columns spelled out. */
.scoreBoardTable {
    /* Full width of its board column: the name column takes the slack, so the scores sit in
       their own right-edge column with real air between - the "invisible grid" Jeff asked for
       (2026-08-30) after padding alone kept reading as run-together. */
    width: 100%;
    border-collapse: collapse;
    margin: 0 auto;
}
.scoreBoardTable .scoreWho { width: 100%; }
.scoreBoardTable .scoreNum { text-align: right; }
.scoreBoardTable td {
    padding: 0.25vmin 0;
    border: 0;
    font-family: var(--mono);
    /* The headline of the whole dialog. vmin so it tracks the smaller viewport edge and stays
       big on a phone in portrait without overflowing in landscape. */
    font-size: calc(13px + 1.1vmin);
    font-weight: 700;
    line-height: 1.35;
}
/* ALIGNMENT, 2026-08-25. Jeff: "lets add a small amount of space between the 3 initials and the
   score... make the initials all line up perfectly. Maybe make the scores all left justified in
   their space so they line up too."

   Three details make it line up, and all three are needed:

   1. A fixed width on the initials column. Courier is monospace, so three characters are always
      the same width - but `width: auto` still lets the column shrink to whatever the widest row
      happens to be, which differs between the 5-, 50- and 500-Streak boards sitting side by
      side. 3ch plus the letter-spacing pins all three boards to one measure.
   2. letter-spacing added to the LAST character too, which is what padding-right compensates
      for: CSS puts the gap AFTER each glyph including the final one, so a right-aligned or
      shrink-wrapped run of initials sits half a space left of where the eye expects.
   3. Scores LEFT-aligned. Right-aligning them lines up the ones digit, which sounds correct and
      is why it was written that way - but it ragged the left edge, so 606 and 1,208 started in
      different places and the column read as crooked. Left is what Jeff asked for and it is
      also what the arcade cabinets did.
   The gap itself is padding-left on the number, deliberately small - "a small amount of space". */
/* NO FIXED WIDTH ANY MORE. `width: 3ch` was right when every name was three letters and wrong the
   moment the board started showing usernames (sql/158): a longer name overflowed its cell and ate
   the number's gap, which is the "lost space between the username and the scores" Jeff saw. The
   column sizes itself from the longest name now, and the gap is padding the name OWNS rather than
   space the number hopes to find - a gap that lives on the wrong side of an overflow is a gap that
   disappears exactly when it is needed.
   The classroom board still shows three letters and is unaffected: auto is 3ch there. */
.scoreWho {
    text-align: left;
    color: #4A6B3D;
    letter-spacing: 0.08em;
    padding-right: 1.1em;
    white-space: nowrap;
}
.scoreNum {
    text-align: right;
    color: var(--game-accent);
    white-space: nowrap;
}

/* The confetti canvas: over everything, through to nothing. */
.confettiCanvas {
    position: fixed;
    inset: 0;
    width: 100vw;
    height: 100vh;
    z-index: 2000;
    pointer-events: none;
}

/* Section headings on the rewritten About page. */
.aboutHeading {
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(11px + 0.5vmin);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: #6E8B4E;
    margin: 2.4vmin 0 0.6vmin 0;
}

/* The About page has no title any more (Jeff: "It's just noise now"), so its first section
   heading is the first thing in the box and must not start with the heading's own top margin -
   it would float the text down below the close button for no reason. */
.aboutFirst { margin-top: 0.6vmin; }

/* ---------------------------------------------------------------------------------------------
   The standing band, where the live score used to be: the last-five average on the left, the
   personal best on the right, the high-score link between them. Neither number is about the word
   on screen, which is why they can be shown while the word's own score cannot.
   --------------------------------------------------------------------------------------------- */
.standingBand {
    display: flex;
    /* center, not baseline: the label sits beside a much larger number, and baseline alignment
       drops it to the number's feet. Jeff, 2026-08-23: "The score labels a little bigger and
       vertically centered." */
    align-items: center;
    justify-content: center;
    gap: 1.3em;
    flex-wrap: wrap;
    margin: 0.2vmin 0 0.4vmin 0;
    font-family: var(--mono);
}
.standingBand:empty { display: none; }
.standingLive { display: inline-flex; align-items: center; gap: 0.45em; }
/* Evil's count (sql/259): "312 words fit", in the label's face and colour so it reads as a fact
   about the word in play, not a fourth score. Filled by drawEvilAlive(), hidden otherwise. */
.standingEvil {
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(12px + 0.7vmin);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: #7a7062;
    line-height: 1.1;
}
/* The word in play's live score (sql/247) leads the band; "timed" beside it, small and in the
   High Scores yellow, says the number is melting. */
/* The mode word in front of the look's name (2026-09-09): the band mark's font and colour, at a
   bit over half the heading's size. */
h1 .titleMode {
    font-family: Arial, Helvetica, sans-serif;
    font-size: 0.58em;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .08em;
    color: #b8860b;
    vertical-align: 0.12em;
}
/* Points coming back (mean-4): the number swells while it climbs, then settles. */
@keyframes standingLiveUp {
    0%   { transform: scale(1); }
    35%  { transform: scale(1.45); color: #b9ce2b; }
    100% { transform: scale(1); }
}
.standingLiveUp { animation: standingLiveUp 1.1s ease-out; display: inline-block; transform-origin: center; }
/* A tick (mean-5, 2026-09-09): red and a little larger for a fifth of a second - up in the first
   half, back in the second - without moving anything around it (a transform takes no layout). */
@keyframes standingLiveTick {
    0%   { transform: scale(1); }
    50%  { transform: scale(1.22); color: #c62828; }
    100% { transform: scale(1); }
}
.standingLiveTick { animation: standingLiveTick 0.2s ease-in-out; display: inline-block; transform-origin: center; }
/* The player's totals, on the finish dialog since 2026-09-09 (they used to crowd the band). */
.finishStanding:empty { display: none; }
.finishStanding { color: #7a7062; font-weight: 700; }
/* Larger and bold - Jeff, 2026-08-24: "Make that wording larger and bold." The labels carry
   real words now (Last 5-Streak, Best 5-Streak), so they can hold their own next to the big
   numbers instead of whispering beside them. */
.standingLabel {
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(12px + 0.7vmin);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: #7a7062;
    line-height: 1.1;
}
.standingNum {
    font-size: calc(14px + 2.1vw);
    font-weight: 700;
    color: var(--game-accent);
    line-height: 1;
}
/* High Scores rides the same band, pushed to the right edge and in a deep dark yellow so it
   reads as the door to a different room rather than a fourth statistic. margin-left:auto is
   what right-justifies it inside the flex row. */
/* Long labels by default, short ones on a phone held upright. 600px rather than a tighter
   number because Jeff asked for "common (even larger) vertical phone screens" - a Pixel Pro is
   412px and was already wrapping, and there is nothing lost by shortening a little early. */
.standingLabelShort { display: none; }
@media (max-width: 600px) {
    .standingLabelLong { display: none; }
    .standingLabelShort { display: inline; }
    /* The band tightens with the words, so the shorter labels buy room rather than just leaving
       a gap where the old ones were. */
    .standingBand { gap: 0.8em; }
}

.standingLink {
    margin: 0 0 0 auto;
    font-size: calc(12px + 0.7vmin);
    font-weight: 700;
    color: #9a7d00;
}

/* ---------------------------------------------------------------------------------------------
   The word-feedback review queue (views/games/nothangman-admin.html). Dense on purpose: it is a
   worklist, read top to bottom, not a page to linger on.
   --------------------------------------------------------------------------------------------- */
#reviewPage { max-width: 1000px; margin: 0 auto; padding: 2vh 3vw 6vh; }
.reviewTable { width: 100%; table-layout: fixed; margin: 0 0 8vw 0; }
.reviewTable th { text-align: left; white-space: nowrap; }
.reviewTable td { padding: 0; vertical-align: top; }
.reviewWord {
    font-family: var(--mono);
    font-weight: 700;
    color: var(--game-accent);
}
.reviewActions { white-space: nowrap; }
.reviewActions .verdictBtn { margin-left: 0.3em; }
.reviewNotes { margin: 0 0 0.6em 1.2em; padding: 0; color: #555555; font-size: 0.95em; }
.reviewNotes li { margin: 0.1em 0; }

/* ---------------------------------------------------------------------------------------------
   CLASSROOM MODE (sql/142). The page gets body.classroom the moment a class URL is recognised,
   and the mode is mostly SUBTRACTION, done here rather than in the render code so the icon runs
   and the site bar keep one shape everywhere: what a student must not see is display:none, not
   conditionally-unrendered. Jeff, 2026-08-25: "Probably no info, about or preferences" - and no
   site bar at all, because every link on it (Shop, Contact, sign-in) leaves the game, which is
   the exact thing the classroom version exists to not do.
   --------------------------------------------------------------------------------------------- */
/* The top bar RETURNED to classrooms on 2026-08-25 - branding only, links dead. Jeff: "it feels
   fair to say that it is important to identify who is developing and hosting this for the
   player... equivalent to a photo credit. Not advertising." So the brand shows and goes nowhere;
   the action buttons (Games / Contact / Shop / sign-in) stay gone. */
body.classroom .gi-bar-actions, body.classroom .gi-bar .gi-account-slot { display: none; }
/* Dead for a seat or a school account, live for a personal account that joined (NH00580):
   classDoor.js lockBrand() sets the class and takes the href with it. */
body.classroom-locked .gi-brand { pointer-events: none; cursor: default; }

/* THE FOOTER CAME BACK THE SAME WAY, 2026-09-19 (a classroom-compatible footer, NH00034). It was
   display:none because every link in it is a door out of the game - true of Etsy, Printables and
   Contact, and not true of Privacy and Terms. Hiding the lot meant the two pages a school or a
   parent actually asks for were the two a classroom could not reach.
   Subtraction, like the rest of this block: the footer stays and loses the doors. Its brand goes
   dead rather than away, exactly as the top bar's does, for the photo-credit reason above. The
   copyright line is not a link and stays. Jeff, 2026-09-19: "take the existing bar and remove all
   links except the privacy and terms". */
body.classroom > .gi-footer .gi-footer-links a:not([href$="/privacy"]):not([href$="/terms"]) { display: none; }

/* AND THEN IT BECAME A BAND, 2026-09-19. Jeff: "shrink the classroom footer... Get rid of the
   logo and company name from the footer (it's in the header and once is enough)... just put the
   HR above everything there, and have the copyright line on the left, and the privacy and terms
   on the right."

   So the marketing footer's two rows collapse into one: .gi-footer-grid stops being a box (display:contents)
   so the links become direct children of .wrap beside the copyright, and .gi-fine-print takes
   order:-1 to sit left of them despite coming after them in the markup. The rule above already
   removed the doors out; this removes the branding, the second fine-print line, and the height. */
body.classroom > .gi-footer { padding: 7px 0; border-top: 1px solid rgba(243, 236, 221, 0.12); }
body.classroom > .gi-footer::before { display: none; }          /* the 4vh spacer the tall footer wanted */
body.classroom > .gi-footer .gi-footer-inner {
    display: flex; align-items: center; justify-content: space-between;
    gap: 14px; flex-wrap: wrap;
}
body.classroom > .gi-footer .gi-footer-grid { display: contents; }
body.classroom > .gi-footer .gi-brand { display: none; }
body.classroom > .gi-footer .gi-footer-links { gap: 18px; }
body.classroom > .gi-footer .gi-fine-print {
    order: -1; margin: 0; padding: 0; border-top: 0; font-size: 0.74rem;
}
body.classroom > .gi-footer .gi-fine-print span + span { display: none; }   /* the Pacific Northwest line */

/* LOW ON THE PAGE, WITHOUT REBUILDING THE PAGE. `top: 100vh` on a sticky element parks it at the
   bottom of the viewport when the content is short, and lets it scroll away normally when the
   content is tall - which is the behaviour already chosen here ("Tall is fine if the user has to
   scroll to see it"). The obvious alternative, making body a flex column with margin-top:auto,
   reaches the same place by turning every child of the game into a flex item, and this page sizes
   itself from the window with negative margins. Not worth the blast radius for a thin band. */
body.classroom {
    /* Minus the page margin, twice: body carries margin:2vw and min-height measures the box
       inside it, so a plain 100vh overshoots the viewport by exactly that margin. --page-margin
       is the name this file already gives that value, for the header and footer's negative
       margins - the same number, asked for rather than retyped. */
    min-height: calc(100vh - 2 * var(--page-margin, 0px));
    display: flex; flex-direction: column;
}
body.classroom > .gi-footer { margin-top: auto; }
body.classroom img[data-action="showPreferences"],
body.classroom img[data-action="showHelp"],
body.classroom img[data-action="showAbout"] { display: none; }
/* The finish dialog's link says "high scores"; in a classroom it opens the class board, which
   is fine, so it stays. The verdict row also stays - a class full of "too easy" is exactly the
   feedback the pool wants. */

/* The join box: the arcade entry's typography at the center of a dialog. Wide letter spacing so
   three characters read as three slots, and uppercase because the board is uppercase. */
.classJoinInput {
    width: 5.5em;
    text-align: center;
    font-family: var(--mono);
    font-size: calc(18px + 1.2vmin);
    font-weight: 700;
    letter-spacing: 0.35em;
    text-transform: uppercase;
    border: 2px solid var(--game-accent);
    border-radius: 6px;
    padding: 0.25em 0 0.25em 0.35em;
}
.classJoinError {
    text-align: center;
    color: #A33B3B;
    font-weight: 700;
    min-height: 1.2em;
    /* Room above it: this line sits directly under the letters box, and on the account door it
       carries the list of domains the class accepts rather than an error - a standing fact that
       read as a complaint about what had just been typed. */
    margin: 0.9em 0 0.2em;
}
/* Your own row on the class board: the cabinet let you find yourself by your initials; a class
   of near-identical three-letter combos deserves one gentle underline more. */
.classBoardYou { text-decoration: underline; text-underline-offset: 3px; }
/* ONE COLUMN, CENTRED, AT THE PUBLIC BOARD'S OWN MEASURE (Jeff, 2026-09-05: "The class high score
   screen has the names and scores justified left and right for way too huge separation. However
   we are doing it for normal games, use the same spacing, and just center the single column.").

   The cause was flex-grow, not the table. `.scoreBoard` is `flex: 1 1 12em`, which is right when
   three of them share the dialog and wrong when there is one: the single board grew to the whole
   ~800px, the table inside is width:100%, and `.scoreWho { width: 100% }` then shoved the name to
   the far left and the score to the far right with a gulf between them. Stopping the growth is
   the whole fix - `space-around` on the container was already centring it - and it leaves the
   internal spacing byte-for-byte the same as a public column's.

   The old `padding-right: 1.6em` override went with it. It was compensating for the gulf by
   widening the gap, which is the opposite of what the problem needed, and it is exactly the
   "same spacing" the fix is supposed to restore.

   A CAP RATHER THAN `flex: 0 1 12em`, which was the first attempt and was wrong in the other
   direction: the 12em BASIS is not a public column's width, because those grow to fill the row -
   three of them share the dialog, so one is about a third of it, and 12em resolves to well under
   half that. Killing the growth squeezed the class board to ~100px and ran the initials into the
   score. A third of the row is what a public column actually gets, so a third is what this asks
   for, and space-around centres what is left. */
body.classroom .scoreBoard { max-width: 33%; }

/* The character keys, shared by the sign-in grid and the rename grid - see .classCharPad for the
   layout they sit in. Jeff, 2026-08-25, on why a picker at all: "Maybe we present the hangman
   letter picker UX for pin/password creation?" So they wear the game's own letters: same Courier,
   same green. */
.classSecretKey {
    font-family: var(--mono);
    font-size: calc(11px + 0.9vmin);
    font-weight: 700;
    /* A finger target, floored in px so it survives a small phone - the same lesson the game
       icons taught (see JEFF'S NUMBER above). */
    min-height: max(30px, 2.2em);
    padding: 0.1em 0;
    color: #3F5A33;
    background-color: #F3F0E8;
    border: thin solid #9BB37A;
    border-radius: 4px;
    cursor: pointer;
}
.classSecretKey:hover { background-color: #E4EDD8; }
.classSecretKey:active { background-color: #5EBFB6; color: black; }
/* What has been chosen, one dot per letter. The three unfilled dots are the minimum length made
   visible, so "not long enough yet" needs no error message to be understood. */
.classSecretShown { display: inline-block; min-height: 1.4em; letter-spacing: 0.28em; }
/* The join dialog holds more than it did; let it scroll on a short screen rather than clipping
   the Let's Play button, which is the one thing that must always be reachable. */
.classJoinDialog { max-height: 92vh; overflow-y: auto; }

/* ---------------------------------------------------------------------------------------------
   YOUR NUMBERS (TODO 6). The deliberate opposite of the arcade board: that screen is a public
   ranking and wants Courier and columns; this one is private and wants sentences, so it keeps the
   dialog's reading font and lets three headline figures carry the visual weight.
   --------------------------------------------------------------------------------------------- */
/* THE TWO SECTIONS (2026-09-05). The rule is the whole separator Jeff asked for, and it needs to
   be a real division rather than a hairline lost between two blocks of numbers - so it gets air on
   both sides and the heading under it sits close to what it names. */
.statsRule {
    border: 0;
    border-top: 1px solid #D8D2C0;
    margin: 1.6em 0 1.2em 0;
}
.statsSectionHead { text-align: center; margin: 0 0 0.2em 0; }

.statsHeadline {
    display: flex;
    flex-wrap: wrap;                 /* three across on a laptop, stacked on a narrow phone */
    justify-content: center;
    gap: 1.2em 2em;
    margin: 0.4em 0 1em 0;
}
.statBig { display: flex; flex-direction: column; align-items: center; min-width: 5em; }
.statBigNum {
    font-family: var(--mono);
    font-size: calc(18px + 1.6vmin);
    font-weight: 700;
    line-height: 1.1;
    color: var(--game-accent);
}
.statBigLabel {
    font-size: calc(9px + 0.4vmin);
    text-transform: uppercase;
    letter-spacing: .07em;
    color: #8A8A8A;
    text-align: center;
}
.statsLine { margin: 0.35em 0; }
/* A word or letter being talked about, in the same monospace the game shows it in - so "SCROTUM"
   on this screen looks like the same object the board showed. */
.statsWord {
    font-family: var(--mono);
    color: var(--game-accent);
    letter-spacing: 0.06em;
}
/* THE READING-LEVEL TABLE. Jeff, 2026-08-28: "Please make it lay out in good columns."
   Five columns - name, count, "games", percent, "won" - because the numbers and the words they
   belong to need opposite alignments, and a single cell holding "12 games" can only have one.
   The count and percent columns are right-aligned so their digits stack; the unit words are
   left-aligned against them so they read as one phrase. tabular-nums keeps 1 the same width as 8,
   without which right-alignment still leaves a ragged edge in a proportional face. */
/* text-align is inherited as `center` in this dialog, so every column states its alignment. */
.statsTable {
    margin: 0.2em 0 0.6em 1.2em;
    border-collapse: collapse;
}
.statsTable td { padding: 0.12em 0; vertical-align: baseline; }

/* SPECIFICITY MATTERS HERE and cost a round to spot: `.statsTable td` is (0,1,1) and a bare
   `.statsBandCount` is (0,1,0), so the shorthand `padding` above beat every per-column
   padding-right and every gutter silently collapsed to zero - "7games", "100%won". Qualifying each
   column with the table class puts them at (0,2,0) and they win. */
.statsTable .statsBandName {
    text-align: left;
    padding-right: 1.6em;
    white-space: nowrap;      /* "Specialized Language" is one phrase, not two lines */
}
.statsTable .statsBandCount {
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;   /* so 1 is as wide as 8 and the digits truly stack */
    text-align: right;
    padding-right: 0.35em;
    white-space: nowrap;
}
.statsTable .statsBandUnit {
    text-align: left;
    padding-right: 1.6em;
    white-space: nowrap;
}
/* The last unit cell has nothing after it, so its gutter is wasted width on a narrow screen. */
.statsTable tr > .statsBandUnit:last-child { padding-right: 0; }
/* THE PREFS SECTIONS (2026-08-29). Three labeled headings replace the one clever title -
   "Appearance & Behavior", "Word Reading Difficulty", "Word Content Filtering" - each a plain
   left-aligned rule-off, and the filter heading carries the Advanced toggle on its right. */
/* The whole dialog was "way small" at 100% zoom - it inherited the game page's shrunken base.
   A real floor on the dialog's own font lifts every control inside it at once. */
#divPreferenceDialog { font-size: max(14px, 1em); }
.prefSection {
    display: flex; align-items: center; justify-content: space-between;
    margin: 0.7em 0 0.3em 0; padding-bottom: 0.15em;
    border-bottom: 1px solid rgba(0, 0, 0, 0.18);
    font-size: 1.05em; text-align: left;
}
.prefSection:first-of-type { margin-top: 0.1em; }


/* THE GRID'S CONTAINER. The fourteen category sliders that followed it (2026-09-03 to 2026-09-07)
   are a classroom's control now - webroot/css/flagSliders.css - and the horizontal reading-level
   control that stood in for the grid under 13 went the same day: that player gets the grid
   itself, minus the content column (.poolTableSolo), in a narrower dialog (.prefsNarrow). */
#wordPoolMatrix { text-align: left; }
/* Narrower for the difficulty-only grid - Jeff, 2026-09-07: "make the whole window narrower for
   this so it balances better with the reduced content." One word column instead of four, so
   the same 80vw left it mostly empty. */
.modal-content.prefsNarrow { width: min(80vw, 46em); }
.poolTableSolo .poolCell { text-align: left; padding-left: 0.6em; }

/* The link out of the High Scores dialog, sitting beside its OK button. */
.scoresActions { display: flex; align-items: center; justify-content: center; gap: 1.2em; }

/* The signed-in corner (2026-08-25): the student's initials-avatar, fixed top-right, opening
   their profile. Fixed rather than in the icon row because the row is rebuilt on every relayout
   and the avatar is chrome, not game. Sits below the modals' z-order so a dialog covers it. */
/* ABOVE THE SITE BAR, not under it. The bar is `position: sticky; z-index: 1000` with a
   `backdrop-filter: blur(10px)`, so an avatar at z-index 5 sat behind it and the blur rendered it
   as a soft disc that could not be clicked - Jeff, 2026-08-27: "It's all blurred out and can't be
   clicked. But there is a round blur there like it might be getting obscured by something laid out
   on top." It was: the bar returned to classrooms on 2026-08-25 and this never moved.
   1001 puts it over the bar, and the offsets match .gi-nav's own padding so it lands INSIDE the
   bar's row rather than on top of its contents - which is free, because body.classroom hides
   .gi-actions and leaves that end of the bar empty. */
#mwAvatarCorner {
    position: fixed;
    top: 7px;
    right: 16px;
    cursor: pointer;
    z-index: 1001;
    line-height: 0;
}
#mwAvatarCorner:hover .mwAvatar { filter: brightness(1.15); }
/* The white ring separates the disc from the dark site bar; this shadow does the same job when
   the bar is not there and the page behind it is cream. Both halves are needed - the avatar sits
   fixed in a corner and does not know what is under it. */
#mwAvatarCorner .mwAvatar { filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.45)); }
.studentProfileAvatar { text-align: center; margin: 0.4em 0 0.2em; }
/* An account's own picture, in the same ring the letter discs wear. */
.accountAvatarPhoto { border-radius: 50%; border: 2px solid #FFFFFF; object-fit: cover; box-sizing: border-box; }

/* JOINING WITH YOUR OWN ACCOUNT (2026-09-23). One reading font and one size throughout, the
   account shown the way a sign-in screen shows it, one field, and the two answers together at
   the bottom right - Jeff: "Different font sizes with each line differently aligned". */
#accountJoin .modal-content { max-width: 34rem; }
.accountJoinText {
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(13px + 0.3vmin);
    line-height: 1.45;
    text-align: left;
    margin: 0.2rem 0 1rem;
}
.accountCard {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    padding: 0.8rem 0.9rem;
    border: 1px solid #D9DFCF;
    border-radius: 8px;
    background: #F7F9F3;
    margin-bottom: 1.1rem;
    text-align: left;
}
.accountCardAvatar { flex: 0 0 auto; line-height: 0; }
.accountCardText { display: flex; flex-direction: column; min-width: 0; gap: 0.15rem; }
.accountCardName, .accountCardMeta {
    font-family: Arial, Helvetica, sans-serif;
    overflow-wrap: anywhere;
}
.accountCardName { font-size: calc(15px + 0.3vmin); font-weight: 700; color: #1F2430; }
.accountCardMeta { font-size: calc(12px + 0.25vmin); color: #5B6152; }
.accountCardMeta a { color: #4E6B2E; }
.accountJoinField {
    display: flex;
    align-items: center;
    gap: 0.8rem;
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(13px + 0.3vmin);
    text-align: left;
}
.accountJoinLabel { font-weight: 700; color: #1F2430; }
#accountJoinInitials {
    width: 4.2em;
    font-family: var(--mono);
    font-size: calc(16px + 0.4vmin);
    font-weight: 700;
    letter-spacing: 0.2em;
    text-align: center;
    text-transform: uppercase;
    padding: 0.3em 0.2em;
    border: 1px solid #9AAE82;
    border-radius: 6px;
}
#accountJoin .classJoinError { text-align: left; margin: 0.6rem 0 0; }
.accountJoinActions { display: flex; justify-content: flex-end; gap: 0.6rem; margin-top: 1rem; }
.accountJoinActions button { font-size: calc(12px + 0.35vmin); padding: 0.45em 1.2em; border-radius: 4px; }
.accountJoinSecondary {
    font-family: Arial, Helvetica, sans-serif;
    border: 0;
    background: #DDE3D3;
    color: #2C3A20;
    cursor: pointer;
}
.accountJoinSecondary:hover { background: #CED6C2; }

/* Two links stacked where one used to sit (2026-08-25). The band's row is already tall enough
   for two small lines, so this costs no width - which is the whole point on a vertical phone,
   where the band was wrapping until the labels were shortened. */
.standingLinks {
    margin: 0 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    line-height: 1.15;
}
.standingLinks .standingLink { margin: 0; }

/* Report mode (sql/152): a reportable name gets the same dotted-underline affordance the
   editable own-name uses, in a warning tone rather than the garden green. */
.scoreWhoReport {
    cursor: pointer;
    text-decoration: underline dotted #B08968;
    text-underline-offset: 3px;
}
.scoreWhoReport:hover { color: #8A4B2D; }
.reportNameHint {
    text-align: center;
    font-size: calc(9.5px + 0.35vmin);
    color: #9a9084;
    margin: 0.5em 0 0;
}


/* The standing band's numbers, centered as a block between the drawing and the links column
   (2026-08-25). flex-wrap + justify-content:center keeps EVERY wrapped line centered on the same
   axis, which is exactly Jeff's "on word wrap, both the top and bottom lines are centered the
   same". The links column keeps its width; the numbers own the middle. */
/* Basis 0: the score box takes only the room the links leave, so the Evil count wraps under This
   Word before the links are pushed to a line of their own (Jeff, 2026-09-26). Its min-content (the
   wider of the two) still decides when the links must wrap. */
.standingNums {
    flex: 1 1 0;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: baseline;
    gap: 0.4em 1.1em;
}

/* The sign-in roster (2026-08-25): each seat is a big finger-friendly button of its three
   letters; New / Add sits under them, larger, because it is the first-day path. */
/* FLEX, NOT GRID, and that is the whole bug fix. `repeat(auto-fill, minmax(5.2em, 1fr))` lays
   down as many tracks as the width allows whether or not there are buttons to fill them, so a
   class with one student put its single button in track 1 - hard left, and nowhere near the
   centre of anything (Jeff, 2026-08-26: "with one button, the name button is not centered").
   auto-FIT would collapse the empty tracks but then stretch the survivor across the full width,
   which is a different wrong answer. Flex with wrap centres whatever is actually there, including
   a lone button and a ragged last row.

   The bottom margin is Jeff's "more vertical space betwen the name buttons and the 'Let's Play'
   button": the seat grid and the big action beneath it were close enough to read as one control. */
.classRosterGrid {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.4rem;
    max-width: 26rem;
    /* REM, NOT EM. This grid inherits a deliberately small font-size from the dialog (8.5px,
       measured), so `1.6em` of separation came out as 13px - not the breathing room it reads as
       in the source. Anything here that is meant to be a fixed amount of SPACE rather than a
       proportion of this element's own type has to be stated in a unit that does not shrink
       with it. */
    margin: 0.4rem auto 1.5rem auto;
}
.classSeatBtn { flex: 0 1 5.2rem; }
.classSeatBtn {
    font-family: var(--mono);
    font-size: calc(13px + 0.8vmin);
    font-weight: 700;
    letter-spacing: 0.14em;
    padding: 0.45em 0;
    color: #3F5A33;
    background-color: #F3F0E8;
    border: thin solid #9BB37A;
    border-radius: 6px;
    cursor: pointer;
}
.classSeatBtn:hover { background-color: #E4EDD8; }
/* THE TWO DOORS (2026-09-09). Equal buttons, a little apart, centred - and when there are no
   seats yet, centred in the height the roster would have taken, so the choice sits in the middle
   of the dialog rather than hugging its title. Jeff: "roughly equal sized... a bit separated, and
   centered on the screen." */
.classRosterArea { display: flex; flex-direction: column; }
.classGameLink { text-align: center; text-decoration: none; }
.classDoors {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 1rem 1.6rem;
    margin: 0.4rem 0 0.8rem;
}
.classDoorsAlone { min-height: 30vh; }
.classDoorBtn { font-size: calc(13px + 0.8vmin); min-width: 12.5rem; padding-left: 1.2em; padding-right: 1.2em; }
/* Google's button is its own drawing: no chrome of ours around it, and never scaled or recoloured. */
.classGoogleDoor { background: none; border: 0; padding: 0; line-height: 0; border-radius: 20px; cursor: pointer; }
.classGoogleDoor:focus-visible { outline: 2px solid #1a73e8; outline-offset: 3px; }
/* The account-join dialog opens OVER the class door. Both are .modal at z-index 1, and the door
   comes later in the document, so without this the door painted on top and the dialog underneath
   could be seen but not clicked (Jeff, 2026-09-09: "the option appears to show up below, but it
   is deactivated or covered"). */
#accountJoin { z-index: 3; }
.classSlotRow { text-align: center; margin: 0.15em 0 0.5em; }
.classSlotRow .verdictBtn { vertical-align: middle; margin-left: 0.6em; }

/* HOLD TO SEE IT (2026-09-05). Square rather than the Clear button's pill, because it is an icon
   and not a word; a hair smaller, because it sits beside Clear and must not outrank it.
   touch-action: none is what makes press-and-hold work on a phone at all - without it the browser
   may claim the touch for a scroll or a double-tap-zoom and the pointerup never lands here, which
   would leave the password legible. user-select stops the long press raising a text-selection
   callout over the very thing being read. */
.secretPeek {
    padding: 0.25em 0.45em;
    line-height: 0;
    touch-action: none;
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}
.secretPeek svg { width: 1.25em; height: 1.25em; display: block; }
/* Held. The same green the filled slots use, so "showing" reads as one state across the row. */
.secretPeekOn { color: #4A6B3D; border-color: #4A6B3D; background: rgba(74, 107, 61, 0.12); }

/* THREE SHADED BOXES, THE SAME FOR BOTH FIELDS (Jeff, 2026-08-26: "Change the UI for both initials
   and password to be the same, and make it visibly 3 boxes, or an area with 3 shaded rectangles").
   The initials used to be a text input with wide letter-spacing and the password a row of bullets -
   two different-looking controls doing the same job on the same screen. Boxes say the capacity
   before anything is typed, which a caret cannot.

   The row is a flex box rather than inline spans so the slots keep even gaps at any size, and it
   is the click target for making its field the active one - the whole row, not just a slot, so a
   small finger cannot miss between them. */
.classSlots {
    display: inline-flex;
    gap: 0.35em;
    vertical-align: middle;
    padding: 0.2em;
    border-radius: 8px;
    cursor: pointer;
}
/* Which field the character grid is typing into. A ring rather than a colour change so it reads
   as "here" and not as "wrong" - and it has to be visible at a glance from across a classroom. */
.classSlotsActive { box-shadow: 0 0 0 3px rgba(117, 155, 103, 0.45); }
.classSlot {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.5em;
    height: 1.9em;
    font-family: var(--mono);
    font-size: calc(17px + 1.1vmin);
    font-weight: 700;
    text-transform: uppercase;
    color: #3F5A33;
    background-color: #EDE8DC;
    border: 2px solid #CFC8BA;
    border-radius: 6px;
}
.classSlotFilled { background-color: #FFFFFF; border-color: var(--game-accent); }
.classBackLink { font-size: 0.9em; color: #9a7d00; }
/* Under the two fields, centred: "make one here, or go back and pick from the list". */
.classPickList { text-align: center; margin: 0.1em 0 0.7em; }
/* Room between the grid and the button that ends the screen, so the last key pressed and the
   commit are not neighbours. */
.classPlayRow { text-align: center; margin-top: 1.2rem; }
/* A password character while it is briefly visible - same slot metrics as the dots. */
.classSecretChar { font-family: var(--mono); font-weight: 700; }

/* THE ONE CHARACTER GRID. Jeff, 2026-08-26: "make it 3 rows of 12 characters (if wide enbough) or
   6 rows of 6 characters (if not)." 36 characters divides both ways exactly, which is why those
   are the two shapes - a fixed column count rather than auto-fit, because auto-fit gives whatever
   the width allows and Jeff asked for one of two known layouts.

   The breakpoint is where twelve 2.1em keys plus their gaps still fit inside the dialog's padding;
   below it the grid is square and the keys stay finger-sized rather than shrinking to fit twelve
   across a phone. */
.classCharPad {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 0.3rem;
    /* rem for the same reason the roster grid uses it - see there. In em, this box inherited a
       8.5px font and capped itself at 170px, which divided by six left 15px keys: narrower than a
       fingertip on the one device this control exists for. */
    max-width: 20rem;
    margin: 0.3rem auto 0.6rem auto;
}
@media (min-width: 620px) {
    .classCharPad { grid-template-columns: repeat(12, 1fr); max-width: 36rem; }
}

/* TOP 10 SCORES sits centered over the boards (Jeff, 2026-08-26). Dropping the reserved gutter is
   what makes "centred" mean centred on the dialog - see the note on .modalHead. */
.modalHeadTitleCentered { text-align: center; padding-right: 0; }

/* ---------------------------------------------------------------------------------------------
   THE ADMIN PANEL (2026-08-27). Three tabs of the same job - looking at words and deciding about
   them - so they share the review queue's table and the score screen's tab strip rather than
   growing a third vocabulary. Nothing here ships to a player; the page is requireAdmin.
   --------------------------------------------------------------------------------------------- */
.adminTabs { margin-bottom: 0.8em; }
.adminHint { max-width: 46em; margin: 0.1em auto 0.6em auto; font-style: italic; }
.adminListHead { margin: 1.2em 0 0.1em; }
.adminCount { color: #7a7062; font-weight: 400; }
.adminTestRow, .adminAddRow { text-align: center; margin: 0.4em 0; }
.adminInput {
    font: inherit;
    font-size: 0.95em;
    padding: 0.3em 0.5em;
    border: 1px solid #CFC8BA;
    border-radius: 5px;
    margin-right: 0.4em;
}
.adminInputWide { min-width: 22em; max-width: 100%; }
.adminVerdict { margin-left: 0.6em; }
.adminOk { color: #3F5A33; font-weight: 700; }
.adminNo { color: #A33B3B; font-weight: 700; }
.adminWhy { color: #7a7062; font-size: 0.9em; }
/* A definition is prose in a table of short cells; let it have the width and wrap. */


/* ---------------------------------------------------------------------------------------------
   THE KEYBOARD, IN ROWS (2026-08-27). One arrangement engine for the game's guess buttons, the
   classroom character grid - see keyboardRows() in js/keyboard.js. Alphabet
   order emits even rows with no offset; typewriter order emits QWERTY's three, staggered.

   --kb-step is ONE KEY PLUS ITS GAP, which is the unit the row offsets are expressed in. The
   em is the keyboard's own font-size, so the whole thing scales with the buttons - including
   through fitToScreen(), which rescales that font-size to fit the page.
   --------------------------------------------------------------------------------------------- */
.kbRows {
    --kb-key: 1.72em;
    --kb-gap: 0.16em;
    --kb-step: calc(var(--kb-key) + var(--kb-gap));
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--kb-gap);
    width: max-content;
    /* THE ROWS MUST BE ABLE TO SHRINK. The table this replaced sized its cells at width:100%, so
       it always fitted whatever column it was in; fixed-width keys do not, and a ten-key QWERTY
       row is wider than a nine-column table ever was. Capping the block at its container and
       letting the keys flex down means a narrow phone, or the wide layout's column beside the
       floating drawing, gets smaller keys rather than a row running off the edge. */
    max-width: 100%;
    margin: 0 auto;                  /* centred on its widest row */
}
.kbRow { display: flex; gap: var(--kb-gap); max-width: 100%; }
/* The typewriter stagger, in keys. Jeff's numbers, not a real keyboard's (see keyboard.js). */
.kbRowDigits { margin-left: calc(-0.6 * var(--kb-step)); }
.kbRowHome { margin-left: calc(0.4 * var(--kb-step)); }
.kbRowBottom { margin-left: calc(0.9 * var(--kb-step)); }
.kbRow > button { flex: 0 1 var(--kb-key); width: var(--kb-key); min-width: 0; }

/* The game's keys inherit the size fitToScreen() scales, so the keyboard's em means what the
   buttons mean. The height is the same unit as the width, which is what makes them square-ish
   rather than the table cells' whatever-fits. */
/* Capped as well as scaled: 3vw was sized for a table that reflowed to 9 or 13 columns, and ten
   keys in one row at that size is wider than the screen. The cap is where ten keys plus their
   gaps come to about 87% of the viewport. */
#divLetters .kbRows { font-size: min(calc(8px + 3vw), 4.6vw); }
#divLetters .letterBtn { font-size: inherit; height: var(--kb-key); line-height: 1; }
/* THE WORD NEEDS ROOM UNDER THE KEYS. `.hangmanWord` carries `margin-top: -2vw`, tuned when the
   keyboard was a <table> whose cells left a band of baseline space below the last row of buttons -
   measured at ~104px on a 1280px viewport, which the negative margin was there to eat. Flex rows
   have no such band: the last row ends where the buttons end, so the negative margin went straight
   on to pull the word into them (Jeff, 2026-08-27: "the word is crowding (no margin) the bottom
   typewriter row... this is happening with either keyboard").
   4vw here collapses against that -2vw for a net 2vw of clear space, which is the same unit the
   word already uses on its sides. */
#divLetters { margin-bottom: 4vw; }

/* The classroom pads are smaller and live inside a dialog; same structure, its own scale. */
.classCharPad .kbRows { font-size: calc(11px + 0.9vmin); }
.classCharPad .classSecretKey {
    font-size: inherit;
    height: max(30px, var(--kb-key));
    min-height: 0;
    padding: 0;
}

/* The flavour picker, centred under the drawing and actually visible (Jeff, 2026-08-27: "hard to
   see and in the wrong spot"). It was styled to disappear - transparent border, muted grey - on
   the theory that it should not distract. It succeeded too well. */
.flavorRow { text-align: center; margin: 0.4em 0 0.2em; line-height: 1; }
#divCanvas .flavorRow { width: 100%; }
.flavorSelect {
    font-family: var(--mono);
    font-size: calc(11px + 0.35vmin);
    color: #3F5A33;
    background: #F3F0E8;
    border: thin solid #9BB37A;
    border-radius: 5px;
    padding: 0.2em 0.4em;
    margin: 0;
    cursor: pointer;
}
.flavorSelect:hover, .flavorSelect:focus { background: #E4EDD8; border-color: #6E8B4E; }

/* Back sits beside Let's Play, quieter than it. */
.classBackBtn { margin-right: 0.8em; }

/* #divCanvas floats in the wide layout so it shrink-wraps the drawing - see handleDisplaySize().
   The rule has to EXIST for updateStylesheet() to flip it rather than inserting a new one at the
   top of the sheet, where later rules would outrank it. */
#divCanvas { float: none; margin-right: 0; }
/* The wide layout narrows and centres this so the drawing and the letters sit together
   (layout.js hugWideLayout); the rule exists for updateStylesheet to change. */
#entirePage { max-width: none; margin-left: auto; margin-right: auto; }

/* THE HIGH-SCORE BANNER (2026-08-27). Jeff: "quick fireworks with a banner, just long enough to
   read 'New High Score', with a cheering sound." It rides on the same fixed full-screen layer the
   confetti uses, above the game and below nothing - there is no dialog on screen while it plays,
   by construction (see maybeFanfare).

   pointer-events: none throughout: the celebration is scenery, and a player who taps during it
   should be tapping whatever is underneath. */
.fanfareBanner {
    position: fixed;
    left: 50%;
    top: 38%;
    z-index: 6;
    pointer-events: none;
    /* VISIBLE IS THE DEFAULT, and the pop is an animation rather than a transition. A transition
       needs the start state to be painted before the end state arrives, so if frames are not being
       produced - a backgrounded tab, a throttled one - the element simply stays at its start state,
       which for a fade-in means the banner never appears at all. That is the worst possible failure
       for the one element whose entire job is to be read. An animation with these styles as the
       resting state fails the other way: no animation, banner still there. */
    transform: translate(-50%, -50%);
    opacity: 1;
    /* forwards, NOT both: `both` also applies the `from` frame before the animation starts, which
       is opacity 0 - and in a tab that is not producing frames the element can sit there. With
       forwards the element's own (visible) style holds until the animation actually runs. */
    animation: fanfarePop 280ms cubic-bezier(.2, 1.5, .4, 1) forwards;

    font-family: var(--mono);
    font-size: calc(16px + 3.2vw);
    font-weight: 700;
    letter-spacing: 0.02em;
    text-align: center;
    /* WRAPPING IS ALLOWED AT EVERY WIDTH. This was `nowrap` with a `normal` override below 560px,
       which left a gap: at 768px the font is 40.6px and "New Top Ten 5-Streak High Score!" measures
       about 780px against a 737px max-width, so the longest of the four titles ran out of its own
       box on exactly the tablet a classroom is most likely to be holding. Caught in
       test/browser/bannerExit.html while looking at something else - the glow draws the eye to the
       box edges, which is where it became obvious. Wrapping changes nothing on a desktop, where
       there is room for one line anyway; it only takes effect where the alternative was overflow. */
    max-width: 96vw;
    padding: 0.35em 0.9em;
    color: #F7F2E6;
    background: rgba(62, 84, 46, 0.92);
    border: 3px solid #F2B134;
    border-radius: 12px;
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
}
/* The overshoot in the easing above is the whole effect: it arrives slightly too big and settles,
   which reads as a stamp rather than a fade-in. */
@keyframes fanfarePop {
    from { transform: translate(-50%, -50%) scale(0.7); opacity: 0; }
    to   { transform: translate(-50%, -50%) scale(1);   opacity: 1; }
}
/* HOW IT LEAVES. Jeff, 2026-08-27: "Not really explode. Maybe flash out of existence? I'm thinking
   an increasing glow effect that goes from 0 to very significant over about 1/3 second... Then a
   flash, a pop, and it disappears leaving the next thing in view."

   One keyframe, not three timers. The three phases are one continuous move, and splitting them
   across separate JS steps would let a throttled tab land between two of them and leave the banner
   sitting there mid-flash - blown out and white - over the summary.

   The DURATION comes from JS (FANFARE_EXIT_MS, set inline); the SPLIT is these percentages, and
   they are derived from the same constants: glow is 333ms of 533ms = 62%, the flash peaks at 78%,
   and the pop takes the rest. Change the ratio in JS and these have to move with it.

   The glow is a box-shadow and a text-shadow together. Shadow alone lit the box but not the words,
   which read as the banner backlit rather than the whole thing charging up. */
.fanfareBanner.fanfareBannerOut {
    animation: fanfareBannerOut 533ms ease-in forwards;
}
@keyframes fanfareBannerOut {
    0% {
        transform: translate(-50%, -50%) scale(1);
        box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
        text-shadow: none;
        background: rgba(62, 84, 46, 0.92);
        border-color: #F2B134;
        opacity: 1;
    }
    /* Glow, winding up. Nothing moves much yet - the light is doing the work. */
    62% {
        transform: translate(-50%, -50%) scale(1.05);
        box-shadow: 0 0 64px 20px rgba(242, 177, 52, 0.85), 0 6px 24px rgba(0, 0, 0, 0.35);
        text-shadow: 0 0 18px rgba(255, 244, 214, 0.95);
        background: rgba(96, 124, 64, 0.95);
        border-color: #FFD97A;
        opacity: 1;
    }
    /* The flash: the box blows out to white and the words go with it. They are unreadable for about
       90ms, which is the point - a flash you can read through is not a flash. */
    78% {
        transform: translate(-50%, -50%) scale(1.13);
        box-shadow: 0 0 150px 70px rgba(255, 250, 234, 0.95), 0 0 70px 28px rgba(242, 177, 52, 1);
        text-shadow: 0 0 26px rgba(255, 255, 255, 1);
        background: rgba(255, 252, 242, 0.98);
        border-color: #FFFFFF;
        opacity: 1;
    }
    /* The pop: out and gone, fast, leaving the summary behind it. */
    100% {
        transform: translate(-50%, -50%) scale(1.45);
        box-shadow: 0 0 200px 100px rgba(255, 250, 234, 0);
        text-shadow: 0 0 40px rgba(255, 255, 255, 0);
        background: rgba(255, 252, 242, 0);
        border-color: rgba(255, 255, 255, 0);
        opacity: 0;
    }
}

/* Somebody who has asked their system for less motion gets the banner without the stamp. It is
   decoration; the words are the point. The exit is suppressed the same way - the banner is removed
   from the DOM on a timer regardless, so with no animation it simply disappears when its time is
   up, which is the un-startling version of the same event. The compound selector has to be named
   explicitly here: `.fanfareBanner` alone is less specific than `.fanfareBanner.fanfareBannerOut`
   and would lose to it. */
@media (prefers-reduced-motion: reduce) {
    .fanfareBanner,
    .fanfareBanner.fanfareBannerOut { animation: none; }

    /* AND EVERY OTHER ANIMATION ON THE PAGE (TODO 502). This rule used to cover the banner
       alone, which left the score's bounce and the streak tick running for
       the same person. A classroom device may have this setting on because motion makes a
       particular child ill or because it makes the screen unreadable to them - it is not a
       preference about taste, and honouring one animation out of five honours none of it.
       Each is suppressed the way the banner is: the element stays, the state it announces is
       still true, only the movement is gone. The canvas effects (the confetti, the fireworks,
       the per-guess stroke and the score's count-up) are JavaScript and cannot be reached from
       here; they ask reduceMotion() in webroot/NotHangman/js/state.js, which reads this same
       system setting. */
    /* The streak number's bounce when it improves, and its tick when the clock takes a bite. */
    .standingLiveUp,
    .standingLiveTick { animation: none; }
}

/* A long title on a narrow phone needs a smaller face as well as the wrapping it now gets at every
   width. The `white-space: normal` that used to be here is gone because it is the default above. */
@media (max-width: 560px) {
    .fanfareBanner { font-size: calc(13px + 3.4vw); line-height: 1.15; }
}

/* ---- Admin-wide fixes (2026-08-30). Jeff, in order: the viewport-proportional game font made
   the panel need 300% zoom (fixed 16px base instead); cells were centered (body's text-align
   leaked in); and then: "just use best practices of clear readability, rather than the stylized
   presentation needed for a game. A normal-weight green word on a grey background is not super
   readable." So the admin panel is a plain white tool: dark text, left aligned, system sans,
   readable sizes - and it uses the width. ---- */
#reviewPage {
    font-size: 16px; max-width: min(96vw, 1700px);
    background: #FFFFFF; border: 1px solid #D8D2C6; border-radius: 10px;
    padding: 1.2em 2em 3em; margin: 1.5em auto;
    color: #1F2430; text-align: left;
    font-family: Arial, Helvetica, sans-serif;
}
#reviewPage .pickTitle, #reviewPage .pickLead { text-align: left; }
#reviewPage .adminHint { margin-left: 0; margin-right: 0; max-width: 60em; }
#reviewPage .helpText { color: #333333; }
/* The word itself: dark and bold, not the game's accent green - readability over branding. */
#reviewPage .reviewWord { color: #1F2430; font-weight: 700; }
#reviewPage .reviewTable td { text-align: left; }
#reviewPage .adminTestRow, #reviewPage .adminAddRow { text-align: left; }
/* The tab strip and the filter rows read as BUTTONS (Jeff: "make these into buttons... put the
   word count in a second row when needed") in a wrapping grid. */
#reviewPage .scoresWindows {
    display: flex; flex-wrap: wrap; gap: 0.4em; justify-content: flex-start; align-items: stretch;
}
#reviewPage .scoresWindow {
    display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
    padding: 0.35em 0.9em; border: 1px solid #B8C4A6; border-radius: 8px;
    background: #F4F4F1; color: #2C3A20; text-decoration: none; cursor: pointer;
    font-size: 0.95em; line-height: 1.25;
}
#reviewPage .scoresWindow:hover { background: #EAF0E3; }
#reviewPage .scoresWindow.scoresWindowOn { background: #55703C; border-color: #55703C; color: #FFFFFF; }
#reviewPage .scoresWindow .adminCount { display: block; font-size: 0.85em; }
#reviewPage .scoresWindow.scoresWindowOn .adminCount { color: #E8EFDD; }

/* ---- The admin word catalog. One wide row per word; the flag pills are the widest cell and
   get the width the narrowed Word/Difficulty/Score columns give up. The controls stay small
   (they fit and read fine - Jeff); the words and their metadata do not. ---- */
.catalogFilters { display: flex; flex-wrap: wrap; gap: 0.5em; align-items: center; justify-content: flex-start; }
/* One height for the search box and the selects, so "contains" lines up with the rest. */
.catalogFilters .adminInput {
    font-size: 0.9em; height: 2.2em; box-sizing: border-box; padding: 0.2em 0.5em; margin: 0;
}
.catMeta { display: block; font-size: 0.85em; color: #5A5A5A; white-space: nowrap; }
/* ONE CONTROL PER CATEGORY, THREE SEGMENTS WIDE (Jeff, 2026-08-30): "it could say 'violence' but
   the actual button would be in three sections with a thin line making them clear."

   The thin line is a real border-collapse rather than a drawn divider: the segments sit flush,
   each borrowing its neighbour's edge, so the control reads as ONE button subdivided instead of
   three buttons parked together. That distinction is the whole point - three separate buttons say
   "pick any", one divided button says "pick which", and only the second is true here.

   Fourteen of these have to fit a row on a wide screen, which is what sets the size. The category
   name sits above its segments so the pair scans as a unit when they wrap. */
/* LEFT, explicitly. The table centres its cells, so a run of fourteen controls that wraps would
   centre its last line and leave it floating in the middle of the cell - the same centring Jeff
   caught once already ("Column names are left justified, but words are centered"). A wrapped run
   has to start at the same left edge on every line or the eye cannot scan down it. */
.catFlags { text-align: left; }
.catFlagCtl {
    display: inline-flex; flex-direction: column; align-items: stretch;
    margin: 0 0.5em 0.4em 0; vertical-align: top;
}
/* OFF is the default and has to LOOK off at a glance across fourteen controls, so the whole
   control dims rather than merely lacking a highlight. */
.catFlagName {
    font-size: 0.7em; line-height: 1.2; letter-spacing: 0.02em; text-align: center;
    color: #8A8A8A; padding-bottom: 0.12em;
}
.catFlagCtl.catFlagOn .catFlagName { color: #2C3A20; font-weight: bold; }
.catFlagSegs { display: inline-flex; }

.catSeg {
    font-family: inherit; font-size: 0.78em; line-height: 1.35;
    padding: 0.18em 0.55em; margin: 0 0.18em 0.18em 0;
    border: 1px solid #B8C4A6; border-radius: 999px; background: #F4F4F1; color: #2C3A20;
    cursor: pointer;
}
.catSeg:hover { background: #EAF0E3; }
.catSeg.catSegOn { background: #55703C; border-color: #55703C; color: #FFFFFF; }
/* The three-in-one variant: square the inner corners, collapse the shared borders, drop the
   inter-button margin. Everything else is inherited from .catSeg so the two stay in step. */
.catSeg3 {
    margin: 0; border-radius: 0; padding: 0.16em 0.42em; min-width: 1.7em; text-align: center;
}
.catSeg3:first-child { border-radius: 4px 0 0 4px; }
.catSeg3:last-child  { border-radius: 0 4px 4px 0; }
.catSeg3 + .catSeg3  { border-left-width: 0; }
/* A lit segment needs its left edge back or the run reads one pixel short at the seam. */
.catSeg3.catSegOn + .catSeg3 { border-left: 1px solid #B8C4A6; border-left-width: 1px; }
.catFlagCtl.catFlagOn .catSeg3:not(.catSegOn) { background: #FFFFFF; }

/* A past game's summary stacks OVER My Scores (Jeff, 2026-08-30) - .modal is z-index 1 and the
   winner among equals is DOM order, which is not a thing to rely on. */
#finish.overStats { z-index: 3; }

/* MY SCORES layout (Jeff, 2026-08-30): the abrupt-label chart in two columns, then the two
   tables side by side - reading level first - stacking when narrow. */
/* My Scores' top: Best Game and Correct Guesses on their own lines, between them best | current
   pairs with each column of numbers right-aligned. A phone folds the pairs into label | number. */
.statsTopLine { margin: 1.2vmin 0 0 2vw; }
.statsPairs {
    display: grid; grid-template-columns: max-content max-content max-content max-content;
    align-items: baseline; gap: 0.3em 0.8em; margin: 1.2vmin 0 0 2vw;
}
.statsPairNum { justify-self: end; font-variant-numeric: tabular-nums; }
.statsPairSecond { margin-left: 2.5em; }
.statsPairGap { grid-column: 1 / -1; height: 0.4em; }
@media (max-width: 560px) {
    .statsPairs { grid-template-columns: max-content max-content; }
    .statsPairSecond { margin-left: 0; }
}
.statsFine { font-size: 0.8em; color: #9a9084; font-weight: 400; }
.statsTables { display: flex; flex-wrap: wrap; gap: 0 4vmin; align-items: flex-start; }
.statsTableCol { min-width: 16em; }

/* Typographic orphans ("runts" - a single word alone on a wrapped line). text-wrap: pretty asks
   the browser to avoid them, Word-style; browsers without it simply ignore the property, so this
   is free where it works and harmless where it does not (Jeff, 2026-08-30). */
p, li, .helpText { text-wrap: pretty; }

/* THE GRID'S NAMES COLUMN (2026-08-30). Right-aligned against the slider it labels, and lit the
   same way the rung markers are so the selected run reads as one block. */
.poolRowName {
    /* Left-aligned now that it sits beside the words rather than against the slider, so the names
       and the word columns share one left edge. Wrapping is allowed: clipping a name is worse
       than a taller row, and nothing downstream assumes a row height (the bands are measured). */
    text-align: left;
    padding: 0.55vmin 0.5em 0.55vmin 0.45em;
    border: 1px solid #E4E4E4;
    font-family: Arial, Helvetica, sans-serif;
    font-size: calc(8.5px + 0.32vmin);
    line-height: 1.15;
    color: #AAAAAA;          /* muted outside the selection, exactly like the word cells */
    cursor: pointer;
}
/* Selected rows carry the same fill as the cells beside them, so the lit run reads as one block
   across the whole row rather than starting at the word columns (Jeff, 2026-08-30). */
.poolRowNameOn { color: #3C4A2C; font-weight: 700; background: #EFF4EA; }
.poolRowName:hover { color: #2F4520; }

/* Extra air above the words section, so it reads as a new subject rather than as more of the
   Appearance block (Jeff, 2026-08-30). */
#prefWordsHead { margin-top: 1.6em; }

/* NARROW: the names column is the first thing to go. The grid still works - the rungs, the
   slider and the example cells are all still there - and at this width the names were what
   pushed the word columns into wrapping.
   COLLAPSED, NOT REMOVED. `display: none` on the cells took them out of their rows, which under
   table-layout: fixed shifts every later cell one column left - so the first word column landed
   in the zero-width names column and vanished (Jeff, 2026-08-30). Keeping the cell and emptying
   it leaves the column count intact. */
@media (max-width: 620px) {
    .poolColName { width: 0; }
    .poolRowName {
        font-size: 0; padding: 0; border: 0; overflow: hidden; max-width: 0;
    }
}

/* THE MODE SWITCH (2026-09-10): Nice / Mean / Evil as one three-part button under the drawing,
   where the look picker sat. The pressed part is filled; Evil is greyed until it exists. Same
   palette as the seat buttons, so it reads as part of the game and not as a form control. */
.modeSwitch { display: inline-flex; border: thin solid #9BB37A; border-radius: 6px; overflow: hidden; }
.modeBtn {
    font-family: var(--mono);
    font-size: calc(11px + 0.35vmin);
    font-weight: 700;
    letter-spacing: 0.06em;
    padding: 0.3em 0.9em;
    color: #3F5A33;
    background: #F3F0E8;
    border: 0;
    border-right: thin solid #9BB37A;
    cursor: pointer;
}
.modeBtn:last-child { border-right: 0; }
.modeBtn:hover:not(:disabled):not(.modeBtnOn) { background: #E4EDD8; }
.modeBtnOn { background: #4A6B3D; color: #FFFFFF; }
.modeBtn:disabled { color: #9a978f; background: #ECE9E1; cursor: default; }
/* The look drop-down (2026-09-11) sits to the left of the mode switch; a gap, and room to wrap
   on a phone. */
.lookSelect { margin-right: 0.8em; vertical-align: middle; }
.flavorRow { line-height: 1.9; }
.flavorRow .modeSwitch { vertical-align: middle; }
#divCanvas.flavorStacked { width: min-content; }
#divCanvas.flavorStacked .lookSelect { margin-right: 0; }
/* The Nice | Mean | Evil control in a score dialog's title (TODO 442): the same switch, smaller,
   in front of the words. */
.scoresModeSwitch { vertical-align: middle; margin-right: 0.6em; font-size: 0.55em; }
.scoresOther { margin-left: 0.6em; }

/* THE REVIEW SCREEN (2026-09-10, admin): one word, whole. A segment a player RECOMMENDED is
   outlined in the accent - "recommended in a third colour" - and a category a player would
   switch off says so under its name.

   A LIST OF THESE SINCE 2026-09-16 (TODO 556), not one filling the screen. Jeff: "it can have a
   really wide and large (set of?) row(s) per word, so each word can be focused on." So the block
   keeps every part it had and gains a rule and a little air between it and the next one - the
   separator is what makes a stack of them readable as a list rather than as one long page. */
.revCard {
    text-align: left; max-width: 60em; margin: 0 0 1.4em;
    padding: 0 0 1.2em; border-bottom: 1px solid var(--border-color, rgba(42, 37, 32, 0.13));
}
.revCard:last-child { border-bottom: 0; }
.revHead { display: flex; align-items: baseline; gap: 0.8em; flex-wrap: wrap; margin: 0 0 0.3em; }
.revWord { font-size: 1.6em; }
/* THE DEFINITION HAS THE LINE TO ITSELF (TODO 557). It shared the head row with the word and its
   buttons, which left a short sense wrapping three times in a narrow column - and the definition
   is the thing being judged. `display: block` is doing real work here: the head above is a flex
   row, and this used to be pulled into it. */
.revDef { display: block; margin: 0 0 0.3em; font-size: 1.05em; }
.revMeta { margin: 0 0 0.3em; font-size: 0.9em; color: #5A5A5A; }
.revInline { display: inline; white-space: normal; }
.revIgnored { color: #A8780F; font-weight: 700; }
.revLift { font-size: 0.75em; padding: 0.05em 0.5em; }
.revControls { margin: 0.6em 0 0.4em; }
/* SEVEN AND SEVEN (TODO 557, Jeff 2026-09-16: "let's move the 14 filter buttons to two rows of 7,
   to make room for the ignore"). A grid rather than a wrap, because wrapping put a different
   number on each line at every window width and the eye cannot learn where a category is. Seven
   equal columns means politics is always fourth on the top row. Below 52em there is no honest way
   to fit seven, so it falls back to as many as fit and the order still reads left to right. */
.revFlags { margin: 0 0 0.6em; display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 0.25em 0.4em; }
.revFlags .catFlagCtl { margin: 0; }
/* The name is centred over its column; in a grid cell wider than the three segments need, the
   segments have to be centred too or the label floats above nothing and the eye reads the pair as
   belonging to different columns. Outside the grid (the finish dialog's compact copy) the control
   is only as wide as its contents and this never arises, which is why it is scoped here. */
.revFlags .catFlagSegs { justify-content: center; }
@media (max-width: 52em) {
    .revFlags { grid-template-columns: repeat(auto-fit, minmax(5.2em, 1fr)); }
}
.revSub { margin: 0.6em 0 0.2em; font-weight: 700; }
.revActions { margin: 0.8em 0 0; display: flex; gap: 0.6em; flex-wrap: wrap; }
/* YELLOW is a player's open proposal, GREEN (.catSegOn) is what is on or staged on (2026-09-11,
   TODO 31 - Jeff: "the recommendations to come in yellow... if I click once, it's green and on.
   And click again to turn it off"). */
.catSeg.catSegRec { background: #F2D257; border-color: #D9B93A; color: #3A3A3A; position: relative; z-index: 1; }
.catSeg.catSegRec:hover { background: #F7DF7A; }
.catFlagCtl.catFlagRecOff .catFlagName { color: #A8780F; font-weight: bold; }
/* The proposal, named: yellow while it differs from what is staged, quiet once it has been taken. */
.revBandTake { font-size: 0.8em; padding: 0.15em 0.6em; }
.revBandTaken { font-size: 0.8em; color: #3F5A33; font-weight: 700; white-space: nowrap; }
.revResolved { color: #7a7062; }
.revOpenTag { background: #F2D257; color: #3A3A3A; border-radius: 3px; padding: 0 0.35em; font-size: 0.8em; margin-left: 0.3em; }
.revStagedNote { margin-left: 0.4em; color: #A8780F; }
.adminVerdictOk { color: #3F5A33; }

/* THE RECOMMENDATION PANEL on the finish dialog (sql/254): the same controls, compact, starting
   from what the word is today. */
.finishRecommend { text-align: left; margin: 0.6vmin 0 0.6vmin 2vw; font-size: 0.85em; }
.recLead { margin: 0 0 0.3em; }
.recRow { margin: 0 0 0.4em; }
.recSelect { font: inherit; }
.recFlags .catFlagCtl { margin-right: 0.4em; }

/* The name a student gives their teacher at the class door (sql/240). A real text field,
   unlike the two 36-key grid fields above it - a name is typed, and routing it through a
   character grid would be an obstacle with nothing on the other side of it.

   It lives HERE rather than in profile.css because #classDisplayName is on this page and on
   no other, and this page never loads that sheet - so the rule reached nothing and the field
   rendered as a browser default (TODO 499). A rule belongs in a sheet its element’s page
   actually loads. */
.classNameInput {
    font: inherit;
    padding: 0.35em 0.6em;
    border-radius: 8px;
    border: 2px solid currentColor;
    background: transparent;
    color: inherit;
    width: 12em;
    max-width: 100%;
}

/* THE SOUND CONTROL, POINTED AT (2026-09-21). The page asks a new player whether to play sounds;
   this is the answer landing on the switch they just set, so they know where it is next time.
   Three pulses of a ring and a lift, then nothing - and it respects a reduced-motion setting,
   which leaves the ring without the movement. */
@keyframes soundJustSetPulse {
    0%   { box-shadow: 0 0 0 0 rgba(155, 70, 35, 0.55); transform: scale(1); }
    40%  { box-shadow: 0 0 0 10px rgba(155, 70, 35, 0); transform: scale(1.18); }
    100% { box-shadow: 0 0 0 0 rgba(155, 70, 35, 0); transform: scale(1); }
}
.soundJustSet {
    border-radius: 50%;
    animation: soundJustSetPulse 0.75s ease-out 3;
}
@media (prefers-reduced-motion: reduce) {
    @keyframes soundJustSetPulse {
        0%   { box-shadow: 0 0 0 0 rgba(155, 70, 35, 0.55); }
        40%  { box-shadow: 0 0 0 10px rgba(155, 70, 35, 0); }
        100% { box-shadow: 0 0 0 0 rgba(155, 70, 35, 0); }
    }
}

/* =============================================================================================
   THE MODERNISED DIALOG (NH00607, 2026-09-22), opt-in, one dialog at a time.

   Jeff: "The info dialog is pretty weak for format, in comparison with the rest of the app now.
   The Classes info dialog is better. But maybe both could be improved, and should be using the
   same style anyway... It is one dialog style across all of GateIron sites. How about we start
   here though... Maybe implement one for the game's info dialog. If I like it, we'll spread to
   the site and then the other sites."

   So this is a LAYER, not a rewrite: a dialog opts in by adding `giDialog` beside `modal`, and
   everything it overrides sits in this block. Nothing about opening, closing or clicking outside
   changes, and the fourteen dialogs that have not opted in are untouched. When it is approved it
   moves to GateIron-Shared as-is and the game keeps a link to it rather than a copy.

   What is actually different from .modal-content, and why:
     - the card is centred in the sheet and scrolls INSIDE itself, so a long read never scrolls
       the page behind it and the close X never leaves the screen;
     - sizes are rem, not vw. The old card was 80vw wide with vw-scaled text, so the same dialog
       read as a pamphlet on a laptop and a billboard on a monitor;
     - a measure of about 66 characters, which is what makes a paragraph readable;
     - the section headings keep their small-caps voice and gain a hairline above, so the sections
       are separated by structure rather than by blank space;
     - the sheet dims and blurs what is behind it, so the dialog is clearly the thing in front.
   ============================================================================================= */
dialog.giDialog {
    /* THE CARD IS CENTRED, AND A TALL ONE IS AGAINST THE TOP (NH00646, 2026-09-22). Every dialog
       used to be pushed down by a flat 10vw, so a short one - Help is about a third of the screen
       - started a quarter of the way down and sat low, and a tall one lost its head off the top.
       The card centres itself in this padding and is capped below the window's height, so it
       reaches the top of its own accord exactly when it is long enough to need to. Jeff: "If the
       dialog is so long that the page needs a scrollbar, the dialog should be placed with minimal
       or no space above it... Centered or higher than center looks better." */
    padding: clamp(0.5rem, 3vh, 2.25rem) 1rem;
    /* THE SHEET IS SAND, NOT SMOKE (Jeff, 2026-09-23): "they need a stronger background. Maybe it
       should show the background behind it at 10% strength... Alternately a light sand color
       reminiscent of the sand in GateIron.com." It is both: the site's own --sand at nine tenths,
       so the game shows through at a tenth of its strength and the card's black text has a light
       ground to sit against rather than a dimmed board. */
    background: rgba(220, 201, 166, 0.9);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
    display: none;
}
dialog.giDialog[open] { display: flex; }
dialog.giDialog > .modal-content {
    /* Named, because the close X is placed against them: the same inset from the top and from the
       right, which is what makes it read as being IN the corner. */
    --gi-pad-y: clamp(1rem, 2.5vmin, 1.6rem);
    --gi-pad-x: clamp(1.1rem, 3vmin, 2rem);
    --gi-x-inset: 0.5rem;
    /* Centred: the auto margins split the free space, and when there is none left they collapse
       and the card sits against the top. Jeff allowed either centred or higher, and centred is
       the one that needs no number nobody can defend. */
    margin: auto;
    width: min(46rem, 100%);
    max-height: calc(100vh - clamp(1.5rem, 8vh, 6rem));
    overflow-y: auto;
    overscroll-behavior: contain;
    background: #FFFDF9;
    border: 1px solid rgba(42, 37, 32, 0.12);
    border-radius: 14px;
    box-shadow: 0 1px 2px rgba(42, 37, 32, 0.08), 0 24px 60px -20px rgba(42, 37, 32, 0.45);
    padding: var(--gi-pad-y) var(--gi-pad-x) clamp(1.4rem, 3vmin, 2rem);
    font-size: 16px;
}
/* THE READING STARTS BESIDE THE X, NOT BELOW IT (Jeff, 2026-09-23): "the text starts unnecessarily
   low because it is starting below the 'X'. But the text is boxed in to never go to the right far
   enough to have a problem with the X." The measure is 66ch in a 46rem card, so a paragraph stops
   about nine rem short of the corner and the two can share the line - which lets the first line
   begin one padding down, the same distance from the top as it is from the left.
   Sticky at offset zero, which for a box with padding is where the bar already sits, so the X
   does not scroll away in a long read. */
dialog.giDialog .modalHead {
    position: sticky;
    top: 0;
    z-index: 1;
    min-height: 0;
    margin: 0;
    padding: 0;
}
dialog.giDialog .modalHead + * { margin-top: 0; }
/* Equal insets from the top and the right of the card, measured to the button box - the cross is
   centred in it, so what the eye reads as the margin is that inset plus the box's own air. The
   puck of card colour keeps it legible over text scrolling under it. */
dialog.giDialog .modalClose {
    top: calc(var(--gi-x-inset) - var(--gi-pad-y));
    right: calc(var(--gi-x-inset) - var(--gi-pad-x));
    transform: none;
    border-radius: 50%;
    background: #FFFDF9;
    box-shadow: 0 0 0 0.3rem #FFFDF9;
}
dialog.giDialog .modalHeadTitle { padding-right: calc(var(--modal-close-size) + 1.5em); }
/* Narrower than the card's own maximum and the measure fills the width, so there is nothing beside
   the X to put a line in: the bar takes back exactly the height the button hangs below it. */
@media (max-width: 46rem) {
    dialog.giDialog .modalHead {
        min-height: calc(var(--modal-close-size) + var(--gi-x-inset) - var(--gi-pad-y) + 0.3rem);
    }
}
dialog.giDialog .aboutHeading {
    font-size: 0.82rem;
    margin: 1.6rem 0 0.5rem;
    padding-top: 1rem;
    border-top: 1px solid rgba(42, 37, 32, 0.10);
    color: #5F7F54;
}
dialog.giDialog .aboutHeading.aboutFirst { margin-top: 0.2rem; padding-top: 0; border-top: 0; }
dialog.giDialog .helpText {
    font-family: 'Hanken Grotesk', Arial, Helvetica, sans-serif;
    font-size: 1rem;
    line-height: 1.6;
    max-width: 66ch;
    margin: 0 0 0.85rem;
    color: #2A2520;
}
/* The 2vw hanging indent goes: the measure above does the work a margin was doing, and at a
   narrow width that indent was eating the line. */
dialog.giDialog .indent { margin-left: 0; }
dialog.giDialog .helpEnd { margin-bottom: 0.2rem; color: #6b6257; font-size: 0.92rem; }
@media (prefers-reduced-motion: no-preference) {
    dialog.giDialog[open] > .modal-content { animation: giDialogIn 180ms ease-out; }
    @keyframes giDialogIn {
        from { opacity: 0; transform: translateY(8px) scale(0.99); }
        to   { opacity: 1; transform: none; }
    }
}

/* The board being replaced while the next one is fetched (NH00647). It stays exactly where it is,
   at its own size, so switching between Nice, Mean and Evil no longer collapses the box to one
   line and springs it back. Only the colour says something is happening. */
.boxStale { opacity: 0.45; transition: opacity 120ms linear; }

/* A guest's content filter (prefs.js): the strip dimmed and inert, and a watermark over the
   permissive columns. */
.guestFilterLocked .poolAxisRow, .guestFilterLocked .poolTrack { opacity: 0.45; }
#wordPoolMatrix { position: relative; }
.guestWatermark {
    position: absolute;
    left: 68%;
    top: 58%;
    width: 64%;
    transform: translate(-50%, -50%) rotate(-22deg);
    font: 700 clamp(1.9rem, 4.4vw, 3.9rem)/1.05 'Courier Prime', 'Courier New', monospace;
    color: var(--game-accent, #759B67);
    opacity: 0.22;
    text-align: center;
    pointer-events: none;
    z-index: 2;
}
/* "Theme:" before the look drop-down under the drawing. */
/* The same type as the drop-down it names, and shrunk with it (layout.js FIT_RULES). */
.lookLabel { white-space: nowrap; font-family: var(--mono); font-size: calc(11px + 0.35vmin); color: #3F5A33; }
