/* =====================================================================
   Fair Bingo — loaded on top of the hub's shared site.css.

   The card keeps its own bunting palette — fair red, federal blue and
   paper white, for the country's 250th — and just borrows the site's
   fonts, so it reads as a printed card sitting on the dark page.

   Two rules make that safe:

   1. NO :root BLOCK. This app's original tokens included --paper, --ink and
      --shadow, which site.css also defines with the *opposite* meanings
      (--paper is the dark page bg there, --ink is cream text). A :root block
      here would wreck the header, footer and every other page component.
      Every token is therefore prefixed --fair- and scoped to .fair.
   2. NO WEIGHTS ABOVE WHAT THE FONTS SHIP. Special Elite is 400 only and
      IBM Plex Mono is 400/500/600 only; anything heavier gets a smeared
      synthetic bold. Real bold lives on --font-sans, which has the range.

   Reset, box-sizing and .sr-only all come from site.css — not repeated here.
   ===================================================================== */

.fair {
    /* Bunting palette. Red runs brighter and navy goes federal-blue, so a
       Fair card never reads as the car-show one at a glance. Gold keeps its
       jobs unchanged: the armed outline and the winning line. Every hex here
       is duplicated in og-image.php — change one, change both. */
    --fair-cream: #f7f1e4;
    --fair-card: #fffdf7;
    --fair-red: #b2222c;
    --fair-red-dark: #7d1219;
    --fair-blue: #1b3a6b;
    --fair-blue-light: #2e5896;
    --fair-text: #16181d;
    --fair-silver: #c9ccd2;
    --fair-silver-dark: #767d86;
    --fair-gold: #d8a534;
    --fair-focus: #0f6ea8;
    --fair-shadow: 0 15px 35px rgb(22 24 29 / 18%);

    /* site.css sets body line-height 1.65; the card wants its tighter rhythm */
    font-family: var(--font-sans);
    line-height: 1.45;
}

.fair button {
    font: inherit;
}

/* Gold-on-cream (site.css's :focus-visible) is too low-contrast inside the
   card. This selector's specificity beats the site's bare :focus-visible. */
.fair button:focus-visible,
.instructions:focus-visible,
.instructions button:focus-visible {
    outline: 4px solid var(--fair-focus);
    outline-offset: 3px;
}

/* ---- The cream panel the whole game sits on ------------------------ */
.fair-shell {
    overflow: hidden; /* clips the bunting band to the rounded top corners */
    border-radius: var(--radius-lg);
    color: var(--fair-text);
    background: var(--fair-cream);
    box-shadow: var(--shadow-lg);
}

/* Bunting rather than the car show's checkerboard: red, white, blue, white
   straight across the top of the card. Still clipped to the rounded corners
   by the shell's overflow: hidden. */
.fair-shell__band {
    height: 15px;
    border-bottom: 3px solid var(--fair-red);
    background:
        repeating-linear-gradient(
            90deg,
            var(--fair-red) 0 16px,
            var(--fair-card) 0 32px,
            var(--fair-blue) 0 48px,
            var(--fair-card) 0 64px
        );
}

.fair-shell__inner {
    padding:
        clamp(1rem, 4vw, 2rem)
        max(clamp(0.75rem, 2.5vw, 1.5rem), env(safe-area-inset-right))
        clamp(1.25rem, 4vw, 2rem)
        max(clamp(0.75rem, 2.5vw, 1.5rem), env(safe-area-inset-left));
}

/* ---- Masthead ------------------------------------------------------- */
.masthead {
    position: relative;
    margin-bottom: clamp(0.9rem, 3vw, 1.4rem);
    text-align: center;
}

.masthead::before,
.masthead::after {
    content: "";
    position: absolute;
    top: 46%;
    width: clamp(20px, 7vw, 54px);
    height: 5px;
    border-block: 2px solid var(--fair-blue);
    background: var(--fair-red);
}

.masthead::before {
    left: 0;
}

.masthead::after {
    right: 0;
}

.masthead__eyebrow,
.instructions__kicker {
    margin: 0 0 0.35rem;
    color: var(--fair-red);
    font-family: var(--font-mono);
    font-size: clamp(0.62rem, 2.2vw, 0.76rem);
    font-weight: 500;
    letter-spacing: 0.18em;
    text-transform: uppercase;
}

/* Special Elite is wider and looser than the Impact this used to be, so the
   display sizes come down and the uppercase/chrome-shadow poster treatment
   (which depended on Impact's solidity) is dialled back to a soft offset. */
.masthead h1 {
    margin: 0;
    color: var(--fair-blue);
    font-family: var(--font-type);
    font-size: clamp(1.9rem, 7vw, 3.2rem);
    font-weight: 400;
    letter-spacing: 0.005em;
    line-height: 1.02;
    text-shadow: 2px 2px 0 rgb(199 203 208 / 70%);
}

.masthead h1 span {
    color: var(--fair-red);
}

