/*
 * Tilesum -- Minimal Tailwind replacement CSS
 *
 * Hand-built utility classes matching the exact set used in templates.
 * Replaces the Tailwind CDN dependency.
 */

/* -- Reset / Base -- */
*, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

/* Links carry no underline by default. This reset was missing, so every
   anchor on the site wore the browser's default underline -- the wordmark,
   the nav, the footer -- and the `hover:underline` classes the templates
   already use were silently no-ops, since there was nothing to add.
   Underline now means something: it is applied deliberately, and only where
   a link sits inside running prose. There, color alone would be the only
   thing marking it as a link (WCAG 1.4.1); in a nav row or the footer the
   grouping already says so. */
a {
    color: inherit;
    text-decoration: none;
}

/* Buttons inherit their surroundings instead of the operating system's.
   Several controls are buttons only because they must POST -- "Log out" in the
   nav, the back-office row actions -- and they were rendering with native
   chrome: a light gray face under text colored for a dark header, which
   measured about 2:1. The same text on the header itself is 5.7:1.
   `.btn` and the utility background classes are defined below and still win,
   so real buttons are unaffected. */
button {
    font: inherit;
    color: inherit;
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
}

/* Removing underlines takes away a hover affordance, so keyboard focus needs
   one of its own -- and it needed one regardless (WCAG 2.4.7). Only
   :focus-visible, so a mouse click does not leave a ring behind. */
a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible {
    outline: 2px solid var(--color-link);
    outline-offset: 2px;
    border-radius: 2px;
}

/* ============================================
   Theme Tokens

   Every chrome color in this file resolves to one of these custom
   properties, so a theme is defined once here rather than by restating
   rules. `base.html` puts `theme-<name>` on <body> from `User.theme`;
   the default palette on :root is what anonymous and free users get.

   The utility classes below keep their Tailwind-subset names
   (.bg-slate-800 and friends) because they are used across every
   template, but they no longer hold literal slate values -- they point
   at these tokens. Read the names as roles, not as colors.

   The --board-* tokens are read by tilesum.js and handed to Phaser, so
   the canvas is themed along with the page around it.

   --color-success / --color-warning / --color-danger are the page's status
   text. They were the literals #4ade80, #fbbf24 and #f87171 wherever a solve
   time, a warning or a form error needed a color, which put green text at
   about 1.5:1 on the two light themes. The eight dark themes keep the original
   values, so only what was broken changed.

   --board-met and --board-over are the two colors a target takes when its
   line hits or overshoots its sum. They were literals in tilesum.js, which
   made the met green #4ade80 on paper's cream board -- 1.6:1, invisible on
   the one theme most likely to be picked for legibility. Every theme now
   names its own pair, and check_contrast holds them to 4.5:1 like any other
   text. The eight dark themes still resolve to the original two colors, so
   nothing a player already knows has changed.
   ============================================ */
:root {
    --color-bg: #0f172a;
    --color-surface: #1e293b;
    --color-border: #334155;
    --color-border-strong: #475569;
    --color-text: #e2e8f0;
    --color-text-dim: #94a3b8;
    --color-text-muted: #8391a6;
    --color-text-bright: #ffffff;
    --color-link: #60a5fa;
    --color-success: #4ade80;
    --color-warning: #fbbf24;
    --color-danger: #f87171;

    --board-bg: #0f172a;
    --board-grid: #1e293b;
    --board-line: #63748f;
    --board-text: #e2e8f0;
    --board-text-dim: #8391a6;
    --board-met: #4ade80;
    --board-over: #f87171;
}

body.theme-dark {
    --color-bg: #111827;
    --color-surface: #1f2937;
    --color-border: #374151;
    --color-border-strong: #4b5563;
    --color-text: #e5e7eb;
    --color-text-dim: #9ca3af;
    --color-text-muted: #89909d;
    --color-text-bright: #ffffff;
    --color-link: #60a5fa;
    --color-success: #4ade80;
    --color-warning: #fbbf24;
    --color-danger: #f87171;

    --board-bg: #111827;
    --board-grid: #1f2937;
    --board-line: #62748d;
    --board-text: #e5e7eb;
    --board-text-dim: #89909d;
    --board-met: #4ade80;
    --board-over: #f87171;
}

body.theme-midnight {
    --color-bg: #020617;
    --color-surface: #0f172a;
    --color-border: #1e293b;
    --color-border-strong: #334155;
    --color-text: #cbd5e1;
    --color-text-dim: #94a3b8;
    --color-text-muted: #72829a;
    --color-text-bright: #f1f5f9;
    --color-link: #60a5fa;
    --color-success: #4ade80;
    --color-warning: #fbbf24;
    --color-danger: #f87171;

    --board-bg: #020617;
    --board-grid: #0f172a;
    --board-line: #596581;
    --board-text: #cbd5e1;
    --board-text-dim: #72829a;
    --board-met: #4ade80;
    --board-over: #f87171;
}