.masthead__subtitle {
    max-width: 35rem;
    margin: 0.6rem auto 0;
    color: var(--fair-blue-light);
    font-size: clamp(0.83rem, 2.8vw, 0.98rem);
    font-weight: 600;
}

/* ---- The pitch ------------------------------------------------------
   The five-second explanation, and the first thing under the title.

   A <details> so it works with no JavaScript and so closing it is free; the
   <summary> keeps the hook on screen even when the rest is folded away, which
   is what a returning player sees. The marker is removed because this reads as
   a sentence, not a disclosure widget — but it stays keyboard-focusable and
   still announces its expanded state, so the affordance is intact for anyone
   navigating by keyboard or screen reader. */
.pitch {
    margin: 0.75rem auto 1.1rem;
    max-width: 34rem;
    text-align: center;
}

.pitch__summary {
    cursor: pointer;
    list-style: none;
}

.pitch__summary::-webkit-details-marker { display: none; }

.pitch__hook {
    display: block;
    color: var(--fair-red-dark);
    font-family: var(--font-display);
    font-size: clamp(1.05rem, 4.4vw, 1.5rem);
    line-height: 1.25;
}

.pitch__summary:focus-visible {
    outline: 3px solid var(--fair-focus);
    outline-offset: 4px;
    border-radius: 6px;
}

/* Small caps, wide tracking, and the blue: the three imperatives are the
   instruction, and they should read as a sign rather than a sentence. */
.pitch__steps {
    margin: 0.55rem 0 0;
    color: var(--fair-blue);
    font-family: var(--font-mono);
    font-size: clamp(0.76rem, 3vw, 0.92rem);
    font-weight: 600;
    letter-spacing: 0.09em;
    text-transform: uppercase;
}

.pitch__body {
    margin: 0.5rem 0 0;
    color: var(--fair-text);
    font-size: clamp(0.82rem, 2.9vw, 0.95rem);
    line-height: 1.5;
}

/* Closed, the hook is doing all the work and wants a little more room under it
   than the open state — where .pitch__steps already supplies the gap. */
.pitch:not([open]) {
    margin-bottom: 0.9rem;
}

/* ---- The card ------------------------------------------------------- */
.game-card {
    position: relative;
    padding: clamp(0.5rem, 2vw, 1rem);
    border: 3px solid var(--fair-blue);
    border-radius: 16px;
    background: var(--fair-card);
    box-shadow: var(--fair-shadow), inset 0 0 0 3px var(--fair-silver);
}

.game-card::before {
    content: "";
    position: absolute;
    inset: 5px;
    z-index: 0;
    border: 1px solid rgb(119 127 135 / 45%);
    border-radius: 9px;
    pointer-events: none;
}

.game-card > * {
    position: relative;
    z-index: 1;
}

.dashboard {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    margin: 0.2rem 0 0.45rem;
    padding-inline: 0.2rem;
}

.dashboard__item {
    display: flex;
    align-items: baseline;
    gap: 0.45rem;
    margin: 0;
    color: var(--fair-blue);
    font-size: clamp(0.78rem, 2.7vw, 0.95rem);
}

.dashboard__label {
    color: var(--fair-silver-dark);
    font-family: var(--font-mono);
    font-size: 0.74em;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

/* VT323 as a little dashboard readout. It runs small for its em size, hence
   the bump; it's already monospaced so tabular-nums is redundant. */
.dashboard strong {
    font-family: var(--font-terminal);
    font-size: 1.35em;
    font-weight: 400;
    letter-spacing: 0.04em;
    line-height: 1;
}

.bingo-letters {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: clamp(3px, 1vw, 7px);
    margin-bottom: 0.3rem;
    color: var(--fair-red);
    font-family: var(--font-type);
    font-size: clamp(1rem, 4vw, 1.6rem);
    line-height: 1.15;
    text-align: center;
}

.bingo-grid {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: clamp(3px, 1vw, 7px);
    width: 100%;
}

.bingo-square {
    position: relative;
    display: grid;
    place-items: center;
    min-width: 0;
    min-height: 0;
    aspect-ratio: 1;
    padding: clamp(0.16rem, 0.8vw, 0.45rem);
    overflow: hidden;
    border: 2px solid var(--fair-blue);
    border-radius: clamp(5px, 1.4vw, 9px);
    color: var(--fair-blue);
    background:
        linear-gradient(145deg, rgb(255 255 255 / 75%), transparent 55%),
        var(--fair-card);
    box-shadow: inset 0 -3px 0 rgb(17 38 59 / 10%);
    cursor: pointer;
    touch-action: manipulation;
    transition: transform 120ms ease, background-color 160ms ease, color 160ms ease, box-shadow 160ms ease;
}

.bingo-square:not(:disabled):active {
    transform: scale(0.96);
}

@media (hover: hover) {
    .bingo-square:not(:disabled):hover {
        border-color: var(--fair-red);
        box-shadow: inset 0 -3px 0 rgb(153 31 39 / 15%), 0 3px 7px rgb(17 38 59 / 16%);
    }
}

/* Stays on --font-sans: the system stack has a real 700, and at this size it's
   far more legible in a 5-across cell than a mono or typewriter face.

   The system stack is wider than the Trebuchet MS this used to be (much wider
   on Linux, where it lands on DejaVu Sans), and a square is a fixed 1:1 box
   with overflow hidden — so the longest prompts clipped. script.js tags each
   label with a data-len bucket and the two rules below step the type down for
   just those, keeping the short labels at full size. */
.square__label {
    position: relative;
    z-index: 2;
    max-width: 100%;
    overflow-wrap: anywhere;
    font-family: var(--font-sans);
    font-size: clamp(0.53rem, 2.25vw, 0.86rem);
    font-weight: 700;
    hyphens: auto;
    line-height: 1.05;
    text-align: center;
}

.square__label[data-len="lg"] {
    font-size: clamp(0.49rem, 2.05vw, 0.79rem);
}

.square__label[data-len="xl"] {
    font-size: clamp(0.45rem, 1.85vw, 0.72rem);
    letter-spacing: -0.01em;
}

.square__mark {
    position: absolute;
    top: 3px;
    right: 3px;
    z-index: 3;
    display: none;
    width: clamp(15px, 4.4vw, 25px);
    aspect-ratio: 1;
    place-items: center;
    border: 2px solid currentcolor;
    border-radius: 50%;
    background: var(--fair-card);
    font-family: var(--font-sans);
    font-size: clamp(0.62rem, 2.5vw, 0.92rem);
    font-weight: 700;
    line-height: 1;
}

.bingo-square.is-marked {
    border-color: var(--fair-red-dark);
    color: var(--fair-card);
    background:
        repeating-linear-gradient(-45deg, transparent 0 7px, rgb(255 255 255 / 5%) 7px 9px),
        var(--fair-red);
    box-shadow: inset 0 0 0 2px rgb(255 250 240 / 25%);
}

.bingo-square.is-marked .square__label {
    padding-inline: 0.08rem;
    text-shadow: 0 1px 1px rgb(0 0 0 / 35%);
}

.bingo-square.is-marked .square__mark {
    display: grid;
    color: var(--fair-red-dark);
}

/* The player's own photograph, filling the square behind the words.

   The square is `display: grid; place-items: center` with one text child, and
   a photograph must not become a second grid item and shove the label off
   centre — so it comes out of flow entirely and fills the box. The button's
   existing overflow: hidden does the cropping and its position: relative is
   the containing block, so this drops in without touching the layout. */
.square__photo {
    position: absolute;
    inset: 0;
    z-index: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Fair red rather than black: the label already reads cream-on-red on every
   other marked square, so a photographed one keeps the same colour
   relationship instead of introducing a fourth.

   The numbers are arithmetic, not taste, and they changed once the card became
   the souvenir rather than the scoreboard.

   It used to be one flat 74% wash over the whole square, which put the label
   safely at 4.99:1 against a worst-case white photograph — and left only 26% of
   the photograph showing. Since the point of a finished card is now that it
   looks like somebody's day at the Fair, the wash drops to 45% and the label
   carries its own plate instead.

   Worst case (a white photograph), composited:
     photo area   45% of rgb(125 18 25) over white -> rgb(196 148 152)
     under label  70% of the same over that        -> rgb(146 57 63)
                  = 6.46:1 against the cream label, at 700 weight

   So the photograph goes from 26% visible to 55%, and the text gets MORE
   readable rather than less. Over a black photograph the plate lands at
   11.2:1, so both extremes are clear. Change either number and redo the sum. */
.square__scrim {
    position: absolute;
    inset: 0;
    z-index: 1;
    background: rgb(125 18 25 / 45%);
    pointer-events: none;
}

/* The plate. Only on squares that actually have a photograph — a marked square
   with no picture is already a solid red cell and would just get a slightly
   darker rectangle floating in the middle of it for no reason.

   The text shadow stays underneath it. The tick badge remains the non-colour
   half of "this one is done": a photograph is a visible difference rather than
   a colour cue, but it is still only a visual one. */
.bingo-square.has-photo .square__label {
    padding: 0.2em 0.3em;
    border-radius: 4px;
    background: rgb(125 18 25 / 70%);
    text-shadow: 0 1px 3px rgb(0 0 0 / 55%);
}

/* Between the file arriving and the picture being written. The camera hand-off
   on a phone already costs a second or two before any of this code runs, so the
   square has to acknowledge the tap immediately or it reads as not having
   registered. Two signals again, never colour alone: the dashed outline says
   "working" and the label says it out loud to a screen reader. */
.bingo-square.is-saving {
    outline: 3px dashed var(--fair-gold);
    outline-offset: -5px;
    animation: saving-breathe 1.2s ease-in-out infinite;
}

@keyframes saving-breathe {
    0%, 100% { outline-offset: -5px; }
    50% { outline-offset: -9px; }
}

.bingo-square.is-winning {
    outline: 3px solid var(--fair-gold);
    outline-offset: -5px;
}

.bingo-square.is-free {
    border-color: var(--fair-blue);
    color: var(--fair-card);
    background:
        radial-gradient(circle, rgb(255 255 255 / 15%) 0 2px, transparent 3px) 0 0 / 10px 10px,
        var(--fair-blue);
    cursor: default;
    opacity: 1;
}

.bingo-square.is-free .square__label {
    font-family: var(--font-type);
    font-size: clamp(0.6rem, 2.9vw, 1.05rem);
    font-weight: 400;
    letter-spacing: 0.02em;
    text-transform: uppercase;
}

.bingo-square.is-free .square__mark {
    color: var(--fair-blue);
}

/* Holds its height whether or not it has text, so arming a square doesn't
   shove the board up the page mid-tap. */
.board-hint {
    min-height: 1.6em;
    margin: 0.45rem 0 0;
    padding-inline: 0.3rem;
    color: var(--fair-red-dark);
    font-family: var(--font-mono);
    font-size: clamp(0.66rem, 2.4vw, 0.8rem);
    font-weight: 500;
    line-height: 1.3;
    text-align: center;
}

/* ---- Win state ------------------------------------------------------ */
.bingo-message {
    margin-top: 0.75rem;
    padding: clamp(0.7rem, 3vw, 1rem);
    border: 2px solid var(--fair-gold);
    border-radius: 10px;
    color: var(--fair-card);
    background: var(--fair-blue);
    box-shadow: inset 0 0 0 2px rgb(255 250 240 / 16%);
    text-align: center;
}

.bingo-message[hidden] {
    display: none;
}

.bingo-message__stamp {
    display: inline-block;
    margin-bottom: 0.3rem;
    padding: 0.1rem 0.6rem;
    border: 3px double var(--fair-card);
    color: var(--fair-card);
    font-family: var(--font-type);
    font-size: clamp(1.15rem, 4.5vw, 1.9rem);
    letter-spacing: 0.06em;
    line-height: 1.1;
    transform: rotate(-2deg);
}

.bingo-message p {
    max-width: 34rem;
    margin: 0 auto;
    font-size: clamp(0.8rem, 2.7vw, 0.96rem);
}

.bingo-message.is-celebrating {
    animation: victory-pulse 600ms ease-out;
}

/* ---- Controls ------------------------------------------------------- */
/* Four buttons across a 320px phone is unreadable, so they block up 2x2 and
   only go single-row once there's width for it. */
.controls {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.55rem;
    margin-top: 1rem;
}

@media (min-width: 560px) {
    .controls {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }
}

.control-button {
    min-height: 48px;
    padding: 0.65rem 0.55rem;
    border: 2px solid var(--fair-blue);
    border-radius: 8px;
    color: var(--fair-blue);
    background: var(--fair-card);
    box-shadow: 0 4px 0 var(--fair-blue);
    font-family: var(--font-mono);
    font-size: clamp(0.72rem, 2.8vw, 0.9rem);
    font-weight: 600;
    letter-spacing: 0.02em;
    cursor: pointer;
    touch-action: manipulation;
    transition: transform 120ms ease, box-shadow 120ms ease, color 120ms ease, background-color 120ms ease;
}

.control-button:hover {
    color: var(--fair-red);
}

.control-button:active {
    transform: translateY(3px);
    box-shadow: 0 1px 0 var(--fair-blue);
}

/* Lit once there's a line on the board — a nudge at the one moment there's a
   result worth sending, without promoting Share over New Card the rest of the
   time. */
.control-button.is-hot {
    border-color: var(--fair-gold);
    box-shadow: 0 4px 0 var(--fair-gold);
}

.control-button--primary {
    border-color: var(--fair-red-dark);
    color: var(--fair-card);
    background: var(--fair-red);
    box-shadow: 0 4px 0 var(--fair-red-dark);
}

.control-button--primary:hover {
    color: var(--fair-card);
    background: var(--fair-red-dark);
}

.fair-sign {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.65rem;
    margin: 1.4rem auto 0;
    color: var(--fair-blue-light);
    font-family: var(--font-mono);
    font-size: clamp(0.68rem, 2.4vw, 0.8rem);
    font-weight: 500;
    text-align: center;
}

.fair-sign p {
    margin: 0;
}

.fair-sign span {
    color: var(--fair-red);
}

.noscript-message {
    margin-top: 1rem;
    padding: 1rem;
    border: 2px solid var(--fair-red);
    color: var(--fair-red-dark);
    background: var(--fair-card);
    font-family: var(--font-sans);
    font-weight: 700;
    text-align: center;
}

/* ---- Dialogs (instructions + clear confirmation) --------------------
   Both live outside .fair in the DOM (the top layer moves them anyway), so
   they carry their own copy of the tokens they need. --fair-blue-light and
   --fair-silver-dark are here for the confirmation's tally readout. */
.instructions {
    --fair-cream: #f7f1e4;
    --fair-card: #fffdf7;
    --fair-red: #b2222c;
    --fair-red-dark: #7d1219;
    --fair-blue: #1b3a6b;
    --fair-blue-light: #2e5896;
    --fair-text: #16181d;
    --fair-silver: #c9ccd2;
    --fair-silver-dark: #767d86;
    --fair-gold: #d8a534;
    --fair-focus: #0f6ea8;

    /* A modal <dialog> is centered by the UA's `margin: auto` against its
       zeroed insets — and site.css line 86 is `* { margin: 0 }`, which wipes
       exactly that and drops the panel in the top-left corner. Centering is
       therefore restored by hand. height/inset are spelled out too so this
       doesn't lean on the rest of the UA rule surviving the same reset.
       z-index is inert for showModal (the top layer outranks it) and only
       matters on the legacy `open` attribute fallback. */
    position: fixed;
    inset: 0;
    z-index: 200;
    height: fit-content;
    margin: auto;

    width: min(calc(100% - 2rem), 520px);
    max-height: min(85vh, 640px);
    max-height: min(85dvh, 640px);
    padding: clamp(1.1rem, 5vw, 2rem);
    overflow: auto;
    border: 3px solid var(--fair-blue);
    border-radius: 14px;
    color: var(--fair-text);
    background: var(--fair-card);
    box-shadow: 0 24px 70px rgb(0 0 0 / 45%), inset 0 0 0 3px var(--fair-silver);
    font-family: var(--font-sans);
    line-height: 1.45;
}

.instructions::backdrop {
    background: rgb(9 20 31 / 75%);
    backdrop-filter: blur(2px);
}

.instructions__header {
    padding-bottom: 0.7rem;
    border-bottom: 5px solid var(--fair-red);
}

.instructions h2 {
    margin: 0;
    color: var(--fair-blue);
    font-family: var(--font-type);
    font-size: clamp(1.5rem, 6.5vw, 2.5rem);
    font-weight: 400;
    letter-spacing: 0.01em;
    line-height: 1.05;
}

.instructions ol {
    display: grid;
    gap: 0.75rem;
    margin: 1.25rem 0 1.5rem;
    padding-left: 1.7rem;
}

.instructions li {
    padding-left: 0.25rem;
    font-weight: 600;
}

.instructions li::marker {
    color: var(--fair-red);
    font-weight: 700;
}

.instructions .control-button {
    width: 100%;
}

/* ---- Clear-board confirmation --------------------------------------
   Same cream panel as the instructions, sized down to a question. */
.instructions--confirm {
    width: min(calc(100% - 2rem), 460px);
}

.instructions__body {
    margin: 1.2rem 0 0;
    font-weight: 500;
}

/* "New Card" here names the button beside it, so it gets the button's red
   rather than an italic that would read as ordinary emphasis. */
.instructions__body em {
    color: var(--fair-red);
    font-style: normal;
    font-weight: 700;
}

/* ---- Share panel ----------------------------------------------------
   The emoji grid is the one place in the game where the card is drawn at a
   glance, so it gets room to breathe and a monospace box to sit square in. */
.share-grid {
    margin: 1rem 0 0;
    font-family: var(--font-mono);
    font-size: clamp(0.95rem, 4.2vw, 1.25rem);
    letter-spacing: 0.08em;
    line-height: 1.3;
    text-align: center;
    white-space: pre;
}

.share-text__label {
    display: block;
    margin: 0.9rem 0 0.3rem;
    color: var(--fair-silver-dark);
    font-family: var(--font-mono);
    font-size: 0.72rem;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

/* Readonly, but still selectable: it's the manual fallback when neither the
   share sheet nor the clipboard is available. */
/* Fine print rather than a second display: the grid above is the thing you
   look at, this is the literal payload, kept small so the panel still fits
   inside .instructions' max-height with the buttons above the fold. */
.share-text {
    display: block;
    width: 100%;
    padding: 0.6rem 0.7rem;
    border: 2px dashed var(--fair-silver);
    border-radius: 8px;
    color: var(--fair-text);
    background: #fff;
    font-family: var(--font-mono);
    font-size: 0.7rem;
    line-height: 1.4;
    resize: vertical;
}

.instructions--share .instructions__actions {
    margin-top: 1rem;
}

/* With no navigator.share the sheet button is hidden, and a two-column grid
   would leave Copy stranded in half the panel. */
.instructions__actions.is-single {
    grid-template-columns: minmax(0, 1fr);
}

.share-done {
    width: 100%;
    margin-top: 0.55rem;
}

/* Reuses the card's dashboard readout, boxed like a torn-off stub. */
.confirm-tally {
    margin: 1.1rem 0 0;
    padding: 0.5rem 0.85rem;
    border: 2px dashed var(--fair-silver);
    border-radius: 8px;
}

.instructions__actions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.6rem;
    margin-top: 1.5rem;
}

/* Under ~340px the two answers stop fitting side by side; stack them with
   the safe one on top so it stays nearest the thumb. */
@media (max-width: 360px) {
    .instructions__actions {
        grid-template-columns: minmax(0, 1fr);
    }
}

body.dialog-open {
    overflow: hidden;
}

/* ---- Confetti ------------------------------------------------------- */
.confetti {
    position: fixed;
    inset: 0;
    z-index: 100;
    overflow: hidden;
    pointer-events: none;
}

.confetti__piece {
    position: absolute;
    top: -10vh;
    left: var(--x);
    width: 10px;
    height: 18px;
    border-radius: 2px;
    background: var(--color);
    opacity: 0;
    animation: confetti-fall var(--duration) var(--delay) cubic-bezier(0.15, 0.65, 0.4, 1) forwards;
}

@keyframes victory-pulse {
    0% { transform: scale(0.97); }
    55% { transform: scale(1.015); }
    100% { transform: scale(1); }
}

@keyframes confetti-fall {
    0% {
        opacity: 1;
        transform: translate3d(0, -5vh, 0) rotate(0deg);
    }
    100% {
        opacity: 0;
        transform: translate3d(var(--drift), 115vh, 0) rotate(var(--spin));
    }
}

/* ---- Narrow phones -------------------------------------------------- */
@media (max-width: 390px) {
    /* The board wants every pixel it can get at this width, and the site's
       default gutter is generous for a full-bleed card. */
    .fair .container {
        padding-inline: 0.6rem;
    }

    .fair-shell__inner {
        padding-left: max(0.6rem, env(safe-area-inset-left));
        padding-right: max(0.6rem, env(safe-area-inset-right));
    }

    .game-card {
        padding: 0.4rem;
        border-width: 2px;
        border-radius: 11px;
    }

    .bingo-square {
        border-width: 1.5px;
    }

    .masthead::before,
    .masthead::after {
        display: none;
    }

    .controls {
        gap: 0.35rem;
    }
}

/* Small phones (SE-class 320px up through the 360px Android floor). Five
   columns leaves ~41-49px of usable width per square, so the long prompts
   need the tighter steps to stay readable rather than clipped. */
@media (max-width: 360px) {
    .square__label {
        font-size: 0.46rem;
    }

    .square__label[data-len="lg"] {
        font-size: 0.42rem;
    }

    .square__label[data-len="xl"] {
        font-size: 0.38rem;
    }

    .bingo-square.is-free .square__label {
        font-size: 0.62rem;
    }

    .square__mark {
        top: 1px;
        right: 1px;
        width: 14px;
        border-width: 1px;
    }

    /* The controls used to be three across at this width and needed shrinking
       to fit; at 2x2 each button has roughly double the room, so the base
       clamp is comfortable again. */
}

@media (prefers-reduced-motion: reduce) {
    .fair *,
    .fair *::before,
    .fair *::after,
    .instructions *,
    .confetti * {
        scroll-behavior: auto !important;
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }

    .confetti {
        display: none;
    }
}

/* ---- The photo layer's panels ---------------------------------------------
   All of these ride on .instructions, which is not optional: site.css line 86
   is `* { margin: 0 }`, which wipes the UA's `margin: auto` and drops any
   dialog that doesn't carry .instructions into the top-left corner. */

.instructions--photo,
.instructions--submit {
    width: min(calc(100% - 2rem), 560px);
}

/* The square's prompt, repeated in the panel so the player knows what they're
   pointing the camera at without having to remember. */
.capture-prompt {
    margin-bottom: 0.9rem;
    padding: 0.6rem 0.75rem;
    border-left: 5px solid var(--fair-red);
    color: var(--fair-text);
    background: var(--fair-cream);
    font-family: var(--font-sans);
    font-size: clamp(0.95rem, 4vw, 1.15rem);
    font-weight: 700;
    line-height: 1.3;
}

/* Big, and stacked rather than side by side: this is the one panel that gets
   used one-handed, in daylight, while walking. */
.capture-actions .control-button {
    min-height: 56px;
    font-size: clamp(0.85rem, 3.4vw, 1rem);
}

/* Deliberately quieter than the two above it. A photograph is the point of
   the game now, so the escape hatch should be findable without competing. */
.capture-skip {
    width: 100%;
    margin-top: 0.75rem;
    border-style: dashed;
    color: var(--fair-silver-dark);
    box-shadow: none;
}

.capture-skip:active {
    transform: none;
    box-shadow: none;
}

/* Swapped in when IndexedDB is unusable — private browsing, or a full disk.
   The game still plays; it just can't keep pictures, and saying so beats a
   camera button that silently does nothing. */
.capture-unavailable {
    display: none;
}

body.photos-off .capture-unavailable {
    display: block;
}

body.photos-off .capture-actions,
body.photos-off .capture-note {
    display: none;
}

.photo-preview {
    margin-bottom: 1rem;
    text-align: center;
}

.photo-preview img {
    max-width: 100%;
    max-height: 46vh;
    border: 2px solid var(--fair-silver);
    border-radius: 8px;
    object-fit: contain;
}

.share-image {
    width: 100%;
    margin-bottom: 0.9rem;
}

/* Save All My Photos. Full width and directly under the card button, because
   the two are the same kind of request — "give me the thing I made" — and a
   player looking for one will look for the other in the same place. Quieter
   than the card button: the card is what most people came to Share for. */
.share-photos {
    width: 100%;
    margin-bottom: 0.9rem;
}

/* Save to My Phone, in the photo panel. Sits above the Replace/Remove row
   rather than inside it: those two change the card, this one does not, and
   putting a harmless action in a row of destructive ones is how a thumb finds
   Remove by accident. */
.photo-save {
    width: 100%;
    margin-bottom: 0.4rem;
    border-color: var(--fair-blue);
    box-shadow: 0 4px 0 var(--fair-blue);
}

.photo-save-note {
    margin: 0 0 0.9rem;
    color: var(--fair-silver-dark);
    font-size: 0.82rem;
    line-height: 1.4;
}

/* Where the card lands when there's no OS share sheet to hand it to. */
.share-card {
    margin-bottom: 1rem;
    text-align: center;
}

.share-card img {
    max-width: 100%;
    max-height: 50vh;
    border: 2px solid var(--fair-silver);
    border-radius: 8px;
}

.share-card figcaption {
    margin: 0.5rem 0;
    color: var(--fair-silver-dark);
    font-family: var(--font-mono);
    font-size: 0.8rem;
}

.share-card .control-button {
    display: inline-block;
    text-decoration: none;
}

.submit-offer {
    width: 100%;
    margin-top: 0.9rem;
    border-color: var(--fair-gold);
    box-shadow: 0 4px 0 var(--fair-gold);
}

/* ---- The submission form ------------------------------------------------ */

.submit-label {
    display: block;
    margin: 0.85rem 0 0.25rem;
    color: var(--fair-blue);
    font-family: var(--font-mono);
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.submit-label span {
    color: var(--fair-silver-dark);
    text-transform: none;
}

.submit-input {
    width: 100%;
    /* 16px is the floor: anything smaller and iOS zooms the page on focus,
       which on a card that fills the screen is a horrible surprise. */
    min-height: 48px;
    padding: 0.55rem 0.65rem;
    border: 2px solid var(--fair-silver);
    border-radius: 8px;
    color: var(--fair-text);
    background: var(--fair-card);
    font-family: var(--font-sans);
    font-size: 16px;
}

.submit-input:focus-visible {
    border-color: var(--fair-focus);
}

/* How the location check works, in the game's own voice. Deliberately NOT
   dressed as legal copy: it sits above the dashed consent box, quieter than
   the form and plainer than the licence, because it is a note about what the
   game does rather than a term anybody is agreeing to. */
.submit-note {
    margin: 1.1rem 0 0;
    color: var(--fair-silver-dark);
    font-family: var(--font-sans);
    font-size: 0.8rem;
    line-height: 1.45;
}

/* The legal copy sits in its own visibly separate box, because it is
   placeholder wording waiting on the Fair and it should look like a quoted
   document rather than like the game talking. */
.consent-block {
    margin: 1.1rem 0 0.75rem;
    padding: 0.75rem 0.85rem;
    border: 2px dashed var(--fair-silver);
    border-radius: 8px;
    background: var(--fair-cream);
}

.consent-block__label {
    margin-bottom: 0.4rem;
    color: var(--fair-red-dark);
    font-family: var(--font-mono);
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
}

.consent-block__label span {
    color: var(--fair-silver-dark);
    font-weight: 400;
    text-transform: none;
}

.consent-block__body {
    max-height: 11rem;
    overflow-y: auto;
    color: var(--fair-text);
    font-family: var(--font-sans);
    font-size: 0.82rem;
    line-height: 1.5;
}

.consent-check {
    display: flex;
    gap: 0.6rem;
    align-items: flex-start;
    margin-bottom: 0.9rem;
    font-family: var(--font-sans);
    font-size: 0.9rem;
    line-height: 1.35;
}

/* The auto-save toggle. Borrows the consent checkbox's 24px hit target — this
   is read and tapped in the same conditions, one-handed in daylight — but sits
   quieter, since it is a preference rather than something being agreed to. */
.autosave-check {
    margin-top: 0.75rem;
    margin-bottom: 0.5rem;
    font-size: 0.86rem;
}

.autosave-check small {
    display: block;
    margin-top: 0.2rem;
    color: var(--fair-silver-dark);
    font-size: 0.76rem;
    line-height: 1.4;
}

.consent-check input {
    flex: 0 0 auto;
    width: 24px;
    height: 24px;
    margin-top: 1px;
    accent-color: var(--fair-red);
}

.submit-errors {
    margin-bottom: 0.75rem;
    padding: 0.6rem 0.7rem;
    border-left: 5px solid var(--fair-red);
    color: var(--fair-red-dark);
    background: var(--fair-cream);
    font-family: var(--font-sans);
    font-size: 0.88rem;
}

.submit-status {
    margin-bottom: 0.6rem;
    min-height: 1.4em;
    color: var(--fair-blue-light);
    font-family: var(--font-mono);
    font-size: 0.85rem;
}

.submit-done {
    margin-bottom: 0.9rem;
    padding: 0.85rem;
    border: 2px solid var(--fair-blue);
    border-radius: 8px;
    background: var(--fair-cream);
    font-family: var(--font-sans);
    font-size: 0.9rem;
    line-height: 1.45;
}

.submit-done p + p {
    margin-top: 0.5rem;
    color: var(--fair-silver-dark);
    font-size: 0.84rem;
}

.submit-send {
    width: 100%;
    min-height: 52px;
    margin-bottom: 0.6rem;
}

.submit-send[disabled] {
    opacity: 0.6;
}

/* ---- Print: a blank card to carry around the show ------------------- */
/* ---- Progress, the three steps, and the blackout ---------------------- */

/* "12 of 24 Fair moments captured · 2 Bingos".

   Reserves its own line height whether or not it has anything to say, for the
   same reason .board-hint does: this sits directly above a grid of tap targets,
   and text appearing under a moving thumb is how somebody marks the wrong
   square. Empty and present beats absent and then sudden. */
.progress-line {
    min-height: 1.35em;
    margin: 0 0 0.35rem;
    padding-inline: 0.2rem;
    color: var(--fair-red-dark);
    font-family: var(--font-mono);
    font-size: clamp(0.72rem, 2.6vw, 0.85rem);
    font-weight: 600;
    letter-spacing: 0.01em;
}

/* The three steps. Numbered by the list, so the numerals stay real numerals to
   a screen reader instead of becoming decorative text. */
.instructions .steps {
    margin: 0.4rem 0 0.9rem;
    padding-left: 1.4rem;
}

.instructions .steps li {
    margin-bottom: 0.5rem;
    line-height: 1.45;
}

.instructions .steps strong {
    color: var(--fair-red-dark);
    font-family: var(--font-mono);
    font-size: 0.94em;
    letter-spacing: 0.05em;
    text-transform: uppercase;
}

.instructions__body--lead {
    color: var(--fair-blue);
    font-weight: 600;
}

/* The demoted six-step list. Present, answering real questions, and visibly
   secondary to the three steps above it. */
.instructions__kicker--sub {
    margin-top: 1.1rem;
    margin-bottom: 0.3rem;
}

.instructions__aside {
    margin: 0 0 0.9rem;
    padding-left: 1.2rem;
    color: var(--fair-silver-dark);
    font-size: 0.88em;
    line-height: 1.45;
}

.instructions__aside li {
    margin-bottom: 0.3rem;
}

/* Blackout is the one that gets to be loud. It is the end of a whole day's
   walking, so the panel is warmer, the stamp is bigger, and the title stops
   pretending to be a sentence. Everything here is a class on the existing
   milestone panel rather than a second panel. */
.instructions--milestone.is-blackout {
    border-color: var(--fair-gold);
    box-shadow: var(--fair-shadow), inset 0 0 0 3px var(--fair-gold);
}

.instructions--milestone.is-blackout h2 {
    color: var(--fair-red-dark);
    font-size: clamp(1.5rem, 6.5vw, 2.1rem);
    letter-spacing: 0.02em;
    line-height: 1.05;
    text-transform: uppercase;
}

.instructions--milestone.is-blackout .instructions__kicker {
    color: var(--fair-gold);
}

@media print {
    @page {
        size: portrait;
        margin: 0.4in;
    }

    *,
    *::before,
    *::after {
        print-color-adjust: exact;
        -webkit-print-color-adjust: exact;
    }

    html,
    body {
        background: #fff;
    }

    /* The print stylesheet exists to produce a blank card to carry round, so
       the photographs go the same way the marks do — and a page of them would
       empty an ink cartridge besides. */
    .square__photo,
    .square__scrim,
    .skip-link,
    .site-header,
    .site-footer,
    .fair-shell__band,
    .controls,
    .board-hint,
    .progress-line,
    .pitch,
    .fair-sign,
    .confetti,
    .instructions,
    .bingo-message {
        display: none !important;
    }

    /* The photograph and its wash are both gone by the rule above, so the
       label's plate would print as a dark rectangle sitting behind text on an
       otherwise blank carry-around card. */
    .bingo-square.has-photo .square__label {
        padding: 0;
        background: none;
        text-shadow: none;
    }

    .fair {
        padding: 0;
    }

    .fair .container,
    .fair .container--narrow {
        max-width: 100%;
        padding-inline: 0;
    }

    .fair-shell {
        width: 100%;
        max-width: 7.5in;
        border-radius: 0;
        background: #fff;
        box-shadow: none;
    }

    .fair-shell__inner {
        padding: 0;
    }

    .masthead {
        margin-bottom: 0.25in;
    }

    .masthead h1 {
        font-size: 34pt;
        text-shadow: none;
    }

    .game-card {
        box-shadow: none;
        break-inside: avoid;
    }

    .bingo-square {
        box-shadow: none;
    }
}