body.theme-paper {
    --color-bg: #faf5e4;
    --color-surface: #fffdf5;
    --color-border: #ddd5bd;
    --color-border-strong: #c4b998;
    --color-text: #2c2a24;
    --color-text-dim: #57534e;
    --color-text-muted: #756e69;
    --color-text-bright: #1c1917;
    --color-link: #1d4ed8;
    --color-success: #166534;
    --color-warning: #92400e;
    --color-danger: #b91c1c;

    --board-bg: #faf5e4;
    --board-grid: #efe7cf;
    --board-line: #8e8362;
    --board-text: #2c2a24;
    --board-text-dim: #6c6661;
    --board-met: #166534;
    --board-over: #b91c1c;
}

body.theme-forest {
    --color-bg: #14532d;
    --color-surface: #166534;
    --color-border: #22703f;
    --color-border-strong: #2f8850;
    --color-text: #e7f5eb;
    --color-text-dim: #b8d6c3;
    --color-text-muted: #bbd6c4;
    --color-text-bright: #ffffff;
    --color-link: #86efac;
    --color-success: #86efac;
    --color-warning: #fde68a;
    --color-danger: #fed7d7;

    --board-bg: #14532d;
    --board-grid: #166534;
    --board-line: #91b29d;
    --board-text: #e7f5eb;
    --board-text-dim: #bbd6c4;
    --board-met: #86efac;
    --board-over: #fecaca;
}

/* Deep blue-green. The only cool-dark theme that is not a neutral. */
body.theme-ocean {
    --color-bg: #082f38;
    --color-surface: #0d3f4a;
    --color-border: #415c61;
    --color-border-strong: #567a81;
    --color-text: #ddf1f4;
    --color-text-dim: #a8ced5;
    --color-text-muted: #8fbcc4;
    --color-text-bright: #ffffff;
    --color-link: #5fd3e8;
    --color-success: #4ade80;
    --color-warning: #fbbf24;
    --color-danger: #fca5a5;

    --board-bg: #082f38;
    --board-grid: #0d3f4a;
    --board-line: #668a92;
    --board-text: #ddf1f4;
    --board-text-dim: #8fbcc4;
    --board-met: #4ade80;
    --board-over: #fb8f8f;
}

/* Warm dark: charcoal browns with an amber link, the counterweight to ocean. */
body.theme-ember {
    --color-bg: #241713;
    --color-surface: #33211b;
    --color-border: #57413a;
    --color-border-strong: #806055;
    --color-text: #f6e6de;
    --color-text-dim: #d3b4a6;
    --color-text-muted: #b8917f;
    --color-text-bright: #fff7f2;
    --color-link: #fbaa6b;
    --color-success: #4ade80;
    --color-warning: #fbbf24;
    --color-danger: #f87171;

    --board-bg: #241713;
    --board-grid: #33211b;
    --board-line: #88695f;
    --board-text: #f6e6de;
    --board-text-dim: #b8917f;
    --board-met: #4ade80;
    --board-over: #f87171;
}

/* Cool light, to paper's warm light. The two are the only light themes. */
body.theme-mist {
    --color-bg: #eef2f7;
    --color-surface: #ffffff;
    --color-border: #c5cdd8;
    --color-border-strong: #798ca6;
    --color-text: #1f2937;
    --color-text-dim: #475569;
    --color-text-muted: #5a6a7d;
    --color-text-bright: #0f172a;
    --color-link: #1d4ed8;
    --color-success: #166534;
    --color-warning: #92400e;
    --color-danger: #b91c1c;

    --board-bg: #eef2f7;
    --board-grid: #e2e8f0;
    --board-line: #73859e;
    --board-text: #1f2937;
    --board-text-dim: #58687b;
    --board-met: #166534;
    --board-over: #b91c1c;
}

/* Maximum legibility rather than a mood: pure black behind pure white, and
   every derived token pushed well past AA. Body text lands at 21:1, the
   ceiling of the scale. */
body.theme-contrast {
    --color-bg: #000000;
    --color-surface: #0d0d0d;
    --color-border: #363636;
    --color-border-strong: #5b5b5b;
    --color-text: #ffffff;
    --color-text-dim: #e6e6e6;
    --color-text-muted: #cccccc;
    --color-text-bright: #ffffff;
    --color-link: #7dd3fc;
    --color-success: #4ade80;
    --color-warning: #fbbf24;
    --color-danger: #f87171;

    --board-bg: #000000;
    --board-grid: #0d0d0d;
    --board-line: #606060;
    --board-text: #ffffff;
    --board-text-dim: #cccccc;
    --board-met: #4ade80;
    --board-over: #f87171;
}

/* Deep violet. Distinct from midnight, which reads blue-black. */
body.theme-plum {
    --color-bg: #1f1029;
    --color-surface: #2d1a3a;
    --color-border: #4e3d5b;
    --color-border-strong: #755a87;
    --color-text: #f0e4f7;
    --color-text-dim: #c9aede;
    --color-text-muted: #ab8cc4;
    --color-text-bright: #ffffff;
    --color-link: #d8b4fe;
    --color-success: #4ade80;
    --color-warning: #fbbf24;
    --color-danger: #f87171;

    --board-bg: #1f1029;
    --board-grid: #2d1a3a;
    --board-line: #7d638e;
    --board-text: #f0e4f7;
    --board-text-dim: #ab8cc4;
    --board-met: #4ade80;
    --board-over: #f87171;
}


body {
    background: var(--color-bg);
    color: var(--color-text);
}

/* -- Layout -- */
.flex { display: flex; }
.inline-block { display: inline-block; }
.flex-col { flex-direction: column; }
.flex-1 { flex: 1 1 0%; }
.items-center { align-items: center; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.gap-4 { gap: 1rem; }
.space-y-6 > * + * { margin-top: 1.5rem; }

/* -- Sizing -- */
.min-h-screen { min-height: 100vh; }
.max-w-lg { max-width: 32rem; }
.max-w-md { max-width: 28rem; }
.w-full { width: 100%; }

/* -- Spacing -- */
.mx-auto { margin-left: auto; margin-right: auto; }
.mt-8 { margin-top: 2rem; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.px-8 { padding-left: 2rem; padding-right: 2rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
.p-5 { padding: 1.25rem; }
.p-6 { padding: 1.5rem; }

/* -- Typography -- */
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
.text-5xl { font-size: 3rem; line-height: 1; }
.font-bold { font-weight: 700; }
.font-semibold { font-weight: 600; }
.text-center { text-align: center; }
.tracking-wider { letter-spacing: 0.05em; }
.tracking-widest { letter-spacing: 0.1em; }

/* -- Colors -- */
.text-white { color: var(--color-text-bright); }
.text-slate-300 { color: var(--color-text); }
.text-slate-400 { color: var(--color-text-dim); }
.text-slate-500 { color: var(--color-text-muted); }
.text-blue-400 { color: var(--color-link); }
.text-green-400 { color: var(--color-success); }
.bg-blue-600 { background-color: #2563eb; }
/* Ink for text sitting on a fixed accent fill (.bg-blue-600, which is a
   literal and does not follow the theme). It must be a literal too. The
   buttons used .text-white, which stopped meaning white the day the utility
   classes were repointed at tokens -- on the light themes
   --color-text-bright is near-black, so "Play Today's Puzzle" was rendering
   dark navy on blue at about 2.4:1. */
.text-on-accent { color: #ffffff; }
.bg-slate-800 { background-color: var(--color-surface); }
.bg-slate-900 { background-color: var(--color-bg); }

/* -- Borders -- */
.border { border-width: 1px; border-style: solid; }
.border-b { border-bottom-width: 1px; border-bottom-style: solid; }
.border-t { border-top-width: 1px; border-top-style: solid; }
.border-slate-700 { border-color: var(--color-border); }
.border-slate-600 { border-color: var(--color-border-strong); }
.rounded-xl { border-radius: 0.75rem; }
.rounded-lg { border-radius: 0.5rem; }

/* -- Effects & Transitions -- */
.transition { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.transform { /* enables transforms */ }

/* -- Hover States -- */
/* Hover darkens rather than lightens. Lightening to #3b82f6 dropped white
   text to 3.68:1 -- the rest state passed at 5.17:1 and only the hover failed,
   which is the kind of thing a static palette audit never sees because the
   color is not a token and the state is not in the DOM at rest. */
.hover\:bg-blue-700:hover { background-color: #1d4ed8; }
.hover\:text-white:hover { color: var(--color-text-bright); }
.hover\:scale-105:hover { transform: scale(1.05); }
.active\:scale-95:active { transform: scale(0.95); }

/* -- Cookie Banner -- */
/* Sticky, not fixed. Fixed took the notice out of the flow and parked it over
   the bottom of the viewport -- which is the footer's spot, so every footer
   link sat underneath it until the notice was dismissed. Sticky keeps it on
   screen while a long page scrolls, then lets it settle into its flow position
   directly above the footer once the end of the page is reached. */
.cookie-banner {
    position: sticky;
    bottom: 0;
    z-index: 50;
}
.hidden { display: none; }

/* -- Link underline -- */
.underline { text-decoration: underline; }
.hover\:underline:hover { text-decoration: underline; }

/* -- Additional spacing for legal pages -- */
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.mt-6 { margin-top: 1.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }

/* -- List styles for legal pages -- */
.list-disc { list-style-type: disc; }
.list-inside { list-style-position: inside; }
.ml-4 { margin-left: 1rem; }
.ml-2 { margin-left: 0.5rem; }

/* -- Additional layout utilities -- */
.block { display: block; }
.inline { display: inline; }
.space-y-2 > * + * { margin-top: 0.5rem; }
.space-y-4 > * + * { margin-top: 1rem; }
.max-w-sm { max-width: 24rem; }

/* -- Additional color utilities -- */
.text-red-200 { color: #fecaca; }
.text-amber-400 { color: var(--color-warning); }
.bg-red-900 { background-color: #7f1d1d; }
.border-red-700 { border-color: #b91c1c; }
.border-amber-700 { border-color: #b45309; }

/* -- Additional spacing -- */
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
.mb-0 { margin-bottom: 0; }
.mt-3 { margin-top: 0.75rem; }

/* ============================================
   Monetization Styles
   ============================================ */

/* -- Forms -- */
.form-input {
    width: 100%;
    padding: 0.75rem 1rem;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 0.5rem;
    color: var(--color-text);
    font-size: 0.875rem;
    line-height: 1.25rem;
}
/* Same focus treatment as every other control, and from the palette rather
   than a literal blue -- a hard-coded ring cannot follow ten themes. */
.form-input:focus {
    outline: 2px solid var(--color-link);
    outline-offset: 1px;
    border-color: var(--color-link);
}
.form-label {
    display: block;
    font-size: 0.875rem;
    color: var(--color-text-dim);
    margin-bottom: 0.25rem;
}
.form-error {
    color: var(--color-danger);
    font-size: 0.75rem;
    margin-top: 0.25rem;
}
.form-group {
    margin-bottom: 1rem;
}

/* -- Buttons -- */
.btn {
    display: block;
    padding: 0.75rem 1.5rem;
    border-radius: 0.5rem;
    font-weight: 700;
    font-size: 0.875rem;
    text-decoration: none;
    border: none;
    cursor: pointer;
    transition: background-color 150ms ease, transform 150ms ease;
}
.btn:hover { transform: scale(1.02); }
.btn:active { transform: scale(0.98); }
.btn-primary {
    background: #2563eb;
    color: #ffffff;
}
.btn-primary:hover { background: #1d4ed8; }   /* darker, not lighter: see .hover\:bg-blue-700 */
.btn-secondary {
    background: var(--color-border);
    color: var(--color-text);
}
.btn-secondary:hover { background: var(--color-border-strong); }
.btn-pro {
    background: linear-gradient(135deg, #f59e0b, #d97706);
    color: #000000;
}
.btn-pro:hover { background: linear-gradient(135deg, #fbbf24, #f59e0b); }

/* -- Pro Badge -- */
.pro-badge {
    display: inline-block;
    background: linear-gradient(135deg, #f59e0b, #d97706);
    color: #000;
    font-size: 0.625rem;
    font-weight: 700;
    padding: 0.125rem 0.375rem;
    border-radius: 0.25rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    vertical-align: middle;
    margin-left: 0.25rem;
}
.pro-badge-small {
    display: inline-block;
    background: linear-gradient(135deg, #f59e0b, #d97706);
    color: #000;
    font-size: 0.5rem;
    font-weight: 700;
    padding: 0.0625rem 0.25rem;
    border-radius: 0.1875rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    vertical-align: middle;
}

/* -- Stats Grid -- */
.stats-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 0.5rem;
}
.stat-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 0.75rem;
    padding: 1rem 0.5rem;
    text-align: center;
    display: flex;
    flex-direction: column;
}
.stat-value {
    font-size: 1.5rem;
    font-weight: 700;
    color: var(--color-text-bright);
}
.stat-label {
    font-size: 0.6875rem;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
@media (max-width: 480px) {
    .stats-grid { grid-template-columns: repeat(2, 1fr); }
}

/* -- Ad Slot -- */
.ad-slot {
    max-width: 728px;
    margin: 2rem auto 0;
    text-align: center;
    min-height: 90px;
}
.ad-placeholder {
    background: var(--color-surface);
    border: 1px dashed var(--color-border);
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 90px;
}
.ad-label {
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--color-text-muted);
    opacity: 0.5;
}
body.is-pro .ad-slot { display: none; }
@media (max-width: 768px) {
    .ad-slot { max-width: 320px; min-height: 50px; }
    .ad-placeholder { min-height: 50px; }
}

/* -- Pricing -- */
.pricing-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1rem;
}
.pricing-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 0.75rem;
    padding: 1.5rem;
    position: relative;
}
.pricing-card-featured {
    border-color: #d97706;
}
.pricing-save {
    position: absolute;
    top: -0.625rem;
    right: 1rem;
    background: linear-gradient(135deg, #f59e0b, #d97706);
    color: #000;
    font-size: 0.6875rem;
    font-weight: 700;
    padding: 0.125rem 0.5rem;
    border-radius: 0.25rem;
    text-transform: uppercase;
}
@media (max-width: 480px) {
    .pricing-grid { grid-template-columns: 1fr; }
}

/* -- Feature List -- */
.feature-list {
    list-style: none;
}
.feature-list li {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.5rem 0;
}
.feature-list li + li {
    border-top: 1px solid var(--color-border);
}
.feature-icon {
    width: 1.25rem;
    height: 1.25rem;
    color: var(--color-success);
    flex-shrink: 0;
    margin-top: 0.125rem;
}

/* -- Archive -- */
.archive-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 0.5rem;
    padding: 0.75rem 1rem;
    text-decoration: none;
    transition: border-color 150ms ease;
}
.archive-row:hover {
    border-color: var(--color-border-strong);
}
.solved-check {
    width: 1.25rem;
    height: 1.25rem;
    color: var(--color-success);
}

/* Separates the settings cards from the profile above them. A heading would
   claim the cards are a subsection of the profile; a rule just says the page
   changes subject here, which is what it does. */
.section-rule {
    border: 0;
    border-top: 1px solid var(--color-border);
    margin: 1.5rem 0;
}

/* -- Theme Picker -- */
.theme-option {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.5rem;
    border-radius: 0.5rem;
    cursor: pointer;
    transition: background 150ms ease;
}
.theme-option:hover { background: color-mix(in srgb, var(--color-text) 6%, transparent); }
.theme-option.locked { opacity: 0.5; cursor: not-allowed; }
.theme-option input[type="radio"] { accent-color: #3b82f6; }
.theme-swatch {
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 0.25rem;
    border: 1px solid var(--color-border-strong);
}
/* Swatch fills for the picker. Deliberately literal: a swatch must show
   the theme it offers, not the theme currently applied. Named swatch-*
   rather than theme-* so they cannot collide with the body theme class. */
.swatch-default { background: #0f172a; }
.swatch-dark { background: #111827; }
.swatch-midnight { background: #020617; }
.swatch-paper { background: #faf5e4; }
.swatch-forest { background: #14532d; }
.swatch-ocean { background: #082f38; }
.swatch-ember { background: #241713; }
.swatch-mist { background: #eef2f7; }
.swatch-contrast { background: #000000; }
.swatch-plum { background: #1f1029; }
.theme-label { color: var(--color-text); font-size: 0.875rem; }

/* -- Footer --
   One treatment for every footer link. They previously came in three: most
   used the dim token with a bright hover, the tip jar used the muted token
   with a hard-coded amber hover, and the version had no rule at all and simply
   inherited. The amber was also a real defect rather than only an
   inconsistency -- #f59e0b on the light themes measures about 2.1:1, well
   under AA, and being a literal it could not follow the palette. */
.footer-link {
    color: var(--color-text-dim);
    text-decoration: none;
}
.footer-link:hover,
.footer-link:focus-visible {
    color: var(--color-text-bright);
}
/* Not a link: dimmer than one, and deliberately so rather than by inheritance. */
.app-version { color: var(--color-text-muted); }

/* -- Pro Gate -- */
.pro-gate {
    max-width: 28rem;
    margin: 0 auto;
}

/* -- Pre-launch countdown --
   Uses the palette tokens rather than literals so the holding page follows
   the default theme like everything else. */
.countdown {
    display: flex;
    justify-content: center;
    gap: 0.5rem;
}
.countdown-unit {
    flex: 1 1 0;
    min-width: 0;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 0.5rem;
    padding: 0.75rem 0.25rem;
}
.countdown-value {
    display: block;
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.2;
    color: var(--color-text-bright);
    font-variant-numeric: tabular-nums;
}
.countdown-label {
    display: block;
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-text-muted);
}
.countdown-done {
    font-size: 1.25rem;
    font-weight: 600;
}

/* -- Back office --
   Staff-only surface under /manage/. Plain and dense on purpose: it is a
   working tool, not a marketing page. */
.manage-nav {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 1.5rem;
}
.manage-nav a {
    padding: 0.375rem 0.75rem;
    border-radius: 0.375rem;
    border: 1px solid var(--color-border);
    color: var(--color-text-dim);
    font-size: 0.875rem;
}
.manage-nav a:hover { color: var(--color-text-bright); }
.manage-nav a.is-current {
    background: var(--color-surface);
    color: var(--color-text-bright);
    border-color: var(--color-border-strong);
}
.manage-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.875rem;
}
.manage-table th,
.manage-table td {
    text-align: left;
    padding: 0.5rem 0.625rem;
    border-bottom: 1px solid var(--color-border);
    color: var(--color-text);
}
.manage-table th {
    color: var(--color-text-muted);
    font-weight: 600;
    text-transform: uppercase;
    font-size: 0.6875rem;
    letter-spacing: 0.05em;
}
/* Wide tables scroll inside their own box rather than the page. */
.manage-scroll { overflow-x: auto; }
.manage-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 0.5rem;
    padding: 1rem;
    margin-bottom: 1rem;
}
.manage-stat { font-size: 1.75rem; font-weight: 700; color: var(--color-text-bright); }
.manage-stat-label { font-size: 0.75rem; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
.badge {
    display: inline-block;
    padding: 0.125rem 0.5rem;
    border-radius: 999px;
    font-size: 0.6875rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
}
.badge-pending  { background: #78350f; color: #fde68a; }
.badge-sent     { background: #1e3a5f; color: #bfdbfe; }
.badge-accepted { background: #14532d; color: #bbf7d0; }
.badge-revoked  { background: #450a0a; color: #fecaca; }
.badge-ok       { background: #14532d; color: #bbf7d0; }
.badge-warn     { background: #78350f; color: #fde68a; }
.badge-bad      { background: #450a0a; color: #fecaca; }

/* -- Wordmark --
   Spelled in game tiles by make_brand. Fixed height, natural width: the SVG is
   41 tiles wide by 5 tall, so a width-driven rule would blow up the header. */
.site-logo { display: inline-block; line-height: 0; }
.logo-wordmark {
    height: 1.35rem;
    width: auto;
    display: block;
}
@media (min-width: 640px) {
    .logo-wordmark { height: 1.6rem; }
}
/* The tiles carry the piece palette, which is deliberately not themed -- the
   same decision the board makes. Nothing here overrides their fill. */
/* The holding page and the landing page both show the wordmark as their
   heading, so it gets more room there. max-width guards the narrowest phones:
   at 41 tiles wide the mark is about 295px, and object-fit lets it shrink
   rather than stretch if the viewport is narrower still. */
.logo-wordmark-lg {
    height: 2.25rem;
    max-width: 100%;
    object-fit: contain;
    margin: 0 auto;
}

/* -- Landing page: today's puzzle card --
   The card is the link, not a panel above one. Its own hover and focus states
   are what make that readable; without them it is a block of text that happens
   to navigate. */
.today-card {
    display: block;
    text-align: center;
}
.today-card:hover {
    border-color: var(--color-border-strong);
    transform: translateY(-2px);
}
.today-card:active {
    transform: translateY(0);
}
/* The global focus ring assumes an inline link and rounds at 2px. On a card
   that reads as a square outline drawn around a rounded panel. */
.today-card:focus-visible {
    border-radius: 0.75rem;
}
.today-card-cue {
    display: inline-block;
    margin-top: 0.75rem;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-link);
}
/* The arrow is decorative -- "Play now" already says it -- so it is drawn in
   CSS rather than added to the sentence a screen reader reads out. */
.today-card-cue::after {
    content: " \2192";
}

/* ============================================
   How to Play diagrams
   ============================================

   Static boards drawn in CSS grid rather than in Phaser. The tutorial page is
   the one page a reader arrives at *before* the canvas has taught them
   anything, so it must not depend on the canvas to make its point -- and a
   second Phaser instance for a set of pictures that never move would be a lot
   of machinery for a screenshot.

   The colors are the board's own tokens (--board-*), so a themed page themes
   its diagrams too -- sum states included, via --board-met and --board-over.
   The tutorial must not name its own green: a reader who learns one green
   here and meets another on the canvas has been taught the wrong lesson. */

.demo-figure {
    /* max-content keeps the callouts the same width as the board they annotate,
       so the arrows stay aligned with the targets at any text size. */
    width: max-content;
    max-width: 100%;
    margin: 0 auto;
}

/* Declared on the wrappers as well as the grids: the callouts around a board
   are siblings of it, not children, and they step past the target gutter using
   these same two values. */
.demo-figure,
.demo-tray,
.demo-rotate,
.demo-board,
.demo-strip,
.demo-piece {
    --demo-cell: 2.15rem;
    --demo-gap: 3px;
}

@media (max-width: 400px) {
    .demo-figure,
    .demo-tray,
    .demo-rotate,
    .demo-board,
    .demo-strip,
    .demo-piece { --demo-cell: 1.8rem; }
}

.demo-board {
    display: grid;
    /* Seven columns: the row-target gutter, then the six board columns. The
       first row is the column targets, matching drawTargets() in tilesum.js. */
    grid-template-columns: repeat(7, var(--demo-cell));
    gap: var(--demo-gap);
    padding: 0.6rem;
    border-radius: 0.75rem;
    background: var(--board-bg);
    border: 1px solid var(--color-border);
}

.demo-cell {
    display: flex;
    align-items: center;
    justify-content: center;
    height: var(--demo-cell);
    border-radius: 4px;
    background: var(--board-grid);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: calc(var(--demo-cell) * 0.44);
    font-weight: 700;
    color: var(--board-text);
    line-height: 1;
}

/* An empty cell is a hollow well; a filled one carries its piece color and the
   ink chosen for it server-side in game/tutorial.py. */
.demo-cell:not(.is-filled):not(.is-hole) {
    box-shadow: inset 0 0 0 1px var(--board-line);
}

.demo-cell.is-hole {
    background: none;
}

/* Piece outline. Only the sides that face something other than the same piece
   are drawn, so six cells read as one shape rather than six squares. */
.demo-cell.edge-t { box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.35); }
.demo-cell.edge-r { box-shadow: inset -2px 0 0 rgba(0, 0, 0, 0.35); }
.demo-cell.edge-b { box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.35); }
.demo-cell.edge-l { box-shadow: inset 2px 0 0 rgba(0, 0, 0, 0.35); }
.demo-cell.edge-t.edge-r { box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.35), inset -2px 0 0 rgba(0, 0, 0, 0.35); }
.demo-cell.edge-t.edge-b { box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.35), inset 0 -2px 0 rgba(0, 0, 0, 0.35); }
.demo-cell.edge-t.edge-l { box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.35), inset 2px 0 0 rgba(0, 0, 0, 0.35); }
.demo-cell.edge-r.edge-b { box-shadow: inset -2px 0 0 rgba(0, 0, 0, 0.35), inset 0 -2px 0 rgba(0, 0, 0, 0.35); }
.demo-cell.edge-r.edge-l { box-shadow: inset -2px 0 0 rgba(0, 0, 0, 0.35), inset 2px 0 0 rgba(0, 0, 0, 0.35); }
.demo-cell.edge-b.edge-l { box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.35), inset 2px 0 0 rgba(0, 0, 0, 0.35); }
.demo-cell.edge-t.edge-r.edge-b { box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.35), inset -2px 0 0 rgba(0, 0, 0, 0.35), inset 0 -2px 0 rgba(0, 0, 0, 0.35); }
.demo-cell.edge-t.edge-r.edge-l { box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.35), inset -2px 0 0 rgba(0, 0, 0, 0.35), inset 2px 0 0 rgba(0, 0, 0, 0.35); }
.demo-cell.edge-t.edge-b.edge-l { box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.35), inset 0 -2px 0 rgba(0, 0, 0, 0.35), inset 2px 0 0 rgba(0, 0, 0, 0.35); }
.demo-cell.edge-r.edge-b.edge-l { box-shadow: inset -2px 0 0 rgba(0, 0, 0, 0.35), inset 0 -2px 0 rgba(0, 0, 0, 0.35), inset 2px 0 0 rgba(0, 0, 0, 0.35); }
.demo-cell.edge-t.edge-r.edge-b.edge-l { box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.35); }

.demo-corner { height: var(--demo-cell); }

.demo-target {
    display: flex;
    align-items: center;
    justify-content: center;
    height: var(--demo-cell);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: calc(var(--demo-cell) * 0.40);
    font-weight: 700;
    line-height: 1;
    color: var(--board-text-dim);
}

/* The three readings tilesum.js gives a target. 'under' keeps the dim default:
   an unfinished line is the normal state, not a warning. */
.demo-target.is-met  { color: var(--board-met); }
.demo-target.is-over { color: var(--board-over); }

/* -- Callouts around a board -- */

.demo-note {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-dim);
}

/* Centered over the six board columns rather than over the whole board: the
   first column is the row-target gutter, so the padding steps past it. */
.demo-note--cols {
    justify-content: center;
    padding-left: calc(var(--demo-cell) + var(--demo-gap));
    padding-bottom: 0.15rem;
}

/* Sits under the row-target gutter, which is the leftmost column. The padding
   is the board's own 0.6rem plus whatever centers a 14px arrow in one cell, so
   the arrow stays under the targets as the cell size changes. */
.demo-note--rows {
    justify-content: flex-start;
    padding-left: calc(0.6rem + (var(--demo-cell) - 14px) / 2);
    padding-top: 0.15rem;
}

.demo-arrow { flex: none; }

/* -- The tray -- */

.demo-tray {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    gap: 0.85rem;
    padding: 0.85rem;
    border-radius: 0.75rem;
    background: var(--board-bg);
    border: 1px solid var(--color-border);
}

.demo-piece {
    display: grid;
    grid-template-columns: repeat(var(--piece-cols), var(--demo-cell));
    gap: 2px;
}

/* -- Rotation -- */

.demo-rotate {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.9rem;
    padding: 0.85rem;
    border-radius: 0.75rem;
    background: var(--board-bg);
    border: 1px solid var(--color-border);
}

.demo-rotate-arrow {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.2rem;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-dim);
}

/* -- Reading a sum -- */

.sum-demo {
    padding: 0.85rem;
    border-radius: 0.75rem;
    background: var(--board-bg);
    border: 1px solid var(--color-border);
    border-left-width: 3px;
    border-left-color: var(--board-text-dim);
}

.sum-demo + .sum-demo { margin-top: 0.75rem; }

.sum-demo.is-met  { border-left-color: var(--board-met); }
.sum-demo.is-over { border-left-color: var(--board-over); }

.sum-demo-title {
    margin: 0 0 0.6rem;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--board-text-dim);
}

.sum-demo.is-met .sum-demo-title  { color: var(--board-met); }
.sum-demo.is-over .sum-demo-title { color: var(--board-over); }

/* Cells, then the arrow, then the reading. The arrow inherits the block's
   status color from .sum-demo, which is the whole reason it is drawn inline. */
/* A grid rather than a centered flex row, so the three blocks line up column
   for column. Centering each one instead let the strips drift apart by the
   width of their own readout, which is the one thing a reader is comparing. */
.sum-demo-body {
    display: grid;
    grid-template-columns: auto auto auto;
    align-items: center;
    justify-content: start;
    gap: 0.5rem;
    color: var(--board-text-dim);
}

.sum-demo.is-met .sum-demo-body  { color: var(--board-met); }
.sum-demo.is-over .sum-demo-body { color: var(--board-over); }

.demo-strip {
    display: grid;
    grid-template-columns: repeat(6, var(--demo-cell));
    gap: var(--demo-gap);
    flex: none;
}

.sum-readout {
    display: grid;
    grid-template-columns: auto auto auto;
    align-items: baseline;
    justify-content: start;
    gap: 0 0.3rem;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    line-height: 1.1;
}

.sum-readout-total  { font-size: 1.15rem; font-weight: 700; color: inherit; }
.sum-readout-of     { font-size: 0.75rem; color: var(--board-text-dim); }
.sum-readout-target { font-size: 1.15rem; font-weight: 700; color: var(--board-text); }

.sum-readout-verdict {
    grid-column: 1 / -1;
    font-family: system-ui, -apple-system, sans-serif;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: inherit;
}

.sum-demo-note {
    margin: 0.6rem 0 0;
    font-size: 0.8125rem;
    line-height: 1.4;
    color: var(--color-text-dim);
}

/* The row of cells alone is wider than a phone once the arrow and the reading
   sit beside it, so below this width the figure stacks and the arrow turns to
   point down the page instead of across it. */
@media (max-width: 520px) {
    .sum-demo-body {
        grid-template-columns: auto;
        justify-content: center;
        justify-items: center;
        gap: 0.35rem;
    }
    .sum-demo-body .demo-arrow--right { transform: rotate(90deg); }
    .sum-readout { justify-content: center; text-align: center; }
    .sum-readout-verdict { text-align: center; }
}

/* -- Install affordance --
   The button is a footer link that happens to be a <button>, so it matches the
   row it sits in. Hidden until pwa.js knows an install is actually possible --
   offering "Install" to an already-installed app is noise. */
.install-button {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    cursor: pointer;
}
.install-help {
    max-width: 32rem;
    margin: 0 auto 1rem;
    padding: 0.85rem 1rem;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 0.75rem;
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--color-text-dim);
}
.install-help p { margin: 0.15rem 0; }
.install-help-title {
    font-weight: 700;
    color: var(--color-text);
    margin-bottom: 0.35rem !important;
}
.install-help strong { color: var(--color-text); }


/* -- Header on a narrow phone --
   "How to Play" wrapped onto three lines at 360px, which cost about 160px of
   height before the board had drawn a single cell. The game is sized from what
   is left over, so a wrapping header directly shrinks the board. Keep the row
   on one line and let it tighten instead. */
.site-header-nav a,
.site-header-nav button {
    white-space: nowrap;
}
@media (max-width: 420px) {
    .site-header { padding-left: 0.6rem; padding-right: 0.6rem; }
    .site-header-nav { gap: 0.6rem; font-size: 0.8125rem; }
    .logo-wordmark { height: 1.05rem; }
}
/* The wordmark is 41 tiles wide. Below this it and the four nav items cannot
   both fit, and shrinking the type far enough to make them costs more than the
   mark is worth here -- so the square T stands in for it. Same brand, an
   eighth of the width. */
.logo-mark { display: none; }
@media (max-width: 360px) {
    .site-header-nav { gap: 0.5rem; font-size: 0.75rem; }
    .logo-wordmark { display: none; }
    .logo-mark {
        display: block;
        height: 1.35rem;
        width: 1.35rem;
        border-radius: 0.3rem;
    }
}
