/* Reusable UI components. Uses theme tokens only. */

/* --- Header bar --- */
/* Visually hidden, still announced. Used by the puzzle-list heading, which is
 * redundant on screen - the Continue card names the mode - but still has to
 * give the screen an accessible name and keep the document outline intact. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.appbar {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 56px;
    /* All 12px of the vertical padding sits below the back row: it rides up
     * against the top bar and the content beneath gets the whole gap. The bar's
     * height comes from its 48px button plus this padding - min-height 56px is
     * not binding - so 0+12 still totals exactly 60px. */
    padding: 0 16px 12px;
    flex-shrink: 0;
}

/* Names where back goes, not where you are - so the bar reads as an exit
 * rather than repeating the name of the page you are already on. Grey and small
 * because it is a wayfinding aid, not a heading. */
/* Pulled toward the button so the chevron and the destination read as one
 * control. The 12px gap measured fine but sat on top of the ~12px of white the
 * 44px button keeps around its 20px glyph, so the pair looked twice as far
 * apart as the number suggested. Scoped to the pair, so other appbar children
 * keep the normal gap. */
.appbar h1,
.appbar h2 {
    font-size: 20px;
    font-weight: 800;
    letter-spacing: -0.01em;
    color: var(--text-primary);
}

.appbar .title-sub {
    font-size: 12px;
    color: var(--text-muted);
    font-weight: 600;
}

.appbar-spacer {
    flex: 1;
}

/* --- Icon button (shared chrome: white soft float) --- */
/* Full-width back row: the chevron and the destination read as one control
 * instead of a 44px square with a word floating beside it. The destination is
 * named rather than just "Back", because on this app's stack the same chevron
 * leads to five different places.
 *
 * The glyph sits in its own span - renderIcons() writes innerHTML into every
 * [data-icon] node, so putting it on the button would erase the label. */
.appbar .back-btn {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 48px;
    padding: 0 16px 0 10px;
    border: none;
    border-radius: var(--radius-btn);
    background: rgba(255, 255, 255, 0.92);
    box-shadow: 0 4px 12px rgba(120, 90, 60, 0.12);
    font-family: inherit;
    text-align: left;
    cursor: pointer;
    transition: transform 0.1s ease, background 0.15s ease;
}

.appbar .back-btn:active {
    transform: scale(0.99);
}

.back-btn-icon {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    color: var(--text-primary);
}

.back-btn-icon svg {
    width: 21px;
    height: 21px;
    stroke: currentColor;
    fill: none;
}

/* --text-soft, not primary: this names where you came from, it is not the
 * heading of where you are. */
.back-btn-label {
    min-width: 0;
    font-size: 14px;
    font-weight: 700;
    letter-spacing: -0.005em;
    color: var(--text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.icon-btn {
    width: 44px;
    height: 44px;
    border: none;
    background: rgba(255, 255, 255, 0.92);
    color: var(--text-primary);
    border-radius: 14px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    box-shadow: 0 4px 12px rgba(120, 90, 60, 0.12);
    transition: transform 0.1s ease, background 0.15s ease;
    flex-shrink: 0;
}

.icon-btn:hover {
    background: #fff;
}

.icon-btn:active {
    transform: scale(0.94);
}

.icon-btn:disabled {
    opacity: 0.55;
    cursor: default;
    pointer-events: none;
}

.icon-btn svg {
    width: 22px;
    height: 22px;
    stroke: currentColor;
    fill: none;
}

/* --bg-surface on a --bg-surface card is white on white, and this rule has now
 * been patched around FIVE separate times - Add word, Share, unlock Cancel,
 * guest welcome, and the auth modals - each time in a slightly different way.
 * Fixing the base instead: a neutral fill that is visible on every ground the
 * app has, and that reads as the quiet, dismissive action a secondary usually
 * is. Secondaries that AFFIRM something (Share, Add, Save) opt into the accent
 * fill on top of this. */
/* There is no focus-visible style anywhere in the player app, so every control
 * fell back to the browser's default blue ring - a colour that appears nowhere
 * in this palette. Low specificity on purpose: any component with its own focus
 * treatment still wins. Outline rather than box-shadow so it cannot disturb the
 * soft shadows the cards rely on. */
:focus-visible {
    outline: 2.5px solid var(--accent);
    outline-offset: 2px;
    border-radius: inherit;
}

.settings-review {
    margin-top: 16px;
}

.settings-legal {
    margin-top: 32px;
}

.legal-link {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: 16px 18px;
    background: var(--bg-surface);
    border: none;
    border-radius: var(--radius-card);
    color: var(--text-primary);
    font-size: 15px;
    font-weight: 500;
    text-align: left;
    cursor: pointer;
    box-shadow: var(--shadow-card);
}

.legal-link:active {
    opacity: 0.85;
}

.legal-link-arrow {
    color: var(--text-muted);
    font-size: 20px;
    line-height: 1;
}

.legal-doc {
    padding: 4px 2px 40px;
    color: var(--text-soft);
    font-size: 14px;
    line-height: 1.55;
}

.legal-doc h3 {
    margin: 24px 0 8px;
    color: var(--text-primary);
    font-size: 16px;
    font-weight: 600;
}

.legal-doc h3:first-of-type {
    margin-top: 12px;
}

.legal-doc p {
    margin: 0 0 12px;
}

.legal-updated {
    margin: 0 0 4px;
    color: var(--text-muted);
    font-size: 13px;
}

.settings-delete-account {
    margin-top: 24px;
    padding-top: 24px;
    border-top: 1px solid var(--line);
}

.settings-delete-note {
    margin: 0 0 14px;
    color: var(--text-muted);
    font-size: 13px;
    line-height: 1.45;
}

.settings-delete-btn {
    width: 100%;
}

/* --- Cards --- */
.card {
    background: var(--bg-surface);
    border-radius: var(--radius-card);
    padding: 18px;
    backdrop-filter: blur(var(--glass-blur));
    box-shadow: var(--shadow-card);
}

/* --- Mode selection cards --- */
.mode-grid {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.mode-card {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 20px;
    border-radius: var(--radius-card);
    background: var(--bg-surface);
    border: none;
    cursor: pointer;
    text-align: left;
    color: var(--text-primary);
    font-family: inherit;
    transition: transform 0.12s ease, background 0.15s ease;
    width: 100%;
    box-shadow: var(--shadow-card);
}

.mode-card:active {
    transform: scale(0.98);
}

.mode-card:hover {
    background: var(--bg-surface-strong);
}

.mode-card .mode-icon {
    width: 60px;
    height: 60px;
    border-radius: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--accent-soft);
    /* The shared wave band paints in currentColor, so the well needs a colour of
     * its own. The glyph sets stroke/fill explicitly, so this only feeds the band. */
    color: var(--accent);
    flex-shrink: 0;
}

.mode-card .mode-icon svg {
    width: 30px;
    height: 30px;
    stroke: var(--accent);
    fill: none;
}

.mode-card[data-mode="easy"] .mode-icon svg,
.mode-card[data-mode="medium"] .mode-icon svg,
.mode-card[data-mode="hard"] .mode-icon svg {
    stroke: none;
    fill: var(--accent);
}

.mode-card .mode-text {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.mode-card .mode-name {
    font-size: 22px;
    font-weight: 800;
    line-height: 1.1;
}

.mode-card .mode-desc {
    font-size: 15px;
    font-weight: 600;
    color: var(--text-muted);
}

.mode-card .mode-arrow {
    flex-shrink: 0;
    display: inline-flex;
}

.mode-card .mode-arrow svg {
    width: 22px;
    height: 22px;
    stroke: var(--text-dim);
    fill: none;
}

.mode-card.locked {
    opacity: 0.55;
    cursor: default;
}

.mode-card.locked:hover {
    background: var(--bg-surface);
}

.mode-card .mode-lock {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.mode-card .mode-lock svg {
    width: 18px;
    height: 18px;
    stroke: var(--text-muted);
    fill: none;
}

.mode-card .mode-lock.hidden {
    display: none;
}

.mode-card .mode-lock:not(.hidden) ~ .mode-arrow {
    display: none;
}

.home-my-puzzles-grid {
    margin-top: 14px;
    margin-bottom: 8px;
}

.custom-list-create {
    width: 100%;
    margin-bottom: 0;
}

.custom-list-headline {
    margin-top: 14px;
}

.custom-solved-headline {
    margin-top: 18px;
}

.custom-invite-desc {
    margin: 0 0 18px;
    font-size: 14px;
    font-weight: 600;
    line-height: 1.45;
    color: var(--text-muted);
}

/* Cancel moved to an X in the corner so Play now can take the full width -
 * it is the only thing anyone opens this dialog to do, and a 50/50 split gave
 * equal weight to the action nobody wants. Two classes, not one: the base
 * .puzzle-unlock-actions sets 1fr 1fr further down this file and would
 * otherwise win on source order.
 *
 * The old rule here was flex-wrap: wrap, which never did anything - the parent
 * has been a grid since it was written. */
.puzzle-unlock-actions.custom-invite-actions {
    grid-template-columns: 1fr;
}

/* Same corner-close as .brief-close and .alert-close, so the modals agree.
 * The unlock modal shares it: its Cancel button was dropped in favour of the
 * corner X, and both carry the same unlock-cancel action. */
.custom-invite-close,
.puzzle-unlock-close,
.apple-name-close {
    position: absolute;
    top: 10px;
    right: 10px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: none;
    color: var(--text-dim);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background 0.15s ease;
}

/* fill: none is not optional - the close sprite declares no fill, so without
 * it the glyph paints as a solid black square. */
.custom-invite-close svg,
.puzzle-unlock-close svg,
.apple-name-close svg {
    width: 15px;
    height: 15px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
}

/* 34px of paint is under the 44px minimum touch target (iOS HIG); grow the hit
 * box without moving the glyph. It overhangs the card corner, but the only
 * thing under there is the backdrop, which closes the dialog too. */
.custom-invite-close::after,
.puzzle-unlock-close::after,
.apple-name-close::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}

.custom-invite-close:active,
.puzzle-unlock-close:active,
.apple-name-close:active {
    background: rgba(40, 74, 81, 0.08);
}

.custom-share-guest-by {
    margin: 0;
    font-size: 15px;
    font-weight: 800;
}

.custom-share-guest-time {
    margin: 8px 0 0;
    font-size: 14px;
    font-weight: 600;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.my-puzzle-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.my-puzzle-tile {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 13px 16px;
    border-radius: var(--radius-card);
    background: var(--bg-surface);
    border: none;
    cursor: pointer;
    text-align: left;
    color: var(--text-primary);
    font-family: inherit;
    transition: transform 0.12s ease, background 0.15s ease;
    width: 100%;
}

.my-puzzle-tile:active {
    transform: scale(0.98);
}

.my-puzzle-tile:hover {
    background: var(--bg-surface-strong);
}

.my-puzzle-icon {
    color: var(--accent);
    width: 38px;
    height: 38px;
    border-radius: 11px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--accent-soft);
    flex-shrink: 0;
}

.my-puzzle-icon svg {
    position: relative;
    z-index: 1;
    width: 20px;
    height: 20px;
    stroke: var(--accent);
    fill: none;
}

.my-puzzle-text {
    flex: 1;
    min-width: 0;
}

.my-puzzle-title {
    display: block;
    font-size: 15px;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.my-puzzle-meta {
    display: block;
    font-size: 12px;
    color: var(--text-muted);
    margin-top: 2px;
}

.my-puzzle-tile .mode-arrow svg {
    width: 18px;
    height: 18px;
    stroke: var(--text-dim);
    fill: none;
}

/* --- Continue card (campaign overview + resume) --- */
/* Replaces .list-mode-progress, which was a bare percentage track and nothing
 * else: it restated what the grid below already shows and gave you nothing to
 * act on, which is why its markup was pulled. This card inverts that. The
 * ACTION leads - resume the puzzle you are part-way through, the one question
 * this screen genuinely cannot answer at a glance - and the overview rides
 * along as a quiet head line.
 *
 * Teal per §2.1: where you stand in the campaign is "the given". The play
 * control is orange, because that is the choice you make. Same wave hero
 * language as .daily-cta and .difficulty-cta. */
.list-continue {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    margin: 0 0 16px;
    padding: 15px 15px 15px;
    border-radius: var(--radius-card);
    background: var(--daily-hero-bg);
    box-shadow: var(--shadow-card);
    color: #fff;
}

.list-continue[hidden] {
    display: none;
}

/* Low and quiet. At 108px the wave crest cut straight through the action row,
 * which is the single biggest reason this card read as busy. Kept in the card's
 * own hue per §2.6 rather than switched to a white alpha. */
.list-continue .daily-hero-waves {
    height: 54px;
    opacity: 0.75;
}

.list-continue-inner {
    position: relative;
    z-index: 1;
}

/* The mode label lives here rather than in the appbar. The two were saying the
 * same thing; the card is the better place for it, so the screen heading is now
 * sr-only instead. */
/* One row: mode + arrows left, count right. Hard is the tight case - eight
 * arrows put it 4px over at 375px in English, and German is ~18px worse again
 * because "SCHWER" is half as wide again as "HARD". The eight-arrow set shrinks
 * to compensate (see --many below) rather than the row wrapping, so the layout
 * is identical across all three difficulties and both locales. */
.list-continue-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: nowrap;
    gap: 12px;
}

/* Mode label only. The direction arrows moved to the pre-game briefing, which
 * states the rules of the board you are about to play - repeating them on the
 * list card was the same fact in two places. */
.list-continue-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: nowrap;
    gap: 12px;
}

.list-continue-mode {
    font-size: 11px;
    font-weight: 800;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.70);
}

.list-continue-count {
    flex-shrink: 0;
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: rgba(255, 255, 255, 0.82);
    white-space: nowrap;
}

/* One pip per puzzle. A campaign is a countable set of levels, so the progress
 * element is countable too - it shows WHICH puzzles are done, not just a ratio,
 * which a percentage track cannot do. */
.list-continue-pips {
    display: flex;
    gap: 4px;
    margin: 10px 0 14px;
}

.list-continue-pip {
    flex: 1 1 0;
    min-width: 0;
    height: 6px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.24);
}

.list-continue-pip.is-solved {
    background: #fff;
}

/* Past ~24 puzzles the pips are too thin to count; degrade to one track. */
.list-continue-pips.is-continuous {
    display: block;
    height: 6px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.24);
    overflow: hidden;
}

.list-continue-pips.is-continuous .list-continue-pip {
    display: block;
    height: 100%;
    background: #fff;
}

/* No inner panel. A lighter rounded box inside an already-rounded card is a
 * card within a card - depth on depth, and the thing that made this look busy.
 * The play control is what says "tappable"; the row does not need a container
 * to prove it. */
.list-continue-action {
    display: flex;
    align-items: center;
    gap: 13px;
    width: 100%;
    padding: 0;
    border: none;
    background: none;
    color: #fff;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
    transition: opacity 0.15s ease, transform 0.12s ease;
}

.list-continue-action[hidden] {
    display: none;
}

.list-continue-action:hover {
    opacity: 0.88;
}

.list-continue-action:active {
    transform: scale(0.99);
}

/* The same category artwork as the tile below, so the card visibly points at a
 * specific row in the grid rather than being generic chrome. */
/* 4:3, matching the source artwork (480x364), so the illustration is shown
 * rather than centre-cropped. As a square it took the middle of the frame,
 * which on several categories is empty background - the Birds thumb was sky
 * with the bird cropped out, and read as an empty placeholder. */
.list-continue-thumb {
    flex: 0 0 auto;
    width: 64px;
    height: 48px;
    border-radius: 12px;
    overflow: hidden;
    /* Dark backing, not light: several category illustrations are very pale
     * (birds, colours) and vanished against a white wash on the teal. An inset
     * hairline gives them an edge without outlining them (§3). */
    background: rgba(0, 0, 0, 0.18);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
}

.list-continue-thumb img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* Same baked-in dark edge, same overscan as .puzzle-tile-bg - this shows the
     * identical artwork right above the tile, so a different zoom would read as
     * a mismatch. */
    transform: scale(1.01);
}

.list-continue-body {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* White, not --accent-light. On a hero card the SURFACE carries the hue and
 * everything on top is white - .daily-cta and .difficulty-cta both do this with
 * their arrows, and .difficulty-cta-icon is a white well with a card-hue glyph.
 * Orange here put a second hero hue inside a hero card, which §2.1 warns
 * against, and it measured worse: 4.66:1 against white's 9.17:1. Dimmed
 * slightly so the title below stays the brightest thing in the row. */
.list-continue-eyebrow {
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.78);
}

.list-continue-title {
    font-size: 15px;
    font-weight: 800;
    letter-spacing: -0.01em;
    color: #fff;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.list-continue-meta {
    font-size: 12px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: rgba(255, 255, 255, 0.72);
}

/* White disc, card-hue glyph - the same construction as .difficulty-cta-icon,
 * which is a white well holding an --accent glyph on the orange hero. On this
 * teal ground white measures 9.17:1 against the orange gradient's 3.53:1 (its
 * --accent-2 end is 2.68:1 and was dissolving into the card), so dropping the
 * orange makes the control both calmer AND more prominent. The page's colour
 * budget is already spent on the tile artwork. */
.list-continue-play {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    background: #fff;
    /* Tinted within the card's own hue rather than the warm card shadow, which
     * went muddy around white on teal (§2.6 reasoning). */
    box-shadow: 0 4px 12px rgba(20, 40, 48, 0.30);
    color: var(--stat-teal);
}

/* Icons.play is a filled path, so no fill:none here. */
.list-continue-play svg {
    width: 15px;
    height: 15px;
    margin-left: 2px;
}

/* Nothing left to resume: the action row goes and the head line carries the
 * completion message on its own. */
.list-continue.is-complete .list-continue-pips {
    margin-bottom: 0;
}

.list-continue.is-loading .list-continue-count,
.list-continue.is-loading .list-continue-title,
.list-continue.is-loading .list-continue-meta {
    color: rgba(255, 255, 255, 0.55);
}

.list-continue.is-loading .list-continue-action {
    pointer-events: none;
    cursor: default;
}

/* Hide-solved: a labelled switch, the same component the Settings rows use.
 * A plain row rather than a card - label and switch grouped at the right edge,
 * sitting directly on the page. width:fit-content with margin-left:auto does
 * the aligning, which also keeps the hit area on the control instead of
 * spanning the full width as invisible dead space. */
.list-hide-solved {
    display: flex;
    align-items: center;
    gap: 12px;
    width: fit-content;
    margin: 0 4px 14px auto;
    padding: 4px 0;
    border: none;
    background: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.list-hide-solved[hidden] {
    display: none;
}

.list-hide-solved-label {
    min-width: 0;
    color: var(--text-soft);
    font-size: 15px;
    font-weight: 700;
    line-height: 1.2;
}

/* --- Puzzle tiles (two-column overview — same language as Daily CTA) --- */
.puzzle-tiles,
.cascade-list-tiles {
    /* Same two-crest curve as the hero waves, shallow enough to read at 104px. */
    --tile-wave-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 150' preserveAspectRatio='none'%3E%3Cpath d='M0,0 H400 V128 C340,150 258,150 200,134 C142,118 58,118 0,140 Z' fill='%23000'/%3E%3C/svg%3E");
}

.puzzle-tiles {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px;
    padding-bottom: 8px;
}

.list-lazy-sentinel {
    grid-column: 1 / -1;
    height: 1px;
    margin: 0;
    padding: 0;
    pointer-events: none;
    opacity: 0;
}

/* Editorial card: a fixed art band on top, content on a consistent surface
 * below. The artwork is the ONLY thing that varies between tiles — chrome,
 * type and progress are identical everywhere, which is what makes a grid of
 * 15 different illustrations read as one system. */
.puzzle-tile {
    position: relative;
    isolation: isolate;
    display: flex;
    flex-direction: column;
    padding: 0;
    border: none;
    border-radius: 18px;
    background: var(--bg-surface);
    box-shadow: var(--shadow-card);
    cursor: pointer;
    text-align: left;
    font-family: inherit;
    color: var(--text-primary);
    overflow: hidden;
    transition: transform 0.12s ease, box-shadow 0.15s ease;
}

.puzzle-tile:hover {
    transform: translateY(-2px);
    box-shadow: 0 12px 28px rgba(120, 90, 60, 0.16);
}

.puzzle-tile:active {
    transform: scale(0.98);
}

/* --- Art band --- */
/* Masked with a wave, so the artwork swells into the card body instead of
 * ending on a hard rectangle edge. The tiles were the only surface in the app
 * with none of its signature language - no wave, no well, no seal - which is
 * what made a grid of them read as foreign. The motif was already here as
 * DAILY_WAVES_SVG, but only as the no-artwork fallback, so it never showed on a
 * real tile. Masking rather than overlaying a filled wave means no colour has
 * to be matched: the card's own background shows through the notch. */
.puzzle-tile .tile-hero {
    position: relative;
    flex: 0 0 auto;
    height: 104px;
    display: block;
    padding: 0;
    overflow: hidden;
    /* Teal hero gradient shows through for themes with no artwork. */
    background: var(--daily-hero-bg);
    -webkit-mask-image: var(--tile-wave-mask);
    mask-image: var(--tile-wave-mask);
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
}

/* Classic list only — cascade keeps the taller art band. */
.puzzle-tiles .puzzle-tile .tile-hero {
    height: 90px;
}

.puzzle-tile-bg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: bottom;
    /* The category WebPs ship with a ~1-2px near-black edge baked into all four
     * sides (sampled: birds/colours/food are all rgb(0,0,0) at the corner, and
     * bright by x=2). object-fit: cover fits these 480x364 sources to the band
     * by WIDTH, so the left and right source edges land exactly on the box
     * edges and the dark line shows. Overscan past it.
     *
     * 1.07 clips ~17 source px per side (480px source fitted by width, 3.5%
     * overflow each side). Remove this once the art is re-exported. */
    transform: scale(1.07);
    z-index: 0;
    pointer-events: none;
}

/* Artwork replaces the wave motif; waves are the no-art fallback. */
.puzzle-tile.has-category-bg .daily-hero-waves {
    display: none;
}

/* No point painting the dark fallback gradient behind an opaque image. */
.puzzle-tile.has-category-bg .tile-hero {
    background: var(--bg-surface);
}

.puzzle-tile .tile-hero .daily-hero-waves {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: 100%;
    z-index: 0;
    pointer-events: none;
}

.puzzle-tile .tile-hero .daily-hero-waves .wave-1 {
    fill: var(--daily-hero-wave);
}

.puzzle-tile .tile-hero .daily-hero-waves .wave-2 {
    fill: var(--daily-hero-wave-strong);
}

/* --- Chips floating on the art --- */
.puzzle-tile .tile-top {
    position: absolute;
    inset: 8px 8px auto 8px;
    z-index: 5;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 8px;
    pointer-events: none;
}

/* A numbered disc, not a "Puzzle 1" pill. Themes repeat across a mode - Easy
 * runs 17 puzzles over 12 themes - so the number is the only thing that makes a
 * tile unique, and it was the smallest, quietest element on the card. Mono per
 * §1.4, which reserves JetBrains Mono for the grid and numeric badges. The word
 * "Puzzle" lives on in the tile's aria-label, where it is actually needed. */
.puzzle-tile .tile-number {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.94);
    color: var(--text-primary);
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 13px;
    font-weight: 700;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    box-shadow: 0 2px 8px rgba(120, 90, 60, 0.22);
}

/* Time-medal badge: metal disc + light glyph, with the soft-tint halo
 * (and a matching coloured glow) so it still lifts off the category art. */
.puzzle-tile .tile-medal {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 3px;
    border-radius: var(--radius-chip, 999px);
}

.puzzle-tile .tile-medal--gold {
    background: var(--medal-gold-soft);
    color: var(--medal-gold-glyph);
}
.puzzle-tile .tile-medal--silver {
    background: var(--medal-silver-soft);
    color: var(--medal-silver-glyph);
}
.puzzle-tile .tile-medal--bronze {
    background: var(--medal-bronze-soft);
    color: var(--medal-bronze-glyph);
}

.puzzle-tile .tile-medal-disc {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    overflow: hidden;
    color: inherit;
}

/* Specular wash: lit top-left, darker base — same 155deg as the podium metals. */
.puzzle-tile .tile-medal-disc::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: linear-gradient(
        155deg,
        rgba(255, 255, 255, 0.22) 0%,
        rgba(255, 255, 255, 0.06) 42%,
        transparent 62%
    );
    pointer-events: none;
}

.puzzle-tile .tile-medal--gold .tile-medal-disc {
    background: linear-gradient(155deg, #F4D56A 0%, var(--medal-gold-fill) 52%, #E0A81C 100%);
    box-shadow:
        0 0 10px var(--medal-gold-fill),
        0 2px 6px rgba(120, 90, 60, 0.28),
        inset 0 1px 1px rgba(255, 255, 255, 0.28),
        inset 0 -1px 1px rgba(80, 50, 0, 0.10);
}
.puzzle-tile .tile-medal--silver .tile-medal-disc {
    background: linear-gradient(155deg, #B8C5D0 0%, #96A8B8 52%, #7E909E 100%);
    box-shadow:
        0 0 10px var(--medal-silver-fill),
        0 2px 6px rgba(80, 90, 100, 0.28),
        inset 0 1px 1px rgba(255, 255, 255, 0.26),
        inset 0 -1px 1px rgba(40, 50, 60, 0.10);
}
.puzzle-tile .tile-medal--bronze .tile-medal-disc {
    background: linear-gradient(155deg, #D4A078 0%, var(--medal-bronze-fill) 52%, #B57848 100%);
    box-shadow:
        0 0 10px var(--medal-bronze-fill),
        0 2px 6px rgba(120, 90, 60, 0.28),
        inset 0 1px 1px rgba(255, 255, 255, 0.24),
        inset 0 -1px 1px rgba(80, 40, 10, 0.10);
}

.puzzle-tile .tile-medal-disc svg {
    position: relative;
    z-index: 1;
    width: 13px;
    height: 13px;
}

.puzzle-tile .tile-status {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    flex-shrink: 0;
}

/* Same struck-disc language as .tile-medal: teal-soft halo, 3D disc, light glyph. */
.puzzle-tile .tile-solved-check {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--stat-teal-soft);
    flex-shrink: 0;
    box-sizing: border-box;
}

.puzzle-tile .tile-solved-check::before {
    content: '';
    position: absolute;
    inset: 3px;
    border-radius: 50%;
    background: linear-gradient(155deg, #4E8F9A 0%, var(--stat-teal) 52%, #2C616E 100%);
    box-shadow:
        0 0 10px color-mix(in srgb, var(--stat-teal) 65%, #5EA8B4),
        0 2px 6px rgba(40, 80, 90, 0.28),
        inset 0 1px 1px rgba(255, 255, 255, 0.26),
        inset 0 -1px 1px rgba(20, 40, 50, 0.12);
}

.puzzle-tile .tile-solved-check::after {
    content: '';
    position: absolute;
    inset: 3px;
    border-radius: 50%;
    background: linear-gradient(
        155deg,
        rgba(255, 255, 255, 0.22) 0%,
        rgba(255, 255, 255, 0.06) 42%,
        transparent 62%
    );
    pointer-events: none;
}

.puzzle-tile .tile-solved-check svg {
    position: relative;
    z-index: 1;
    width: 14px;
    height: 14px;
    stroke: #EAF7F6;
    fill: none;
}

/* --- Content --- */
.puzzle-tile .tile-body {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 3px;
    padding: 6px 13px 10px;
}

/* The theme is the interesting content, so it leads. The puzzle number moved
 * to a chip on the art. */
.puzzle-tile .tile-theme {
    font-size: 15px;
    font-weight: 800;
    line-height: 1.2;
    letter-spacing: -0.01em;
    color: var(--text-primary);
    text-transform: capitalize;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.puzzle-tile .tile-progress {
    display: flex;
    align-items: center;
    gap: 8px;
    /* Holds the row height steady across all three states: progress bar, word
     * count, and the unlock price (whose coin + ring is the tallest at 18px). */
    min-height: 18px;
}

/* Unstarted tiles show what you're getting into instead of an empty bar. */
.puzzle-tile .tile-words {
    font-size: 11px;
    font-weight: 700;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.puzzle-tile .tile-bar {
    flex: 1;
    height: 6px;
    border-radius: 999px;
    background: var(--fill-quiet);
    overflow: hidden;
}

/* One progress colour app-wide - was a different hue on every tile. Unifying
 * was right; the hue was not. "Words found" is a solved-count, so §2.2 makes it
 * teal. The gradient went too: at 6px tall it read as a single colour anyway. */
.puzzle-tile .tile-bar-fill {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: var(--stat-teal);
    transition: width 0.35s ease;
}

.puzzle-tile .tile-count {
    font-size: 11px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    flex-shrink: 0;
}

.puzzle-tile .tile-remaining {
    font-size: 11px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    white-space: nowrap;
}

/* Cascade in-progress: remaining sits under bar+count, not squeezed beside them. */
.puzzle-tile .tile-progress-stack {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 3px;
    min-width: 0;
}

.puzzle-tile .tile-progress-stack .tile-remaining {
    line-height: 1.2;
}

/* --- Locked --- */
/* Same wave seam as unlocked: `.tile-hero` keeps `--tile-wave-mask`, so the
 * card surface shows through the notch. Locked only changes colour, wash, and
 * the Unlock row — not the transition geometry. */
.puzzle-tile.locked {
    --tile-locked-body: #f8f6f8;
    --tile-locked-ink: #262342;
    --tile-locked-muted: #65607e;
    /* Must match `.tile-body` so the mask notch and content read as one surface. */
    background: var(--tile-locked-body);
    box-shadow:
        0 8px 20px rgba(70, 54, 90, 0.10),
        inset 0 0 0 1px rgba(255, 255, 255, 0.55);
}

.puzzle-tile.locked.has-category-bg .tile-hero {
    background: #5b5b5b;
}

.puzzle-tile.locked .puzzle-tile-bg {
    filter: grayscale(55%) saturate(45%) contrast(85%);
    opacity: 0.46;
    transform: scale(1.02);
}

.puzzle-tile.locked .daily-hero-waves {
    opacity: 0.55;
    filter: grayscale(40%);
}

/* Full-card wash on `.puzzle-tile.locked` (sibling of hero/body, not clipped
 * by the hero wave mask). Chrome + content sit above; body stays transparent
 * so the wash shows through the lower half. */
.tile-lock-overlay {
    position: absolute;
    inset: 0;
    z-index: 4;
    /* Warm taupe, not the old cool rgba(118, 104, 132) purple-grey: locked art
     * read lavender against a cream-and-orange palette. Same lightness and the
     * same 0.40 alpha, so the wash hides exactly as much as it did before -
     * only the hue moves, from ~270deg to the accent's ~28deg. */
    background: rgba(141, 116, 94, 0.40);
    pointer-events: none;
    border-radius: inherit;
}

.tile-lock-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 25px;
    height: 25px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.88);
    color: #716b88;
    box-shadow: 0 3px 8px rgba(52, 43, 65, 0.14);
    backdrop-filter: blur(8px);
}

.puzzle-tile.locked .tile-lock-badge .fa-lock-keyhole {
    font-size: 11px;
    line-height: 1;
    color: inherit;
}

.puzzle-tile.locked .tile-timepill,
.puzzle-tile.locked .tile-number {
    background: rgba(255, 255, 255, 0.90);
    box-shadow: 0 3px 8px rgba(52, 43, 65, 0.14);
    backdrop-filter: blur(8px);
}

.puzzle-tile.locked .tile-body {
    position: relative;
    z-index: 5;
    gap: 3px;
    padding: 6px 13px 10px;
    background: transparent;
}

.puzzle-tile.locked .tile-theme {
    color: var(--tile-locked-ink, #262342);
}

.puzzle-tile.locked .tile-progress {
    justify-content: center;
}

/* --- Unlock price (locked tiles) --- */
.puzzle-tile .tile-unlock {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    min-width: 0;
    max-width: 100%;
    padding: 5px 14px;
    border-radius: var(--radius-chip);
    background: #fff;
    color: var(--tile-locked-muted, #65607e);
    box-shadow:
        0 4px 10px rgba(65, 52, 82, 0.14),
        inset 0 0 0 1px rgba(100, 89, 120, 0.05);
}

.puzzle-tile .tile-unlock-label {
    font-size: 12px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: inherit;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.puzzle-tile .tile-unlock-coin {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--topbar-coin-bg);
    box-shadow: 0 0 0 2px var(--topbar-coin-ring);
    color: #fff;
    box-sizing: content-box;
    flex-shrink: 0;
}

.puzzle-tile .tile-unlock-coin svg {
    width: 100%;
    height: 100%;
}

.puzzle-tile .tile-unlock-cost {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 13px;
    font-weight: 700;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    color: inherit;
    flex-shrink: 0;
}

.puzzle-tile .tile-unlock.is-short {
    box-shadow:
        0 2px 6px rgba(65, 52, 82, 0.08),
        inset 0 0 0 1px rgba(100, 89, 120, 0.04);
    color: var(--text-soft);
}

.puzzle-tile .tile-unlock.is-short .tile-unlock-label,
.puzzle-tile .tile-unlock.is-short .tile-unlock-cost {
    color: inherit;
}

/* --- Home top bar (avatar + streak | settings) --- */
/* The bar starts at the true top of the screen, not at the safe-area line, and
 * absorbs the inset as padding instead. Anchoring it at top: var(--safe-top)
 * made its 92%-opaque cream wash begin with a hard horizontal edge exactly at
 * the bottom of the status bar / Dynamic Island - raw --app-bg above it, wash
 * below. The wash now runs behind the clock and camera and holds full strength
 * through the inset before fading, so there is no boundary to see. Content sits
 * in the same place as before: padding-top replaces the old offset 1:1. */
.global-topbar {
    position: absolute;
    top: 0;
    left: var(--safe-left);
    right: var(--safe-right);
    z-index: 26;
    display: none;
    padding: calc(10px + var(--safe-top)) 16px 8px;
    background: linear-gradient(to bottom,
        rgba(255, 243, 230, 0.92) 0,
        rgba(255, 243, 230, 0.92) calc(var(--safe-top) + 34px),
        rgba(255, 243, 230, 0) 100%);
}

.app-shell.topbar-visible .global-topbar {
    display: block;
}

/* Safety: never show top/bottom nav while loading screen is active. */
#screen-loading.active + .global-topbar {
    display: none !important;
}

#screen-loading.active ~ .bottom-nav {
    display: none !important;
}

.global-topbar .home-topbar {
    padding: 0;
}

.home-topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 4px 0 18px;
}

/* One gap value across the bar: .home-topbar already uses 12px between the
 * left group and settings, so the items inside match it. */
.home-topbar-left {
    display: flex;
    align-items: center;
    gap: 12px;
    min-width: 0;
}

.home-avatar {
    width: 44px;
    height: 44px;
    border: none;
    border-radius: 50%;
    padding: 0;
    cursor: pointer;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-family: inherit;
    font-size: 18px;
    font-weight: 800;
    color: #fff;
    background: var(--fill-dark);
    box-shadow: 0 4px 12px rgba(36, 63, 71, 0.22);
    transition: transform 0.1s ease;
    overflow: hidden;
    position: relative;
}

.home-avatar-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 50%;
    display: block;
}

.home-avatar-img[hidden] {
    display: none !important;
}

.home-avatar:has(.home-avatar-img:not([hidden])) #home-avatar-letter {
    display: none;
}

.home-avatar:active {
    transform: scale(0.96);
}

.home-level-wrap {
    position: relative;
    flex-shrink: 0;
    /* Room for the corner points badge so it doesn't hit the streak */
    margin-right: 14px;
    /* No padding-top. It used to reserve 6px for the XP badge's overhang, which
     * made this wrap 46px against the 40px pills - and with align-items: center
     * that pushed the level badge 3px below the streak and coins. The badge
     * overflows upward into .global-topbar's own top padding instead, which is
     * always at least 10px (and 10px + the safe-area inset on a notched phone). */
}

.home-level-badge {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-height: 40px;
    padding: 8px 12px;
    border: none;
    border-radius: var(--radius-chip);
    background: rgba(255, 255, 255, 0.92);
    box-shadow: 0 4px 12px rgba(120, 90, 60, 0.12);
    font-family: inherit;
    font-size: 14px;
    font-weight: 800;
    color: var(--text-primary);
    text-transform: uppercase;
    letter-spacing: 0.02em;
    cursor: pointer;
}

.home-level-label {
    white-space: nowrap;
    line-height: 1;
}

.home-level-points {
    position: absolute;
    top: -8px;
    right: -18px;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 20px;
    padding: 3px 8px;
    border-radius: 999px;
    background: var(--teal);
    box-shadow: 0 2px 6px rgba(51, 112, 127, 0.35);
    color: #fff;
    font-size: 10px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0;
    text-transform: none;
    line-height: 1;
    white-space: nowrap;
    pointer-events: none;
}

.home-streak {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    gap: 2px;
    min-height: 40px;
    padding: 8px;
    border-radius: var(--radius-chip);
    background: rgba(255, 255, 255, 0.92);
    box-shadow: 0 4px 12px rgba(120, 90, 60, 0.12);
}

.home-streak-icon {
    display: inline-flex;
    width: 20px;
    height: 20px;
    color: var(--topbar-flame);
}

.home-streak-icon svg {
    width: 20px;
    height: 20px;
    fill: currentColor;
    stroke: none;
}

.home-streak-value {
    font-size: 14px;
    font-weight: 800;
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

/* The coin's ring is a box-shadow, so it overflows the 20px box without taking
 * layout width - that is why this gap used to be 12px against the streak's 6px.
 * With a 2.5px ring (the value every other coin in the app uses) an 8px gap
 * leaves the same optical space beside both glyphs. */
.home-coins {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-height: 40px;
    padding: 8px 12px;
    border-radius: var(--radius-chip);
    background: rgba(255, 255, 255, 0.92);
    box-shadow: 0 4px 12px rgba(120, 90, 60, 0.12);
}

.home-coins-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--topbar-coin-bg);
    box-shadow: 0 0 0 2.5px var(--topbar-coin-ring);
    color: #fff;
    box-sizing: content-box;
}

.home-coins-icon svg {
    width: 100%;
    height: 100%;
}

.home-coins-value {
    font-size: 14px;
    font-weight: 800;
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

.home-settings-btn {
    width: 44px;
    height: 44px;
    border: none;
    border-radius: 14px;
    padding: 0;
    cursor: pointer;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.92);
    color: var(--topbar-icon);
    box-shadow: 0 4px 12px rgba(120, 90, 60, 0.12);
    transition: transform 0.1s ease, background 0.15s ease;
}

.home-settings-btn:active {
    transform: scale(0.96);
}

.home-settings-btn svg {
    width: 22px;
    height: 22px;
    stroke: var(--topbar-icon);
    color: var(--topbar-icon);
    fill: none;
}

/* --- Score pill --- */
.pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: var(--bg-surface);
    padding: 7px 13px;
    border-radius: var(--radius-chip);
    font-size: 14px;
    font-weight: 700;
}

.pill svg {
    width: 16px;
    height: 16px;
    stroke: var(--accent);
    fill: none;
}

/* --- Stat rows --- */
.stat-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 0;
    border-bottom: 1px solid var(--line);
}

.stat-row:last-child {
    border-bottom: none;
}

.stat-row .label {
    color: var(--text-muted);
    font-size: 14px;
}

.stat-row .value {
    font-size: 18px;
    font-weight: 700;
}

/* --- Statistics screen --- */

/* Hero. Total score drives both the level and the leaderboard, so it is the
 * identity stat and earns the hero treatment: the same construction as the
 * Daily CTA — saturated teal gradient, wave bands, orange eyebrow. */
.stats-hero {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    padding: 18px 18px 16px;
    margin-bottom: 12px;
    border-radius: var(--radius-card);
    background: var(--daily-hero-bg);
    box-shadow: var(--shadow-card);
    color: #fff;
}

.stats-hero .daily-hero-waves {
    height: 100%;
}

.stats-hero-top,
.stats-hero-level {
    position: relative;
    z-index: 1;
}

.stats-hero-top {
    display: flex;
    align-items: center;
    gap: 14px;
}

.stats-hero-eyebrow {
    display: block;
    font-size: 11px;
    font-weight: 800;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.68);
}

.stats-hero-value {
    display: block;
    margin-top: 5px;
    font-size: 36px;
    font-weight: 800;
    line-height: 1;
    letter-spacing: -0.025em;
    font-variant-numeric: tabular-nums;
}

.stats-hero-level {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 18px;
}

.stats-hero-badge {
    flex-shrink: 0;
    padding: 4px 10px;
    border-radius: var(--radius-chip);
    background: rgba(255, 255, 255, 0.20);
    font-size: 11px;
    font-weight: 800;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    white-space: nowrap;
}

.stats-hero-track {
    flex: 1;
    min-width: 0;
    height: 6px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.22);
    overflow: hidden;
}

.stats-hero-fill {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: #fff;
    transition: width 0.45s ease;
}

.stats-hero-points {
    flex-shrink: 0;
    font-size: 11px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: rgba(255, 255, 255, 0.88);
}

/* --- Summary tiles --- */
.stats-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin-bottom: 12px;
}

.stat-tile {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: auto auto;
    column-gap: 12px;
    align-items: center;
    padding: 14px;
    border-radius: 16px;
    background: var(--bg-surface);
    box-shadow: inset 0 0 0 1px rgba(40, 74, 81, 0.06);
}

/* Flat glyph in a soft tinted well — replaces the 3D raster art. */
.stat-tile-icon-wrap {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    border-radius: 12px;
    /* Spans both text rows so the icon centres against value + label. */
    grid-row: 1 / span 2;
    grid-column: 1;
}

.stat-tile-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
}

/* Stroke icons by default. `is-filled` is required for glyphs that paint with
 * fill (flame, starFilled, easy/medium/hard) — a blanket `fill: none` would
 * override their inline fill attribute and render them invisible. */
.stat-tile-icon svg,
.stat-mode-icon svg,
.stat-highlight-glyph svg {
    width: 100%;
    height: 100%;
    stroke: currentColor;
    fill: none;
}

.stat-tile-icon.is-filled svg,
.stat-mode-icon.is-filled svg,
.stat-highlight-glyph.is-filled svg {
    fill: currentColor;
    stroke: none;
}

/* Both classes on the wrap, so this is (0,3,0) and genuinely beats
 * .stat-tile--gold .stat-tile-icon-wrap (0,2,0) regardless of source order —
 * otherwise the gold well paints behind the coin disc. */
.stat-tile--gold .stat-tile-icon-wrap.stat-tile-icon-wrap--coin {
    background: none;
}

/* The coin is a filled disc everywhere in the app — topbar balance, quest
 * reward badge, tile unlock price — never a glyph in a tinted square. Mirrors
 * .home-coins-icon exactly, scaled for the tile. */
.stat-tile-coin {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: var(--topbar-coin-bg);
    box-shadow: 0 0 0 4px var(--topbar-coin-ring);
    color: #fff;
    box-sizing: content-box;
}

.stat-tile-coin svg {
    width: 100%;
    height: 100%;
}

.stat-tile-value {
    grid-column: 2;
    align-self: end;
    font-size: 26px;
    font-weight: 800;
    line-height: 1.05;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
}

.stat-tile-label {
    grid-column: 2;
    align-self: start;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-muted);
    line-height: 1.3;
}

/* Tone = meaning. Gold for currency, teal for accumulated progress, orange for
 * streak energy. Three hues, all from the system — no per-tile rainbow. */
.stat-tile--gold .stat-tile-value { color: var(--stat-gold); }
.stat-tile--gold .stat-tile-icon-wrap {
    background: var(--stat-gold-soft);
    color: var(--stat-gold);
}

.stat-tile--teal .stat-tile-value { color: var(--stat-teal); }
.stat-tile--teal .stat-tile-icon-wrap {
    background: var(--stat-teal-soft);
    color: var(--stat-teal);
}

.stat-tile--orange .stat-tile-value { color: var(--accent-2); }
.stat-tile--orange .stat-tile-icon-wrap {
    background: var(--accent-soft);
    color: var(--accent-2);
}

/* --- Icon wells: the wave motif, scaled down --- */
/* Every tinted icon well carries one soft wave band at its base, so the wells
 * echo the card surfaces instead of reading as generic rounded squares. The
 * band is a mask filled with currentColor, so it always picks up the well's own
 * hue — teal wells get a teal wave, gold wells a gold one, no extra tokens. */
.ach-hero-icon,
.stats-hero-icon,
.cascade-hub-icon,
.delete-account-icon,
.stat-tile-icon-wrap,
.stat-highlight-icon,
.stat-mode-icon,
.achievement-card-icon,
.achievement-overlay-icon,
.overlay-card .trophy,
.lb-row .rank,
.mode-card .mode-icon,
.my-puzzle-icon,
.alert-icon,
.puzzle-unlock-icon {
    position: relative;
}

.ach-hero-icon::before,
.stats-hero-icon::before,
.cascade-hub-icon::before,
.delete-account-icon::before,
.stat-tile-icon-wrap::before,
.stat-highlight-icon::before,
.stat-mode-icon::before,
.achievement-card-icon::before,
.achievement-overlay-icon::before,
.overlay-card .trophy::before,
.lb-row .rank::before,
.mode-card .mode-icon::before,
.my-puzzle-icon::before,
.alert-icon::before,
.puzzle-unlock-icon::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 0;
    border-radius: inherit;
    background: currentColor;
    opacity: 0.22;
    pointer-events: none;
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M0,60 C22,45 38,47 55,59 C72,71 86,69 100,57 L100,100 L0,100 Z' fill='%23000'/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M0,60 C22,45 38,47 55,59 C72,71 86,69 100,57 L100,100 L0,100 Z' fill='%23000'/%3E%3C/svg%3E");
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
}

/* The coin tile has no well, so it gets no wave. */
.stat-tile--gold .stat-tile-icon-wrap.stat-tile-icon-wrap--coin::before {
    content: none;
}

/* Glyphs sit above the band. */
.ach-hero-glyph,
.stats-hero-glyph,
.cascade-hub-glyph,
.delete-account-glyph,
.stat-tile-icon,
.stat-tile-coin,
.stat-highlight-glyph,
.stat-mode-icon svg,
.ach-glyph,
.level-up-overlay-level,
.quest-complete-overlay-glyph,
.achievement-overlay-icon > svg,
.overlay-card .trophy-glyph,
.rank-value,
.mode-card .mode-icon > svg,
.puzzle-unlock-glyph {
    position: relative;
    z-index: 1;
}

/* --- Best daily time --- */
.stats-highlight-card {
    padding: 12px 14px;
    margin-bottom: 4px;
}

.stat-highlight {
    display: flex;
    align-items: center;
    gap: 12px;
}

.stat-highlight-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    flex-shrink: 0;
    border-radius: 12px;
    background: var(--accent-soft);
    color: var(--accent-2);
}

/* Nested so the well is not a renderIcons target — otherwise rendering the
 * glyph wipes the corner seal out of it. */
.stat-highlight-glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
}

.stat-highlight-body {
    min-width: 0;
    flex: 1;
}

.stat-highlight-label {
    display: block;
    font-size: 11px;
    font-weight: 800;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Times read orange everywhere else in the app (podium, leaderboard rows). */
.stat-highlight-value {
    display: block;
    margin-top: 3px;
    font-size: 30px;
    font-weight: 800;
    line-height: 1;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    color: var(--accent-2);
}

.stat-highlight-play {
    appearance: none;
    flex-shrink: 0;
    align-self: center;
    width: auto;
    min-height: 0;
    margin: 0;
    padding: 8px 12px;
    border: none;
    border-radius: var(--radius-chip);
    background: var(--accent-soft);
    color: var(--accent-2);
    font-family: inherit;
    font-size: 12px;
    font-weight: 800;
    line-height: 1.2;
    white-space: nowrap;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.stat-highlight-play:active {
    transform: scale(0.98);
    filter: brightness(0.97);
}

/* --- By mode --- */
.stats-mode-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.stat-mode-row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 12px 14px;
    border: none;
    border-radius: 14px;
    background: var(--bg-surface);
    box-shadow: inset 0 0 0 1px rgba(40, 74, 81, 0.06);
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.stat-mode-row:active {
    transform: scale(0.99);
    filter: brightness(0.98);
}

.stat-mode-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    flex-shrink: 0;
    border-radius: 12px;
    background: var(--stat-teal-soft);
    color: var(--stat-teal);
}

.stat-mode-icon svg {
    width: 26px;
    height: 26px;
}

/* Difficulty ramp: one hue, increasing presence. A ramp carries meaning; a
 * different colour per row would just be decoration. */
.stat-mode-row[data-mode="medium"] .stat-mode-icon {
    background: rgba(81, 148, 162, 0.30);
}

.stat-mode-row[data-mode="hard"] .stat-mode-icon {
    background: rgba(81, 148, 162, 0.42);
}

.stat-mode-body {
    min-width: 0;
    flex: 1;
}

.stat-mode-name {
    display: block;
    font-size: 15px;
    font-weight: 800;
    letter-spacing: -0.01em;
}

.stat-mode-meta {
    display: block;
    margin-top: 2px;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.stat-mode-solved {
    flex-shrink: 0;
    padding: 5px 11px;
    border-radius: var(--radius-chip);
    background: var(--accent-soft);
    color: var(--accent-2);
    font-size: 12px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.stat-mode-row.is-empty .stat-mode-solved {
    background: var(--fill-quiet);
    color: var(--text-muted);
}

/* --- Social login buttons (Sign in with Apple / Facebook) --- */
.social-login-stack {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin-bottom: 8px;
}

.social-btn[hidden],
.social-linked-row[hidden] {
    display: none !important;
}

.social-linked-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
    min-height: 52px;
    padding: 0 18px;
    border-radius: var(--radius-btn, 12px);
    background: var(--surface-2, rgba(0, 0, 0, 0.04));
    color: var(--text-muted);
    font-size: 16px;
    font-weight: 700;
    line-height: 1.2;
}

.social-btn {
    appearance: none;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
    min-height: 52px;
    padding: 0 18px;
    border: none;
    /* Pill, like every other full-width action in the app (§1.3, §2.5). At
     * --radius-btn this sat directly above a pill Continue and read as a
     * different class of control. Also straightens the two Settings link
     * buttons, which use this same component. */
    border-radius: var(--radius-chip);
    font-family: inherit;
    font-size: 16px;
    font-weight: 700;
    line-height: 1.2;
    cursor: pointer;
    transition: transform 0.1s ease, filter 0.15s ease, opacity 0.15s ease;
}

.social-btn:active {
    transform: scale(0.98);
}

.social-btn:hover {
    filter: brightness(1.05);
}

.social-btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    flex-shrink: 0;
}

.social-btn-icon svg {
    width: 22px;
    height: 22px;
    stroke: none;
}

.social-btn--apple .social-btn-icon svg,
.social-btn--facebook .social-btn-icon svg {
    fill: currentColor;
}

.social-btn--apple {
    background: #000;
    color: #fff;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}

/* Google Identity light/outline button:
 * Fill #FFFFFF, stroke #747775 1px, type #1F1F1F Medium 14/20, 4-color G on white.
 * https://developers.google.com/identity/branding-guidelines */
.social-btn--google {
    background: #fff;
    color: #1f1f1f;
    border: 1px solid #747775;
    box-shadow: none;
    font-family: Roboto, "Google Sans", Arial, sans-serif;
    font-size: 14px;
    font-weight: 500;
    line-height: 20px;
    letter-spacing: 0.15px;
    gap: 12px;
}

.social-btn--google:hover,
.social-btn--google:active {
    filter: none;
    background: #f8faff;
}

.social-btn--google .social-btn-icon {
    width: 20px;
    height: 20px;
    background: #fff;
}

.social-btn--google .social-btn-icon svg {
    width: 20px;
    height: 20px;
    fill: none;
}

.social-btn--google .social-btn-icon svg path:nth-child(1) { fill: #EA4335; }
.social-btn--google .social-btn-icon svg path:nth-child(2) { fill: #4285F4; }
.social-btn--google .social-btn-icon svg path:nth-child(3) { fill: #FBBC05; }
.social-btn--google .social-btn-icon svg path:nth-child(4) { fill: #34A853; }

.social-btn--facebook {
    background: #1877f2;
    color: #fff;
    box-shadow: 0 2px 8px rgba(24, 119, 242, 0.24);
}

.social-btn.is-linked,
.social-btn:disabled {
    opacity: 0.72;
    cursor: default;
    filter: none;
}

.social-btn.is-linked:active,
.social-btn:disabled:active {
    transform: none;
}

.name-field {
    padding: 14px 0 0;
}

/* Field labels are chrome, not content. At 14px against a 16px/600 input they
 * sat only one step below the value and every field read as two lines of equal
 * weight - which is what made a five-field form feel heavy. Small, bold, tracked
 * and uppercase is the label idiom the rest of the app already uses
 * (.section-title, the CTA eyebrows, the nav labels): the eye skips it and lands
 * on the control. Scoped to .name-field, so Settings' toggle rows - which use
 * .stat-row .label beside a switch, where the label IS the content - are
 * untouched. */
.name-field .label {
    display: block;
    margin-bottom: 8px;
    color: var(--text-muted);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

/* The label owns the gap now, so the controls that carried their own top margin
 * no longer double it. */
.name-field .label + .custom-type-picker,
.name-field .label + .name-edit-row,
.name-field .label + .word-chips {
    margin-top: 0;
}

/* Empty, this reserved a visible gap under the WORDS label before a single word
 * was typed - the first thing anyone saw was a hole. */
.word-chips:empty {
    display: none;
}

/* The arrow was the browser's native select chevron - a different shape, weight
 * and colour on every platform, and on none of them the app's. This is Icons
 * .chevron rotated to point down: same 24 viewBox, same stroke-width 2, same
 * round caps, inked at --text-soft's 66%. Encoded rather than referenced
 * because a background-image cannot read the icon module.
 *
 * appearance:none is what lets it show at all; without it the platform arrow
 * sits on top. Keep the -webkit- prefix - iOS Safari still needs it. */
.name-field select.name-input {
    display: inline-block;
    flex: none;
    width: auto;
    max-width: 100%;
    margin-top: 10px;
    field-sizing: content;
    -webkit-appearance: none;
    appearance: none;
    padding-right: 42px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232E2A4A' stroke-opacity='0.66' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 14px center;
    background-size: 18px 18px;
}

/* The chevron takes the accent on focus, so the whole control responds as one
 * object rather than the ring lighting up around a grey arrow. */
.name-field select.name-input:focus {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23E3650F' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
}

.settings-locale-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 12px;
    padding-top: 14px;
}

.settings-locale-row .name-field {
    padding-top: 0;
    min-width: 0;
}

.settings-locale-row .name-field select.name-input {
    display: block;
    width: 100%;
    field-sizing: fixed;
}

.name-edit-row {
    display: flex;
    gap: 10px;
    margin-top: 10px;
    align-items: stretch;
}

.name-input {
    flex: 1 1 0%;
    min-width: 0;
    min-height: 44px;
    padding: 10px 14px;
    border-radius: var(--radius-btn, 12px);
    border: none;
    /* A field you type INTO should read as recessed. This was #fff on a 72%-white
     * card - lighter than its own container, which is backwards, and the reason
     * the inputs disappeared. Warm cream well, plus a hairline with enough
     * contrast to actually draw the edge. */
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 30%, transparent);
    background: var(--bg-deep);
    color: var(--text-primary);
    font: inherit;
    /* 15px. Note: iOS Safari auto-zooms a focused field below 16px. The viewport
     * meta carries maximum-scale=1 / user-scalable=no, which normally suppresses
     * it, but that has been version-dependent since iOS 10 - worth confirming on
     * a real device. */
    font-size: 15px;
    font-weight: 600;
}

/* .btn.secondary is --bg-surface, and so is the card behind it - the Add button
 * was white on white. Give it the soft accent fill, as on the solved overlay. */
#btn-add-word {
    background: var(--accent-soft);
    color: var(--accent-2);
}

/* Height, padding and auto width come from .btn-sm and the base now. */
.name-edit-row .btn {
    flex: 0 0 auto;
    white-space: nowrap;
}

/* On focus the field lifts to white - the active input becomes the brightest
 * thing on the card, which is the inverse of the resting state. */
.name-input:focus {
    outline: none;
    background: #fff;
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent),
                0 0 0 3px var(--accent-soft);
}

.password-field {
    position: relative;
    display: block;
    width: 100%;
}

.password-field .name-input {
    padding-right: 48px;
}

.password-toggle {
    position: absolute;
    top: 50%;
    right: 4px;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    margin: 0;
    padding: 0;
    border: none;
    border-radius: 10px;
    background: transparent;
    color: var(--text-soft, rgba(46, 42, 74, 0.62));
    cursor: pointer;
    transform: translateY(-50%);
}

.password-toggle:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px var(--accent-soft);
}

.password-toggle svg {
    width: 20px;
    height: 20px;
    stroke: currentColor;
    fill: none;
}

.name-save-btn {
    width: auto;
    min-height: 44px;
    min-width: 72px;
    padding: 10px 16px;
    font-size: 14px;
    flex-shrink: 0;
}

.field-error {
    color: var(--error);
    font-size: 14px;
    line-height: 1.5;
    margin: 0;
}

.field-error:not([hidden]) {
    padding: 14px 16px;
    border-radius: 14px;
    background: rgba(214, 48, 49, 0.07);
    border: 1px solid rgba(214, 48, 49, 0.18);
}

.custom-create-actions {
    display: flex;
    flex-direction: column;
    gap: 14px;
    margin-top: 22px;
}

.field-hint {
    color: var(--text-muted);
    font-size: 12px;
    margin: 8px 0 0;
    line-height: 1.4;
}

.custom-word-inline-hint {
    margin: 4px 0 -2px;
    padding: 0;
    font-size: 12px;
    line-height: 1.35;
    color: var(--text-muted);
}

.custom-word-inline-hint[data-state="warn"] {
    color: color-mix(in srgb, var(--warning) 70%, var(--text-muted));
}

.settings-push-hint {
    margin: 0 0 12px;
}

.settings-push-enable-btn {
    width: 100%;
}

.difficulty-picker {
    display: flex;
    gap: 8px;
    margin-top: 10px;
}

.difficulty-option {
    flex: 1;
    padding: 10px 0;
    border: 1px solid var(--line);
    border-radius: var(--radius-btn, 12px);
    background: var(--bg-surface);
    color: var(--text-primary);
    font: inherit;
    font-size: 14px;
    font-weight: 700;
    cursor: pointer;
}

.difficulty-option.active {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}

/* Was a 2-column grid: on a 375px screen each column is ~140px, so
 * "Chain words in order · choose a timer" wrapped to four lines. Full-width
 * rows give the descriptions one line each and match the .mode-card language
 * used elsewhere for a pick-one list. */
.custom-type-picker {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.custom-type-option {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    border: none;
    border-radius: 14px;
    background: var(--bg-surface);
    box-shadow: inset 0 0 0 1px rgba(40, 74, 81, 0.06);
    color: var(--text-soft);
    cursor: pointer;
    text-align: left;
    transition: background 0.15s, border-color 0.15s, color 0.15s, transform 0.1s, box-shadow 0.15s;
}

.custom-type-option:active {
    transform: scale(0.98);
}

/* A solid 2px accent ring was the loudest thing on the page. The filled icon
 * well carries the selection instead, with the ring dropped to a hairline. */
.custom-type-option.active {
    background: var(--accent-soft);
    color: var(--text-primary);
    box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--accent) 45%, transparent);
}

.custom-type-icon {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: 10px;
    background: color-mix(in srgb, var(--accent) 14%, var(--bg-surface));
    color: var(--accent);
}

.custom-type-option.active .custom-type-icon {
    background: var(--accent);
    color: #fff;
}

.custom-type-icon svg {
    width: 18px;
    height: 18px;
    stroke: currentColor;
    fill: none;
}

.custom-type-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

/* --text is not a token in this theme (the tiers are --text-primary / -soft /
 * -muted / -dim), so this silently inherited. Same phantom-variable class as the
 * --text-secondary bug in §11. Sizes moved from rem onto the px type scale. */
.custom-type-title {
    font-weight: 700;
    font-size: 15px;
    color: var(--text-primary);
}

.custom-type-desc {
    font-size: 13px;
    line-height: 1.35;
    color: var(--text-soft);
}

.direction-option {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 6px;
    border: none;
    border-radius: 13px;
    /* Off and on are the same object at two saturations, not two different
     * fills. Off is neutral and dimmed; on is the accent tint with a confident
     * arrow. A solid --accent fill was the most saturated thing on a page built
     * entirely from soft tints, and it read as harsh next to everything else. */
    background: rgba(40, 74, 81, 0.05);
    box-shadow: inset 0 0 0 1px rgba(40, 74, 81, 0.08);
    color: var(--text-dim);
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s, color 0.15s, transform 0.1s;
}

.direction-option:active {
    transform: scale(0.96);
}

.direction-option.active {
    background: var(--accent-soft);
    box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--accent) 40%, transparent);
    color: var(--accent-2);
}

.direction-option .direction-svg {
    width: 22px;
    height: 22px;
    display: block;
}

.direction-option[data-direction="horizontal"] .direction-svg { transform: rotate(0deg); }
.direction-option[data-direction="vertical"] .direction-svg { transform: rotate(90deg); }
.direction-option[data-direction="diagonal-down"] .direction-svg { transform: rotate(45deg); }
.direction-option[data-direction="diagonal-up"] .direction-svg { transform: rotate(-45deg); }
.direction-option[data-direction="horizontal-reverse"] .direction-svg { transform: rotate(180deg); }
.direction-option[data-direction="vertical-reverse"] .direction-svg { transform: rotate(270deg); }
.direction-option[data-direction="diagonal-down-reverse"] .direction-svg { transform: rotate(225deg); }
.direction-option[data-direction="diagonal-up-reverse"] .direction-svg { transform: rotate(135deg); }

.size-step-btn {
    flex: 0 0 40px;
    width: 40px;
    height: 40px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--bg-base, var(--bg-surface));
    color: var(--text-primary);
    font: inherit;
    font-size: 20px;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
}

.size-step-btn:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

.size-step-btn:not(:disabled):active {
    transform: scale(0.96);
}

.size-step-value {
    flex: 1;
    text-align: center;
    font-size: 22px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}

/* BOTH classes deliberately. .word-chips is defined in game.css for the play
 * screen - `max-height: 22vh; overflow-y: auto` so a long word list scrolls
 * inside the play header - and game.css loads AFTER this file. A single-class
 * override here is (0,1,0) against (0,1,0), so the later sheet won and this
 * list silently kept the play screen's scroll cap. The word list is the thing
 * you are building; it should never be a scroll box. */
.word-chips.custom-word-tags {
    justify-content: flex-start;
    max-height: none;
    /* overflow, not overflow-y. The base .word-chips sets overflow-x: hidden
     * for the play screen's scrolling list, and per spec a `visible` on one
     * axis computes to `auto` when the other is `hidden` - so the old
     * `overflow-y: visible` here was silently a no-op, and the box still
     * clipped. That cost the remove buttons the top and bottom of their tap
     * areas. max-height is none here, so there is nothing to scroll. */
    overflow: visible;
    padding: 0;
    margin: 10px 0 2px;
    min-height: 0;
    /* 14px, not 10px: the chips are 32px tall, so a 10px gap put wrapped rows
     * on a 42px pitch - and the remove buttons' 44px tap areas would have
     * overlapped the row above, where a stray tap deletes the wrong word. 14px
     * puts the pitch at 46px and buys the targets 2px of clearance. */
    gap: 14px;
}

/* .chip is defined in game.css - 11px on --bg-surface, built for the play
 * screen where it sits on a tinted backdrop. Inherited onto this white card it
 * was white-on-white and too small to read, which is why added words appeared
 * not to show at all. They are the CONTENT of this page, so they get real
 * presence, in the board's teal: the words you type and the board they go into
 * read as the same thing, while orange stays on the actions (Add, Create). */
.custom-word-tags .chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 6px 6px 6px 12px;
    font-size: 14px;
    font-weight: 700;
    letter-spacing: 0.01em;
    background: var(--stat-teal-soft);
    color: var(--stat-teal);
    cursor: default;
}

/* Was a bare glyph at 0.6 opacity - not obviously a control, and a small target.
 * A disc inside the chip reads as a button and gives it a real tap area. */
.custom-word-tags .chip .chip-remove {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    border: none;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.6);
    color: inherit;
    font-family: inherit;
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    padding: 0;
    opacity: 1;
    transition: background 0.15s ease, transform 0.1s ease;
}

/* fill: none is not optional - the close sprite declares no fill, so without
 * this it paints as a solid black square instead of two strokes. The sprite's
 * own stroke-width of 2 is set for a 24px box; at 12px it would thin to 1px
 * and look broken next to 700-weight chip text, hence the override. */
.custom-word-tags .chip .chip-remove svg {
    width: 12px;
    height: 12px;
    stroke: currentColor;
    stroke-width: 3;
    fill: none;
}

/* The disc is 20px because the chip is only 32px tall - it cannot itself reach
 * the 44px minimum touch target (iOS HIG) without swallowing the chip. This
 * grows the hit area past the paint without moving anything. Neighbouring
 * remove buttons sit ~60px apart across a row and 46px down a column, so the
 * expanded areas never reach each other. */
.custom-word-tags .chip .chip-remove::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}

.custom-word-tags .chip .chip-remove:active {
    transform: scale(0.9);
}

.custom-partial-choice {
    margin-top: 22px;
    padding: 18px 18px 16px;
    border-radius: 18px;
    background: linear-gradient(180deg, rgba(255, 193, 7, 0.14) 0%, rgba(255, 152, 0, 0.08) 100%);
    box-shadow: inset 0 0 0 1px rgba(255, 152, 0, 0.22);
}

.custom-partial-choice[hidden] {
    display: none !important;
}

.custom-partial-header {
    display: flex;
    align-items: flex-start;
    gap: 14px;
    margin-bottom: 12px;
}

.custom-partial-icon {
    flex: 0 0 36px;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    font-weight: 800;
    line-height: 1;
    color: #c05621;
    background: rgba(255, 255, 255, 0.72);
    box-shadow: inset 0 0 0 1px rgba(255, 152, 0, 0.25);
}

.custom-partial-header-text {
    flex: 1;
    min-width: 0;
}

.custom-partial-title {
    margin: 0 0 4px;
    font-size: 17px;
    font-weight: 800;
    line-height: 1.25;
    color: var(--text-primary);
}

.custom-partial-stats {
    margin: 0;
    font-size: 14px;
    font-weight: 700;
    line-height: 1.4;
    color: #c05621;
}

.custom-partial-hint {
    margin: 0 0 16px;
    padding: 0 0 0 50px;
    font-size: 14px;
    line-height: 1.55;
    color: var(--text-soft);
}

.custom-partial-sections {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin-bottom: 18px;
}

.custom-partial-block {
    margin: 0;
}

.custom-partial-label {
    display: block;
    margin-bottom: 8px;
    padding-left: 2px;
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}

.custom-partial-chip-panel {
    padding: 12px;
    border-radius: 14px;
    background: rgba(255, 255, 255, 0.62);
    box-shadow: inset 0 0 0 1px rgba(40, 74, 81, 0.06);
}

.custom-partial-chip-panel--muted {
    background: rgba(255, 255, 255, 0.38);
}

.custom-partial-chips {
    justify-content: flex-start;
    min-height: 0;
    padding: 0;
    gap: 8px;
}

.custom-partial-chips .chip {
    font-size: 13px;
    padding: 7px 11px;
}

.custom-partial-chips--muted .chip {
    opacity: 0.78;
    text-decoration: line-through;
}

.custom-partial-empty {
    font-size: 14px;
    color: var(--text-muted);
}

.custom-partial-actions {
    display: flex;
    flex-direction: column;
    gap: 14px;
    margin-top: 0;
    padding-top: 4px;
}

@media (max-width: 380px) {
    .custom-partial-hint {
        padding-left: 0;
    }
}

.custom-word-tags .chip .chip-remove:hover {
    background: #fff;
}

/* --- Toggle switch --- */
.switch {
    position: relative;
    width: 52px;
    height: 30px;
    flex-shrink: 0;
}

.switch input {
    opacity: 0;
    width: 0;
    height: 0;
}

.switch .track {
    position: absolute;
    inset: 0;
    /* Was --bg-surface-strong: 92% white, invisible on a white card and on
     * the cream page. --fill-quiet is the unfilled-track colour used by every
     * progress bar, so the off state now reads on any ground. */
    background: var(--fill-quiet);
    border-radius: 999px;
    transition: background 0.2s ease;
    cursor: pointer;
}

.switch .track::before {
    content: "";
    position: absolute;
    width: 24px;
    height: 24px;
    left: 3px;
    top: 3px;
    background: #fff;
    border-radius: 50%;
    transition: transform 0.2s ease;
}

.switch input:checked + .track {
    background: var(--accent);
}

.switch input:checked + .track::before {
    transform: translateX(22px);
}

.custom-share-actions {
    display: flex;
    gap: 10px;
    margin-top: 14px;
}

/* Only the share of the row — 48px/15px is the base size now. */
.custom-share-actions .btn {
    flex: 1;
}

.custom-share-delete-wrap {
    margin-top: 24px;
    padding-bottom: 8px;
}

/* Was an inline width/height on the span. */
.btn-glyph {
    display: inline-flex;
    width: 18px;
    height: 18px;
    flex-shrink: 0;
}

/* Icons.share is a stroke glyph whose circles carry no fill="none", so SVG's
 * default black filled them and the connecting strokes vanished - it read as
 * three loose dots. Same fault the play header's timer had. */
.btn-glyph svg {
    width: 100%;
    height: 100%;
    fill: none;
    stroke: currentColor;
}

.appbar-text {
    flex: 1;
    min-width: 0;
}

.appbar-sub {
    font-size: 13px;
    font-weight: 600;
    color: var(--text-muted);
    margin-top: 2px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* --- Leaderboard --- */
.leaderboard {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.lb-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 11px 14px;
    background: var(--bg-surface);
    border-radius: 12px;
}

.lb-row .rank {
    width: 26px;
    height: 26px;
    border-radius: 8px;
    background: var(--bg-surface-strong);
    /* Ranks 4+ keep the neutral chip. Naming the colour keeps their wave band
     * soft instead of inheriting the row's full-strength --text-primary - and
     * it has to live HERE, above the tier rules: at equal specificity a later
     * `.lb-row .rank { color }` would override every medal hue. */
    color: var(--text-soft);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 13px;
    font-weight: 700;
    flex-shrink: 0;
}

/* The podium three read from the medal family, so 1st place here and a gold
 * time medal on the solved overlay are the same metal. Soft fill + hue text
 * instead of the old solid #FDCB6E / #B2BEC3 / #E17055, which were hardcoded
 * and off-system (§11). The chip joins the shared icon-well wave group above,
 * whose band paints in currentColor - so the colour is all each tier sets. */
.lb-row .rank--1 {
    background: var(--medal-gold-soft);
    color: var(--medal-gold);
}

.lb-row .rank--2 {
    background: var(--medal-silver-soft);
    color: var(--medal-silver);
}

.lb-row .rank--3 {
    background: var(--medal-bronze-soft);
    color: var(--medal-bronze);
}

.lb-row .name {
    flex: 1;
    font-weight: 600;
    font-size: 14px;
}

.lb-name-hidden {
    font-style: italic;
}

.podium-name .lb-name-hidden {
    font-style: italic;
}

.lb-row .time {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    color: var(--accent-2);
}

/* Score is teal, time is orange - see the semantic hues in STYLE_GUIDE §2.2. */
.lb-row .score {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    color: var(--stat-teal);
    font-size: 14px;
}

.lb-row .rank.medal-rank {
    color: inherit;
}

.lb-row .rank.medal-rank svg {
    width: 16px;
    height: 16px;
    display: block;
}

.home-leaderboard-card {
    margin-top: 12px;
}

.lb-ellipsis {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    padding: 12px 0 14px;
    color: var(--text-muted, #888);
}

.lb-ellipsis-dot {
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: currentColor;
    opacity: 0.6;
}

.lb-row.is-me {
    background: var(--accent-soft);
    box-shadow:
        var(--shadow-card),
        inset 0 0 0 2px var(--accent-2);
}

.lb-row.is-me .name {
    font-weight: 800;
    color: var(--text);
}

.podium-slot--me .podium-player {
    padding: 8px 10px 6px;
    border-radius: var(--radius-card);
    background: var(--accent-soft);
    box-shadow: inset 0 0 0 2px var(--accent-2);
}

.podium-slot--me .podium-name {
    font-weight: 800;
    color: var(--text);
}

.daily-leaderboard-wrap {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.daily-podium {
    display: grid;
    grid-template-columns: 1fr 1.16fr 1fr;
    align-items: end;
    gap: 9px;
    /* The blocks have flat bottoms by design — give them a floor to stand on
     * instead of letting them collide with the leaderboard list below. */
    border-bottom: 1px solid var(--podium-floor);
    margin-bottom: 14px;
}

.podium-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    min-width: 0;
}

.podium-slot--empty {
    visibility: hidden;
}

.podium-player {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    width: 100%;
    margin-bottom: 8px;
    text-align: center;
}

.podium-block {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    width: 100%;
    border: none;
    /* Flat bottom: the block stands on the podium floor. */
    border-radius: 18px 18px 0 0;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
}

/* Top light: the face catches the light, the base stays seated. */
.podium-block::after {
    content: '';
    position: absolute;
    inset: 0 0 auto 0;
    height: 1px;
    z-index: 2;
    background: rgba(255, 255, 255, 0.45);
    pointer-events: none;
}

/* Signature wave bands, same motif as the Daily / Difficulty heroes. */
.podium-waves {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    /* Bottom band only. At 100% the crests cut through the numeral — these
     * blocks are a third the height of the hero cards the motif came from. */
    height: 44%;
    z-index: 0;
    pointer-events: none;
}

/* Struck medallion: ivory disc + white halo ring + soft lift. Same
 * construction as .home-coins-icon (flat fill plus ring) and the same
 * white-on-hero-gradient idea as .daily-hero-play. The disc is identical on
 * every tier — the medal is the constant, the pedestal is the variable. */
.podium-medal {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--podium-medal-face);
    /* Mono, per §1.4, which reserves JetBrains Mono for the grid and numeric
     * badges. A rank medallion is exactly that, and Nunito's soft round digits
     * were the reason these read as plain white dots rather than struck discs. */
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-weight: 700;
    line-height: 1;
    /* Zero tracking: trailing letter-space would knock a single digit
     * off-centre inside the disc. */
    letter-spacing: 0;
    font-variant-numeric: tabular-nums;
}

/* Player label above each block. */
.podium-name {
    font-size: 13px;
    font-weight: 700;
    line-height: 1.25;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Time becomes an orange badge - the same object as .podium-score in teal, so a
 * figure on a pill reads the same way on either podium. §2.2 keeps orange for
 * anything against the clock.
 *
 * Painted in --accent-2 by design call: it reads brighter against the medal
 * blocks than a darker orange does. Note white on --accent-2 is ~3.4:1, under
 * the 4.5:1 AA minimum, and 12px/800 is below the large-text threshold that
 * would relax it. Going darker is the fix if that ever needs to pass. */
.podium-time {
    align-self: center;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 20px;
    padding: 3px 9px;
    border-radius: var(--radius-chip);
    background: var(--accent-2);
    box-shadow: 0 2px 6px rgba(227, 101, 15, 0.35);
    color: var(--text-on-accent);
    font-size: 12px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    white-space: nowrap;
}

/* Score becomes a teal badge - the same object as .home-level-points in the top
 * bar, so a figure on a teal pill means "points" wherever you meet it.
 *
 * This was sharing the rule above, so podium scores rendered orange while ranks
 * 4-10 in the list directly beneath rendered teal: the podium contradicted its
 * own leaderboard. §2.2 assigns teal to score and anything solved, so the list
 * was right. It also lifts contrast from ~3.1:1 (12px --accent-2 on cream) to
 * 5.58:1 white-on-teal. */
.podium-score {
    align-self: center;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 20px;
    padding: 3px 9px;
    border-radius: var(--radius-chip);
    background: var(--teal);
    box-shadow: 0 2px 6px rgba(51, 112, 127, 0.35);
    color: var(--text-on-accent);
    font-size: 12px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    white-space: nowrap;
}

/* --- Winner: the reward gold --- */
.podium-block--gold {
    background: var(--podium-gold-bg);
    box-shadow: var(--podium-gold-shadow);
}

.podium-block--gold .wave-1 { fill: var(--podium-gold-wave); }
.podium-block--gold .wave-2 { fill: var(--podium-gold-wave-strong); }

/* The halo is white on every tier, deliberately: the medallion is the constant
 * and the pedestal is the variable. A rim in each tier's own metal was tried
 * and read as heavier and more literal than this design wants. */
.podium-medal--gold {
    width: 40px;
    height: 40px;
    font-size: 20px;
    color: var(--podium-gold-rank);
    box-shadow: 0 0 0 4px var(--podium-medal-ring), var(--podium-medal-lift);
}

/* --- Silver: 2nd place --- */
.podium-block--silver {
    background: var(--podium-silver-bg);
    box-shadow: var(--podium-silver-shadow);
}

.podium-block--silver .wave-1 { fill: var(--podium-silver-wave); }
.podium-block--silver .wave-2 { fill: var(--podium-silver-wave-strong); }

.podium-medal--silver {
    width: 31px;
    height: 31px;
    font-size: 16px;
    color: var(--podium-silver-rank);
    box-shadow: 0 0 0 3px var(--podium-medal-ring), var(--podium-medal-lift);
}

/* --- Bronze: 3rd place --- */
.podium-block--bronze {
    background: var(--podium-bronze-bg);
    box-shadow: var(--podium-bronze-shadow);
}

.podium-block--bronze .wave-1 { fill: var(--podium-bronze-wave); }
.podium-block--bronze .wave-2 { fill: var(--podium-bronze-wave-strong); }

.podium-medal--bronze {
    width: 27px;
    height: 27px;
    font-size: 14px;
    color: var(--podium-bronze-rank);
    box-shadow: 0 0 0 2.5px var(--podium-medal-ring), var(--podium-medal-lift);
}

.podium-slot--1 .podium-block { height: 96px; }
.podium-slot--2 .podium-block { height: 68px; }
.podium-slot--3 .podium-block { height: 54px; }

/* The winner's name carries a little more weight than the field's. */
.podium-slot--1 .podium-name {
    font-size: 14px;
    font-weight: 800;
}

.podium-block--ghost {
    background: var(--podium-silver-bg);
    opacity: 0.18;
}

.daily-leaderboard-rest:not(:empty) {
    margin-top: 2px;
}

/* "Your rank" is the same object as .cascade-hub-rank-badge - a compact pill
 * saying where you landed - so it is built from the same parts: chip radius,
 * a --accent-2-tinted surface, soft label + mono value inline.
 *
 * It used to be a full-width --accent-soft block carrying --shadow-card, a 22%
 * accent ring AND a 36px mono numeral, which made the line introducing the
 * podium louder than the podium itself. Same hue and same meaning, a third of
 * the weight: the medal blocks stay the loudest thing on the screen. */
.all-time-your-rank {
    align-self: center;
    display: inline-flex;
    /* center, not baseline. Neither string has a descender, so on a shared
     * baseline the two ink blocks share a bottom edge - and the 8px-tall label
     * then hangs off the 12.8px numeral, its centre sitting 2.13px below the
     * pill's. Centring puts both ink centres on the pill's midline (label
     * -0.37px, value -0.45px). The lost baseline costs nothing here: there is
     * no third element for the phrase to line up with. */
    align-items: center;
    gap: 8px;
    padding: 7px 16px;
    margin-bottom: 4px;
    border-radius: var(--radius-chip);
    /* Solid accent rather than the --accent-soft wash: this pill means *you*,
     * and at full strength it reads as a badge rather than a tint.
     *
     * --accent-strong, not --accent-2: white on accent-2 is 3.42:1, and neither
     * half clears AA there - the 11px label needs 4.5:1 and the 17px/800 figure
     * is still under the 18.66px large-text threshold. accent-strong is 5.68:1
     * and carries both.
     *
     * Colour set once here so the label and the figure inherit one value and
     * read as a single phrase. */
    background: var(--accent-strong);
    color: var(--text-on-accent);
}

/* line-height 1 on BOTH spans, not just the value. At 1.2 the label's extra
 * half-leading padded its line box, which shifted the text within the pill.
 * Equal line-heights keep each span's box symmetric about its own ink, which is
 * what lets align-items: center land on the glyphs rather than on leading. */
.all-time-your-rank-label {
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0.01em;
}

.all-time-your-rank-value {
    font-family: var(--font-grid);
    font-size: 17px;
    font-weight: 800;
    line-height: 1;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
}

/* .btn sets display: inline-flex, which outranks the UA stylesheet's
 * [hidden] { display: none } - so a hidden .btn still painted. Same explicit
 * opt-in the admin sheet uses for its own [hidden] elements. */
.puzzle-unlock-card .btn[hidden] {
    display: none;
}

/* Offered when the balance is short: Unlock is unusable, so it is REPLACED by
 * the one puzzle the player can actually open rather than sitting there greyed
 * out beside it.
 *
 * Stacked full width rather than dropped into Unlock's grid cell: that cell is
 * 122px, and .btn's 22px side padding leaves 78px for the label. "Play a free
 * puzzle" measures 131px and "Kostenloses Rätsel spielen" 199px, so a half
 * cell cannot hold either without wrapping or gutting the copy. */
.puzzle-unlock-free {
    width: 100%;
    margin-top: 10px;
}

.puzzle-unlock-actions.is-single {
    grid-template-columns: 1fr;
}

/* --- Countdown --- */
.countdown {
    text-align: center;
    padding: 8px 0 4px;
}

.countdown .label {
    font-size: 12px;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

.countdown .time {
    font-family: var(--font-grid);
    font-size: 30px;
    font-weight: 700;
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
    margin-top: 4px;
}

/* --- Daily hero card --- */
.daily-hero {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    border-radius: var(--radius-card);
    background: var(--daily-hero-bg);
    box-shadow: var(--shadow-card);
    padding: 26px 24px 22px;
    color: #fff;
}

.daily-hero-waves {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: 150px;
    z-index: 0;
    pointer-events: none;
}

.daily-hero-waves .wave-1 {
    fill: var(--daily-hero-wave);
}

.daily-hero-waves .wave-2 {
    fill: var(--daily-hero-wave-strong);
}

.daily-hero-content {
    position: relative;
    z-index: 1;
}

/* White at the same opacity as .stats-hero-eyebrow and .ach-hero-eyebrow. The
 * orange was the one hue on a teal card that did not belong to it: on these
 * grounds the app speaks in white at varying opacity. */
/* Room reserved for the corner share button, so a longer localisation of this
 * label wraps or ellipsises rather than sliding under it. */
.daily-hero-eyebrow {
    max-width: calc(100% - 120px);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 13px;
    font-weight: 800;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.68);
}

.daily-hero-title {
    margin: 10px 0 0;
    font-size: 32px;
    line-height: 1.04;
    font-weight: 800;
    letter-spacing: -0.015em;
}

.daily-hero-sub {
    margin: 12px 0 0;
    font-size: 17px;
    font-weight: 600;
    line-height: 1.35;
    color: rgba(255, 255, 255, 0.92);
}

.daily-hero-sub:empty {
    display: none;
}

.daily-hero-play {
    margin-top: 24px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    gap: 12px;
    padding: 15px 32px 15px 26px;
    border: none;
    border-radius: var(--radius-chip);
    background: #fff;
    color: #284A51;
    font-family: inherit;
    font-size: 19px;
    font-weight: 800;
    cursor: pointer;
    box-shadow: 0 10px 24px rgba(20, 40, 48, 0.28);
    transition: transform 0.1s ease, box-shadow 0.15s ease;
}

.daily-hero-play:hover {
    box-shadow: 0 12px 30px rgba(20, 40, 48, 0.34);
}

.daily-hero-play:active {
    transform: scale(0.97);
}

/* Two badges, no container: the panel existed to hold a sentence that said what
 * the badges now say. A wrapper around two chips is a box for its own sake. */
/* Once solved, the play button is hidden and this is the only thing left to do
 * on the card - so it reads as a smaller sibling of .daily-hero-play: same
 * white pill, same deep-teal ink, just quieter. White on the hero teal measures
 * 9.17:1, which is why controls on this ground are white rather than accent. */
/* Top-right corner of the hero. .daily-hero-content is already position:
 * relative and sits inside the card's padding, so 0/0 lands exactly on the
 * content box's corner - level with the eyebrow rather than the card edge.
 * Taken out of flow deliberately: the badge row and countdown close back up. */
/* Placement only. Colour, size, radius, weight and press feedback come from
 * .btn .btn-white .btn-sm.
 *
 * This used to be a hand-rolled white pill, and it had to be: --accent-soft
 * over the hero measures 2.32:1 and no orange-family treatment clears AA there.
 * .btn-white is that same answer promoted to a system level — and at 44px it
 * also stops being a 36px touch target, which was under the minimum.
 *
 * .hidden carries !important in base.css, so it still beats .btn's display. */
.daily-hero-share {
    position: absolute;
    top: -2px;
    right: 0;
    z-index: 2;
    margin-top: 0;
    /* The lift is the one thing the system cannot know about: it separates a
     * light pill from the dark card it sits on. */
    box-shadow: 0 6px 16px rgba(20, 40, 48, 0.22);
}

.daily-hero-share-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
}

.daily-hero-share-icon svg {
    width: 100%;
    height: 100%;
    stroke: currentColor;
    fill: none;
}

.daily-hero-solved {
    margin-top: 24px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
}

.daily-hero-solved-badge {
    margin: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 6px 14px;
    border-radius: var(--radius-chip);
    background: rgba(255, 255, 255, 0.92);
    color: #284A51;
    font-size: 13px;
    font-weight: 800;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

/* Same shape and weight as the solved badge beside it - the pair reads as one
 * component in two states. Off the podium it stays neutral white; a tier fill
 * only appears when there is a tier to report. */
.daily-hero-solved-rank {
    margin: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    padding: 6px 14px;
    border-radius: var(--radius-chip);
    background: rgba(255, 255, 255, 0.18);
    font-size: 13px;
    font-weight: 800;
    color: #fff;
}

.daily-hero-solved-rank .daily-solved-medal {
    display: inline-flex;
    width: 15px;
    height: 15px;
}

/* Icons.medal is a filled glyph (it is in FILLED_STAT_ICONS). */
.daily-hero-solved-rank .daily-solved-medal svg {
    width: 15px;
    height: 15px;
    fill: currentColor;
    stroke: none;
}

/* Tier keeps its signal through the medal tokens rather than three one-off hex
 * values. The -soft variants are translucent, so they tint on the teal instead
 * of fighting it, and the label stays white for contrast. */
/* Each tier paired its metal fill with its own darker metal as the label, which
 * measured 2.44:1 (gold), 2.15:1 (silver) and 1.73:1 (bronze) - all far under
 * the 4.5:1 needed at 13px, bronze being nearly illegible. Metal-on-metal has
 * nowhere to go: the two values are too close by construction.
 *
 * The app's own ink solves all three at once - 8.25 / 5.56 / 4.71 - and keeps
 * the medal legible as a medal, because the tier still comes from the fill.
 * White was checked first and is worse: 1.65:1 on gold. */
.daily-hero-solved--top1 .daily-hero-solved-rank,
.daily-hero-solved--top2 .daily-hero-solved-rank,
.daily-hero-solved--top3 .daily-hero-solved-rank {
    color: var(--text-primary);
}

.daily-hero-solved--top1 .daily-hero-solved-rank {
    background: var(--medal-gold-fill);
}

.daily-hero-solved--top2 .daily-hero-solved-rank {
    background: var(--medal-silver-fill);
}

.daily-hero-solved--top3 .daily-hero-solved-rank {
    background: var(--medal-bronze-fill);
}

.daily-hero-play .play-tri {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
}

.daily-hero-play .play-tri svg {
    width: 22px;
    height: 22px;
}

.daily-hero-meta {
    margin-top: 22px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    font-size: 14px;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.9);
}

.daily-hero-meta .dot {
    opacity: 0.55;
}

.daily-screen--past .daily-hero-play,
.daily-screen--past .daily-hero-meta,
.daily-screen--past .daily-quests-section {
    display: none;
}

/* --- Daily CTA (home screen, matches the Today's Puzzle hero) --- */
.home-welcome {
    margin-bottom: 14px;
}

.home-welcome-greeting {
    margin: 0;
    font-size: 22px;
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.25;
    color: var(--text);
}

.home-welcome-guest {
    font-size: 0.68em;
    font-weight: 600;
    color: var(--text-muted);
    letter-spacing: 0;
}

.home-guest-auth-wrap {
    margin-top: 12px;
}

.home-guest-auth-hint {
    margin: 0 0 10px;
    font-size: 13px;
    font-weight: 600;
    line-height: 1.4;
    color: var(--text-muted);
}

.home-guest-auth-actions {
    display: flex;
    align-items: stretch;
    gap: 10px;
}

.home-guest-auth-actions .home-create-account-btn,
.home-guest-auth-actions .home-apple-btn {
    flex: 1 1 0;
    min-width: 0;
}

.home-guest-auth-actions .home-create-account-btn {
    min-height: 52px;
    font-size: 15px;
    font-weight: 700;
}

.home-guest-auth-actions .home-apple-btn {
    width: auto;
}

.auth-form-card .auth-form {
    display: flex;
    flex-direction: column;
    /* .name-field carries its own padding-top for the settings screen; inside a
     * form the gap already spaces the rows, so it only doubled up. */
    gap: 16px;
}

.auth-form-card .name-field {
    padding: 0;
}

/* .name-input is `flex: 1 1 0%` because it was built for .name-edit-row, where
 * it shares a row with a Save button. In a block container that resolves to the
 * browser's default input width - about 20 characters - which is why these sat
 * at roughly three quarters of the card. */
.auth-form-card .name-input {
    flex: 0 0 auto;
    width: 100%;
}

/* The field also has to read as recessed, and --bg-deep (#FBF3EA) against this
 * card's #fff6ec differs by three points per channel - invisible. A neutral
 * wash gives the well an actual floor; the accent hairline from .name-input
 * still draws the edge. */
.auth-form-card .name-input:not(:focus) {
    background: rgba(40, 74, 81, 0.05);
}

/* Stacked, not side by side. Equal-width halves made "Send reset link" wrap to
 * two lines in English, and German ("Link zum Zurücksetzen senden") is half as
 * long again - this is the one place a label cannot be shortened to fit. */
.auth-form-card .apple-name-actions {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* Cancel here inherits the neutral secondary - it backs out of the form, so it
 * should not wear the action hue. */

.auth-forgot-link {
    align-self: flex-start;
    margin: -4px 0 0;
    padding: 0;
    border: none;
    background: none;
    font: inherit;
    font-size: 13px;
    font-weight: 700;
    /* Was var(--accent, #0d8a9a) - that teal fallback is from a palette this
     * app does not use, and would have shipped if --accent ever went missing. */
    color: var(--accent-2);
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 3px;
    padding: 4px 0;
}

.auth-forgot-success {
    margin: 0;
    font-size: 14px;
    font-weight: 600;
    line-height: 1.45;
    /* --quest-green does not exist, so this was rendering a hardcoded off-palette
     * green. The app's token is --success. */
    color: var(--success);
}

.home-guest-apple-wrap {
    margin-top: 12px;
}

.home-guest-apple-hint {
    margin: 0 0 10px;
    font-size: 13px;
    font-weight: 600;
    line-height: 1.4;
    color: var(--text-muted);
}

.daily-cta {
    height: 200px;
    position: relative;
    isolation: isolate;
    overflow: hidden;
    display: flex;
    align-items: center;
    gap: 14px;
    width: 100%;
    padding: 16px 18px;
    border: none;
    cursor: pointer;
    text-align: left;
    font-family: inherit;
    color: #fff;
    border-radius: var(--radius-card);
    background: var(--daily-hero-bg);
    box-shadow: var(--shadow-card);
    transition: transform 0.12s ease, box-shadow 0.15s ease;
}

.daily-cta:hover {
    box-shadow: 0 12px 30px rgba(20, 40, 48, 0.28);
}

.daily-cta:active {
    transform: scale(0.98);
}

.daily-cta .daily-hero-waves {
    height: 100%;
}

.daily-cta-icon,
.daily-cta-text,
.daily-cta-arrow {
    position: relative;
    z-index: 1;
}

.daily-cta-icon {
    width: auto;
    height: 120px;
    flex-shrink: 0;
    display: block;
    object-fit: contain;
    transform: rotate(-12deg);
    /* The app icon is a full-bleed square with hard corners - the art this
     * replaced carried its own rounding and transparency. Without a radius the
     * tilt exposes four sharp points on the teal card. */
    border-radius: 25px;
    margin: 0 7px;
    /* Soft lift so the 3D art reads on the teal card. */
    filter: drop-shadow(0 4px 8px rgba(10, 24, 30, 0.35));
}

.daily-cta-text {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.daily-cta-eyebrow {
    font-size: 11px;
    font-weight: 800;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--accent);
}

.daily-cta-title {
    font-size: 26px;
    font-weight: 800;
    margin-top: 2px;
}

.daily-cta-arrow {
    flex-shrink: 0;
    display: inline-flex;
}

.daily-cta-arrow svg {
    width: 20px;
    height: 20px;
    stroke: rgba(255, 255, 255, 0.9);
    fill: none;
}

/* --- Choose Difficulty CTA (home) --- */
.difficulty-cta {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    display: flex;
    align-items: center;
    gap: 16px;
    width: 100%;
    padding: 20px 20px;
    border: none;
    cursor: pointer;
    text-align: left;
    font-family: inherit;
    color: #fff;
    border-radius: var(--radius-card);
    background: var(--difficulty-hero-bg);
    box-shadow: var(--shadow-card);
    transition: transform 0.12s ease, box-shadow 0.15s ease;
}

/* Above the waves, below the content. The waves sit at auto z-index earlier in
 * the tree, so this generated box paints over them; the icon, text and arrow
 * are all z-index 1 and stay clear of it. */
.difficulty-cta::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 0;
    border-radius: inherit;
    background: var(--difficulty-hero-shade);
    pointer-events: none;
}

.difficulty-cta[data-action="choose-difficulty"] {
    margin-top: 20px;
}

.cascade-cta {
    margin-top: 14px;
}

/* Cascade Puzzles hub: single featured puzzle, no list (Phase A MVP). */
.cascade-hub-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    text-align: center;
    padding: 32px 24px;
    margin-top: 12px;
    border-radius: var(--radius-card);
    background: var(--bg-surface);
    box-shadow: var(--shadow-card);
}

/* An icon well like the rest of the app - soft tint, hue glyph, wave band -
 * rather than a solid accent block with a white glyph, which was the only one
 * of its kind. Cascade is a race against the clock, so it takes the time hue
 * (SG 2.2), not the generic accent. */
.cascade-hub-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 60px;
    height: 60px;
    border-radius: 18px;
    background: color-mix(in srgb, var(--accent-2) 16%, var(--bg-surface));
    color: var(--accent-2);
}

.cascade-hub-glyph {
    display: inline-flex;
}

/* Icons.cascade is a filled glyph - its rects declare fill="currentColor" and
 * stroke="none", which override anything set on the <svg>, so it paints from
 * `color` either way. */
.cascade-hub-glyph svg {
    width: 30px;
    height: 30px;
}

.cascade-hub-title {
    margin: 4px 0 0;
    font-size: 22px;
    font-weight: 800;
}

.cascade-hub-desc {
    margin: 0;
    max-width: 320px;
    font-size: 14px;
    color: var(--text-muted);
}

.cascade-hub-meta {
    display: flex;
    align-items: center;
    gap: 14px;
    margin: 6px 0 14px;
    font-size: 13px;
    font-weight: 700;
    color: var(--text-muted);
}

.cascade-hub-meta-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

/* Badges, not loose icon+text pairs: each is a tinted pill carrying a filled
 * disc, the same construction as the time pill on the cascade tiles, so the
 * detail page and the list speak the same way. currentColor drives the tint,
 * the disc and the value together, so each badge needs one hue set on it. */
.cascade-hub-meta-item {
    padding: 5px 12px 5px 5px;
    border-radius: var(--radius-chip);
    background: color-mix(in srgb, currentColor 12%, var(--bg-surface));
    font-weight: 800;
}

.cascade-hub-meta-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: currentColor;
}

/* Icons.clock declares no stroke-width, so it would render hairline here.
 * #fff rather than currentColor: the glyph sits on the disc, not beside it. */
.cascade-hub-meta-item svg {
    width: 13px;
    height: 13px;
    fill: none;
    stroke: #fff;
    stroke-width: 2.2;
    color: #fff;
}

/* A value is coloured by what it measures (SG 2.2): the clock is orange, the
 * word count is teal. They were both muted grey, so neither read as a value. */
.cascade-hub-meta-item--time {
    color: var(--accent-2);
}

.cascade-hub-meta-item--words {
    color: var(--stat-teal);
}

.cascade-hub-result {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    width: 100%;
    margin-top: 0;
    margin-bottom: 2px;
}

.cascade-hub-rank-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 14px;
    border-radius: var(--radius-chip);
    /* Solid accent, matching the all-time "Your rank" pill - the two are the
     * same object in different places, so they carry the same treatment.
     * --accent-strong for the contrast reason documented there. */
    background: var(--accent-strong);
    color: var(--text-on-accent);
    font-weight: 800;
    line-height: 1.2;
}

.cascade-hub-rank-badge-label {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.01em;
}

.cascade-hub-rank-badge-value {
    font-family: var(--font-grid);
    font-size: 15px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}

/* Width only. The colour comes from .btn.secondary.affirm, which is the one
 * light-orange share treatment the app uses everywhere. */
.cascade-hub-share {
    width: auto;
    min-width: 160px;
}

/* Words found is a solved count, so teal (SG 2.2) - it was carrying the orange
 * accent, which the guide reserves for time and action. */
.cascade-hub-result-value {
    font-size: 14px;
    font-weight: 700;
    line-height: 1.35;
    text-align: center;
    color: var(--stat-teal);
}

/* Two-up, matching the campaign list. These were full-width rows while the
 * campaign grid beside them was two columns, so the same component read as two
 * different things depending on which mode you entered. Nothing here assumed
 * the extra width - the time pill is flex-shrink:0 and the status badge is a
 * fixed 24px - so the tiles just reflow. */
.cascade-list-tiles {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px;
    margin-top: 12px;
    padding-bottom: 8px;
}

.cascade-list-tiles .puzzle-tile {
    width: 100%;
}

.cascade-list-tile .tile-status > [data-icon] svg,
.cascade-list-tile .tile-status > svg {
    width: 13px;
    height: 13px;
}

/* Was a stroked stopwatch dial with the duration set 8px inside it — the only
 * skeuomorphic object on the page, and unreadable at that size. Now a pill: a
 * clock disc carries the meaning, the duration is plain legible text beside it. */
.cascade-list-tile .tile-timepill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
    padding: 5px 10px 5px 8px;
    border-radius: var(--radius-chip);
    background: rgba(255, 255, 255, 0.92);
    box-shadow: 0 2px 6px rgba(120, 90, 60, 0.18);
    color: var(--accent);
}

/* A filled disc rather than a bare glyph: on the white pill an orange outline
 * had to compete with the orange number beside it, and the mark now reads as an
 * object. Orange because this measures time (SG 2.2). */
.cascade-list-tile .tile-timepill-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--accent);
    color: #fff;
}

/* Icons.clock declares no stroke-width of its own, so it would inherit 1 and
 * render hairline at this size. */
.cascade-list-tile .tile-timepill-icon svg {
    width: 12px;
    height: 12px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2.2;
}

.cascade-list-tile .tile-timepill-time {
    font-size: 13px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    letter-spacing: -0.01em;
    color: var(--text-primary);
}

.cascade-list-empty {
    margin: 24px 0;
    text-align: center;
    font-size: 14px;
    color: var(--text-muted);
}

.difficulty-cta:hover {
    box-shadow: 0 12px 30px rgba(160, 90, 40, 0.28);
}

.difficulty-cta:active {
    transform: scale(0.98);
}

.difficulty-cta .daily-hero-waves {
    height: 100%;
}

.difficulty-cta .daily-hero-waves .wave-1 {
    fill: var(--difficulty-hero-wave);
}

.difficulty-cta .daily-hero-waves .wave-2 {
    fill: var(--difficulty-hero-wave-strong);
}

.difficulty-cta-icon,
.difficulty-cta-text,
.difficulty-cta-arrow {
    position: relative;
    z-index: 1;
}

.difficulty-cta-icon {
    position: relative;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 60px;
    height: 60px;
    border-radius: 16px;
    background: #fff;
    color: var(--accent);
}

/* Same wave band as the achievement and statistics icon wells — a mask filled
 * with currentColor, so it picks up the well's own hue. */
.difficulty-cta-icon::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 0;
    border-radius: inherit;
    background: currentColor;
    opacity: 0.16;
    pointer-events: none;
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M0,60 C22,45 38,47 55,59 C72,71 86,69 100,57 L100,100 L0,100 Z' fill='%23000'/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M0,60 C22,45 38,47 55,59 C72,71 86,69 100,57 L100,100 L0,100 Z' fill='%23000'/%3E%3C/svg%3E");
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
}

/* Nested so the well is not a renderIcons target — otherwise rendering the
 * glyph would wipe the seal out of it. */
.difficulty-cta-glyph {
    position: relative;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
}

.difficulty-cta-glyph svg {
    width: 100%;
    height: 100%;
    color: var(--accent);
}

/* The seal follows its well: teal wells keep the teal default, and Best Daily
 * Time's orange well gets an orange seal. (0,2,0) beats .icon-seal (0,1,0). */
.stat-highlight-icon .icon-seal {
    background: var(--accent);
}

/* The home CTAs sit on orange cards, so their seal keeps the accent rather than
 * the achievements' teal. */
.icon-seal--accent {
    background: var(--accent);
    box-shadow: 0 0 0 2.5px #fff;
}

.difficulty-cta-icon .icon-seal {
    width: 24px;
    height: 24px;
    left: -6px;
    bottom: -6px;
}

.difficulty-cta-icon .icon-seal svg {
    width: 14px;
    height: 14px;
    color: #fff;
}

.difficulty-cta-text {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.difficulty-cta-title {
    font-size: 20px;
    font-weight: 800;
    line-height: 1.1;
}

.difficulty-cta-desc {
    font-size: 15px;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.9);
}

.difficulty-cta-arrow {
    flex-shrink: 0;
    display: inline-flex;
}

.difficulty-cta-arrow svg {
    width: 22px;
    height: 22px;
    stroke: rgba(255, 255, 255, 0.92);
    fill: none;
}

/* --- Section title --- */
.section-title {
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
    margin: 18px 4px 10px;
    font-weight: 700;
}

/* --- Toast --- */
.toast {
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    left: 50%;
    bottom: 90px;
    transform: translateX(-50%) translateY(20px);
    background: rgba(0, 0, 0, 0.82);
    color: #fff;
    padding: 11px 18px;
    border-radius: 999px;
    font-size: 14px;
    font-weight: 600;
    opacity: 0;
    transition: opacity 0.2s ease, transform 0.2s ease;
    pointer-events: none;
    z-index: 50;
    white-space: nowrap;
}

/* Same construction as .bottom-nav-icon, because a toast that answers a chrome
 * control should show that control's own glyph. fill is pinned off on purpose:
 * soundOn/soundOff ship no fill attribute and their speaker body is a CLOSED
 * path, so it renders as a solid blob the moment fill defaults back on. */
.toast-icon {
    display: inline-flex;
    flex: none;
    width: 22px;
    height: 22px;
}

.toast-icon svg {
    width: 100%;
    height: 100%;
    stroke: currentColor;
    fill: none;
}

.toast.show {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

/* In-app mock of an iOS lock-screen / banner push (dev preview + design check). */
.push-preview {
    position: absolute;
    left: 12px;
    right: 12px;
    top: 12px;
    z-index: 140;
    pointer-events: none;
    opacity: 0;
    transform: translateY(-12px);
    transition: opacity 0.25s ease, transform 0.28s cubic-bezier(0.34, 1.4, 0.64, 1);
}

.push-preview.show {
    opacity: 1;
    transform: translateY(0);
}

.push-preview[hidden] {
    display: none !important;
}

.push-preview-card {
    padding: 12px 14px;
    border-radius: 18px;
    background: rgba(255, 255, 255, 0.94);
    box-shadow: 0 12px 32px rgba(20, 16, 40, 0.18);
    backdrop-filter: blur(12px);
    color: #1c1c1e;
    text-align: left;
}

.push-preview-app {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 4px;
}

.push-preview-icon {
    width: 22px;
    height: 22px;
    border-radius: 6px;
    object-fit: cover;
}

.push-preview-app-name {
    flex: 1;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: rgba(28, 28, 30, 0.55);
}

.push-preview-now {
    font-size: 12px;
    font-weight: 600;
    color: rgba(28, 28, 30, 0.4);
}

.push-preview-title {
    font-size: 14px;
    font-weight: 700;
    line-height: 1.25;
}

.push-preview-body {
    margin-top: 2px;
    font-size: 13px;
    font-weight: 500;
    line-height: 1.35;
    color: rgba(28, 28, 30, 0.78);
}

/* In-flow under the board (between .grid-wrap and play bottom nav) — never
 * overlays letters. The floating #toast.tutorial path is retired for this. */
.play-tutorial-toast {
    display: none;
    flex: 0 0 auto;
    align-self: center;
    max-width: min(420px, calc(100% - 24px));
    margin: 4px 12px 8px;
    padding: 11px 18px;
    border-radius: 999px;
    background: rgba(253, 203, 110, 0.95);
    color: #1a1a1a;
    font-size: 14px;
    font-weight: 700;
    line-height: 1.3;
    text-align: center;
    z-index: 4;
    pointer-events: none;
}

.play-tutorial-toast.show {
    display: block;
}

/* Legacy floating tutorial toast — keep styles harmless if still applied. */
.toast.tutorial {
    background: rgba(253, 203, 110, 0.95);
    color: #1a1a1a;
    font-weight: 700;
    bottom: calc(var(--bottom-nav-wave-height, 32px) + 12px + var(--safe-bottom, 0px));
    max-width: min(420px, calc(100% - 24px));
    white-space: normal;
    text-align: center;
    line-height: 1.3;
}

html.ios-browser-banner .toast.tutorial {
    bottom: calc(var(--bottom-nav-wave-height, 32px) + 12px + var(--ios-app-banner-height, 0px));
}

/* --- App splash (loading + iOS handoff) --- */
/* No fill of its own. .screen is inset by --safe-top/--safe-bottom, so a flat
 * colour here only covered the middle and left the shell's --app-bg gradient
 * showing in the strips above and below - a band at the notch and another at
 * the home indicator. The shell already paints the full height. */
#screen-loading {
    background: var(--app-bg, #fbf3ea);
}

.app-splash {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 24px;
    padding: 0 24px;
}

/* The web loading screen (logo + progress bar) runs on native too. It used to
 * be hidden here so only the static LaunchScreen showed, which meant the native
 * shell had a caption and no progress at all - nothing moved while the app
 * loaded. Both draw the same logo on the same cream ground, so the handover
 * from LaunchScreen to this reads as the caption swapping for the bar. */

html.native-shell .backdrop,
html.native-shell #app {
    background: #FBF3EA;
}

/* The icon file is a full-bleed square - iOS and Android mask it themselves, so
 * on its own it renders hard-cornered. 22.5% approximates the iOS mask, so the
 * splash shows the same shape the player taps on their home screen. */
.app-splash-logo {
    width: min(168px, 38vw);
    height: auto;
    aspect-ratio: 1;
    object-fit: contain;
    border-radius: 22.5%;
}

.app-splash-bar {
    width: min(200px, 48vw);
    height: 6px;
    border-radius: var(--radius-chip, 999px);
    background: var(--fill-quiet, rgba(40, 74, 81, 0.12));
    overflow: hidden;
}

.app-splash-bar-fill {
    display: block;
    width: 42%;
    height: 100%;
    border-radius: inherit;
    background: linear-gradient(90deg, var(--accent-light, #ffa364), var(--accent-2, #E3650F));
    animation: splash-bar-slide 1.35s ease-in-out infinite;
}

@keyframes splash-bar-slide {
    0% { transform: translateX(-120%); }
    100% { transform: translateX(320%); }
}

/* --- Loading spinner (dev / fallback) --- */
.spinner {
    width: 34px;
    height: 34px;
    border: 3px solid var(--bg-surface-strong);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* --- Offline splash (native iOS, no connection) --- */
.offline-splash {
    width: min(100%, 340px);
    padding: 0 8px;
    text-align: center;
}

.offline-face {
    width: 96px;
    height: 96px;
    margin: 0 auto 24px;
    border-radius: 50%;
    background: var(--bg-surface-strong);
    box-shadow: var(--shadow-card);
    display: flex;
    align-items: center;
    justify-content: center;
}

.offline-face svg {
    width: 56px;
    height: 56px;
    display: block;
}

.offline-title {
    font-size: 22px;
    font-weight: 800;
    line-height: 1.25;
    margin-bottom: 10px;
    color: var(--text-primary);
}

.offline-desc {
    font-size: 15px;
    line-height: 1.45;
    color: var(--text-soft);
    margin-bottom: 28px;
}

.offline-retry {
    width: min(100%, 280px);
}

#screen-offline.active,
#screen-loading.active {
    z-index: 200;
}

#screen-offline.active + .global-topbar,
#screen-offline.active ~ .bottom-nav {
    display: none;
}

/* --- Retention: streak risk, quests, achievements --- */
.home-streak.at-risk {
    background: rgba(255, 220, 210, 0.98);
    box-shadow: 0 0 0 2px rgba(255, 100, 60, 0.35);
}

.home-streak.at-risk .home-streak-icon {
    color: #e85d04;
}

.home-menu-grid {
    margin-bottom: 8px;
}

/* --- Alerts: the home retention notices --- */
/* Built as an ordinary card - --bg-surface on --shadow-card, exactly what
 * .puzzle-tile uses - because §1.3 is explicit that cards float rather than
 * outline. An earlier pass gave these a tinted ground and a left rail; the rail
 * bled around the corner radius and the tint muddied against the cream page.
 *
 * The tone now lives where the app always puts it: a tinted icon well carrying
 * the shared wave band, plus the link colour. The card itself stays quiet. */
.alert {
    position: relative;
    margin: 12px 0 4px;
    border-radius: var(--radius-card);
    background: var(--bg-surface);
    box-shadow: var(--shadow-card);
    font-size: 14px;
    line-height: 1.35;
}

.alert--info {
    --alert-hue: var(--stat-teal);
    --alert-tint: var(--stat-teal-soft);
}

.alert--warn {
    --alert-hue: var(--accent-2);
    --alert-tint: var(--accent-soft);
}

/* Right padding clears the close button so text never wraps under it. */
.alert-main {
    display: flex;
    align-items: center;
    gap: 13px;
    width: 100%;
    margin: 0;
    padding: 13px 46px 13px 14px;
    border: none;
    border-radius: inherit;
    background: none;
    font: inherit;
    font-size: inherit;
    line-height: inherit;
    color: var(--text-primary);
    text-align: left;
}

button.alert-main {
    cursor: pointer;
}

.alert-icon {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: 12px;
    background: var(--alert-tint);
    color: var(--alert-hue);
}

/* `trophy` is a stroke glyph; `flame` carries fill/stroke on its own path, so
 * those attributes win over these and each paints correctly. The old well set
 * only `color`, which left the stroke glyph a solid black blob.
 * position/z-index lift the glyph above the wave band. */
.alert-icon svg {
    position: relative;
    z-index: 1;
    width: 21px;
    height: 21px;
    stroke: currentColor;
    fill: none;
}

.alert-body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.alert-link {
    font-size: 12px;
    font-weight: 700;
    color: var(--alert-hue);
}

.alert-close {
    position: absolute;
    top: 7px;
    right: 7px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: none;
    color: var(--text-dim);
    cursor: pointer;
}

.alert-close svg {
    width: 13px;
    height: 13px;
    stroke: currentColor;
    fill: none;
}

.alert-close:active {
    background: rgba(40, 74, 81, 0.08);
}

/* A podium finish takes the medal hue rather than the standing teal. Same
 * specificity as .alert--info, so these must stay below it. */
.home-yesterday-rank--top1 {
    --alert-hue: var(--medal-gold);
    --alert-tint: var(--medal-gold-soft);
}

.home-yesterday-rank--top2 {
    --alert-hue: var(--medal-silver);
    --alert-tint: var(--medal-silver-soft);
}

.home-yesterday-rank--top3 {
    --alert-hue: var(--medal-bronze);
    --alert-tint: var(--medal-bronze-soft);
}

.quest-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-bottom: 16px;
}

.quest-row {
    position: relative;
    overflow: hidden;
    min-height: 90px;
    padding: 14px 14px 12px;
    border-radius: 16px;
    background: var(--bg-surface-solid);
    transition: background 0.2s ease;
}

.quest-row.completed {
    background: var(--bg-surface-solid);
}

.quest-row-waves {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    /* Fixed, not a percentage of the row. Rows are not all the same height —
     * a goal carrying a progress bar or a DONE pill runs ~93px against a plain
     * one's 54px — so 78% drew a 73px crest on one and a 42px crest on the
     * next. 42px is what a standard row already produced, so the common case
     * is unchanged and the tall ones now match it. */
    height: 42px;
    z-index: 0;
    pointer-events: none;
}

/* One hue at two strengths, the way every other wave in the system works —
 * rather than three hand-picked hex values. The third crest and its bespoke
 * curve are gone: this now draws the same two-crest path as the daily hero,
 * the podium and the difficulty card. */
.quest-row-waves .wave-1 {
    fill: var(--shade-cool);
    opacity: 0.45;
}

.quest-row-waves .wave-2 {
    fill: var(--shade-cool);
    opacity: 0.74;
}

/* Completed rows swap the hue, not the construction. --success rather than the
 * rgba(46, 184, 92) that used to sit here: that was a second green, close
 * enough to --success to look like a mistake and far enough to be one.
 *
 * The opacities are NOT the grey's. Matching the numbers made the green band
 * look taller, because green separates from white more readily than a
 * near-neutral grey does: at .10 / .18 the crests measured deltaE 5.95 / 10.73
 * against the row, where the grey reads 5.04 / 8.31. The upper crest was
 * visible on a completed row and invisible on the others, so the eye put the
 * band's top edge in two different places. These values match the grey's
 * perceived weight instead of its alpha. */
.quest-row.completed .quest-row-waves .wave-1 {
    fill: var(--success);
    opacity: 0.085;
}

.quest-row.completed .quest-row-waves .wave-2 {
    fill: var(--success);
    opacity: 0.14;
}

.quest-row-main {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.quest-row--single:not(.completed) .quest-row-main {
    gap: 0;
}

.quest-row-top {
    display: flex;
    justify-content: space-between;
    gap: 10px;
    align-items: flex-start;
}

.quest-title {
    flex: 1;
    min-width: 0;
    font-weight: 700;
    font-size: 14px;
    line-height: 1.35;
    color: var(--text-primary);
}

.quest-row.completed .quest-title {
    color: var(--text-soft);
}

.quest-reward-badge {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 5px 10px 5px 8px;
    border-radius: 999px;
    border: 1px solid transparent;
    /* Gold, not orange: this is a coin reward, and the coin family is gold
     * everywhere - the unlocked achievement chip (.ach-reward) uses exactly
     * this pair. Orange here read as an action colour on a value that is not
     * an action. Earned still flips to green below: that is the done signal,
     * not the currency. */
    background: var(--stat-gold-soft);
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 13px;
    font-weight: 800;
    color: var(--stat-gold);
    white-space: nowrap;
}

.quest-reward-badge.is-earned {
    background: rgba(46, 184, 92, 0.14);
    color: #1f7a43;
}

/* The coin is one object across the whole app - topbar balance, stat tile,
 * achievement chip, reward banners - so it reads as the same currency wherever
 * it appears. These were the last hardcoded orange pair; the gold tokens make
 * this row match. */
.quest-reward-coin {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--topbar-coin-bg);
    box-shadow: 0 0 0 2.5px var(--topbar-coin-ring);
    color: #fff;
    box-sizing: content-box;
}

.quest-reward-coin svg {
    width: 100%;
    height: 100%;
}

.quest-reward-value {
    line-height: 1;
}

.quest-bar {
    height: 7px;
    border-radius: 999px;
    background: rgba(40, 74, 81, 0.08);
    overflow: hidden;
}

.quest-row.completed .quest-bar {
    background: rgba(46, 184, 92, 0.16);
}

.quest-bar-fill {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--accent), var(--accent-2));
    transition: width 0.35s ease;
}

.quest-row.completed .quest-bar-fill {
    background: linear-gradient(90deg, #2eb872, #27ae60);
}

.quest-status {
    display: block;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-soft);
}

.quest-status--done {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    align-self: flex-start;
    padding: 4px 10px 4px 8px;
    border-radius: 999px;
    background: rgba(46, 184, 92, 0.16);
    color: #1f7a43;
    font-size: 12px;
    font-weight: 800;
    letter-spacing: 0.02em;
    text-transform: uppercase;
}

.quest-status-icon {
    display: inline-flex;
    width: 14px;
    height: 14px;
}

.quest-status-icon svg {
    width: 14px;
    height: 14px;
    stroke: currentColor;
    fill: none;
}

.quest-reward {
    font-size: 12px;
    font-weight: 700;
    color: var(--accent);
    white-space: nowrap;
}

.quest-progress {
    display: block;
    margin-top: 6px;
    font-size: 12px;
    color: var(--text-soft);
}

/* --- Achievements: progress hero --- */
/* Same construction as the statistics hero, so the two nav screens rhyme. */
.ach-hero {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    margin: 0 0 16px;
    padding: 18px 18px 16px;
    border-radius: var(--radius-card);
    background: var(--daily-hero-bg);
    box-shadow: var(--shadow-card);
    color: #fff;
}

.ach-hero .daily-hero-waves {
    height: 100%;
}

.ach-hero-top {
    position: relative;
    z-index: 1;
}

.ach-hero-top {
    display: flex;
    align-items: center;
    gap: 14px;
}

.ach-hero-eyebrow {
    display: block;
    font-size: 11px;
    font-weight: 800;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: #ffffffad;
}

.ach-hero-value {
    display: block;
    margin-top: 5px;
    font-size: 32px;
    font-weight: 800;
    line-height: 1;
    letter-spacing: -0.025em;
    font-variant-numeric: tabular-nums;
}

/* White disc, card-hue glyph - the construction .list-continue-play documents
 * for this same teal ground, where white measures 9.17:1. Shared by both hero
 * cards so Achievements and Statistics read as one family. Left-hand placement
 * follows the app's dominant idiom (achievement cards, mode cards, difficulty
 * CTAs, stat tiles); both heroes previously hung a 30%-white glyph off the right
 * edge, where it dissolved into the card and left a hole across the middle. No
 * shadow: an elevated white disc reads as pressable, and these are not. */
.ach-hero-icon,
.stats-hero-icon {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: #fff;
    color: var(--stat-teal);
}

.ach-hero-glyph,
.stats-hero-glyph {
    display: inline-flex;
}

/* The count says how far you have come; this says how far is left, which is the
 * figure that actually pulls. It also earns the card's full width - with the disc
 * alone the right half sat empty. Deliberately secondary to the 32px count: the
 * eyebrow's type treatment, at a lower white. */
.ach-hero-remaining {
    margin-left: auto;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 2px;
    text-align: right;
}

.ach-hero-remaining-value {
    font-size: 22px;
    font-weight: 800;
    line-height: 1;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    color: rgba(255, 255, 255, 0.85);
}

.ach-hero-remaining-label {
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.55);
}

.ach-hero-glyph svg,
.stats-hero-glyph svg {
    width: 26px;
    height: 26px;
    stroke: currentColor;
    fill: none;
}

.ach-hero-glyph.is-filled svg,
.stats-hero-glyph.is-filled svg {
    fill: currentColor;
    stroke: none;
}

.ach-hero.is-loading .ach-hero-value {
    display: inline-block;
    min-width: 7ch;
    min-height: 1em;
    margin-top: 5px;
    border-radius: 6px;
    color: transparent;
    background: rgba(255, 255, 255, 0.18);
    animation: ach-hero-pulse 1.1s ease-in-out infinite;
}

@keyframes ach-hero-pulse {
    0%, 100% { opacity: 0.55; }
    50% { opacity: 1; }
}

/* --- Groups --- */
.achievement-group + .achievement-group {
    margin-top: 6px;
}

.achievement-grid {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* --- Cards --- */
.achievement-card {
    display: flex;
    gap: 12px;
    align-items: center;
    padding: 12px 14px;
    border-radius: 14px;
    background: var(--bg-surface);
}

/* Earned cards get a stronger surface; the gold seal and reward chip carry the
 * earned signal, so no outline is needed. */
.achievement-card.unlocked {
    background: var(--bg-surface-strong);
}

/* Locked is quiet, not broken. The old rule used grayscale(0.4) + opacity 0.55
 * on the whole card, which muddied the text as well as the art. */
.achievement-card.locked .achievement-card-title {
    color: var(--text-soft);
}

.achievement-card.locked.achievement-card--connectable,
.achievement-card.locked.achievement-card--linkable {
    cursor: pointer;
}

/* --- Icon well: flat glyph, tier ramp --- */
.achievement-card-icon {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    flex-shrink: 0;
    border-radius: 13px;
    /* Tier ramp: one hue, rising presence across a group's tiers. Same idea as
     * the difficulty ramp on the statistics By Mode rows. */
    background: rgba(81, 148, 162, calc(0.13 + var(--ach-tier, 0) * 0.07));
    color: var(--stat-teal);
}

.achievement-card.unlocked .achievement-card-icon {
    background: rgba(240, 197, 58, calc(0.20 + var(--ach-tier, 0) * 0.07));
    color: var(--stat-gold);
}

/* The glyph lives in its own span. renderIcons() assigns innerHTML to every
 * [data-icon] element, so the well itself must NOT carry data-icon — otherwise
 * rendering wipes the tier badge and the earned check out of it. */
.ach-glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
}

.ach-glyph svg {
    width: 100%;
    height: 100%;
    stroke: currentColor;
    fill: none;
}

.ach-glyph.is-filled svg {
    fill: currentColor;
    stroke: none;
}

/* Corner seal: the single marker slot. A solid disc with a white glyph and a
 * ring that punches it clear, overflowing the well on purpose so it sits
 * outside the rounded square rather than inside the artwork. Shows the category
 * glyph in deep teal while locked, and a check in gold once earned — so a card
 * carries at most two badges, this and the tier stamp. */
.icon-seal {
    position: absolute;
    left: -4px;
    bottom: -4px;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 19px;
    height: 19px;
    border-radius: 50%;
    background: #33707f;
    color: #fff;
    box-shadow: 0 0 0 2.5px var(--bg-surface-strong);
    pointer-events: none;
}

/* Earned: the seal goes gold, matching the coin and the check seal. */
.achievement-card.unlocked .icon-seal {
    background: var(--topbar-coin-bg);
}

.icon-seal svg {
    width: 11px;
    height: 11px;
    stroke: currentColor;
    stroke-width: 2.6;
    fill: none;
}

/* Tier stamp. The flat glyphs lost the numbers the old raster icons carried, so
 * the threshold comes back as a badge. It sits top-RIGHT, diagonally opposite
 * .icon-seal at bottom-left. Overflows the well deliberately — the ring punches
 * it clear of the artwork behind it.
 * Text is currentColor, so it tracks the well: teal when locked, gold when
 * earned, with no extra rules. */
.ach-tier {
    position: absolute;
    right: -5px;
    top: -5px;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 17px;
    height: 16px;
    padding: 0 4px;
    border-radius: 999px;
    background: var(--bg-surface-strong);
    color: currentColor;
    font-family: var(--font-ui);
    font-size: 9px;
    font-weight: 800;
    line-height: 1;
    letter-spacing: 0.01em;
    font-variant-numeric: tabular-nums;
    box-shadow: 0 0 0 1px rgb(176 200 204), 0 1px 3px rgb(166 193 199);
    pointer-events: none;
}

/* Earned: the ring goes gold so it sits with the gold well and check seal
 * instead of reading cold against them. Same two-layer structure, warm values.
 * (0,3,0) beats .ach-tier (0,1,0), so ordering is not load-bearing here. */
.achievement-card.unlocked .ach-tier {
    box-shadow: 0 0 0 1px rgb(232 197 128), 0 1px 3px rgb(222 185 112);
}

/* --- Reward chip: the coin, same construction as the quest reward badge --- */
.ach-reward {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 9px 4px 4px;
    border-radius: var(--radius-chip);
    background: var(--fill-quiet);
    font-family: var(--font-grid);
    font-size: 12px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    white-space: nowrap;
}

.achievement-card.unlocked .ach-reward {
    background: var(--stat-gold-soft);
    color: var(--stat-gold);
}

.ach-reward-coin {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 15px;
    height: 15px;
    border-radius: 50%;
    background: var(--topbar-coin-bg);
    box-shadow: 0 0 0 2px var(--topbar-coin-ring);
    color: #fff;
    box-sizing: content-box;
    flex-shrink: 0;
}

.achievement-card.locked .ach-reward-coin {
    background: rgba(40, 74, 81, 0.22);
    box-shadow: 0 0 0 2px rgba(40, 74, 81, 0.12);
}

.ach-reward-coin svg {
    width: 100%;
    height: 100%;
}

.achievement-card-text {
    flex: 1;
    min-width: 0;
}

.achievement-connect-btn {
    flex-shrink: 0;
    align-self: center;
    min-height: 38px;
    padding: 0 18px;
    margin: 0;
    border: none;
    border-radius: 999px;
    font: inherit;
    font-size: 13px;
    font-weight: 800;
    letter-spacing: 0.01em;
    color: #fff;
    white-space: nowrap;
    cursor: pointer;
    transition: transform 0.12s ease, box-shadow 0.15s ease, filter 0.15s ease;
}

.achievement-connect-btn:hover {
    filter: brightness(1.06);
}

.achievement-connect-btn:active {
    transform: scale(0.97);
}

.achievement-connect-btn--apple {
    background: linear-gradient(145deg, #3d3d3d 0%, #000 100%);
    box-shadow:
        0 2px 8px rgba(0, 0, 0, 0.22),
        inset 0 1px 0 rgba(255, 255, 255, 0.12);
}

.achievement-connect-btn--facebook {
    background: linear-gradient(145deg, #4a9dff 0%, #1877f2 100%);
    box-shadow:
        0 2px 10px rgba(24, 119, 242, 0.28),
        inset 0 1px 0 rgba(255, 255, 255, 0.18);
}

.achievement-card-title {
    display: block;
    font-weight: 700;
    font-size: 15px;
}

.achievement-card-caption {
    display: block;
    font-size: 13px;
    line-height: 1.35;
    color: var(--text-soft);
    margin-top: 2px;
}

/* Top banners anchor at the physical top; safe-area inset is padding on the card
 * (same pattern as .global-topbar) so the paper runs behind the status bar. */
.achievement-overlay {
    position: absolute;
    top: 0;
    left: var(--safe-left);
    right: var(--safe-right);
    z-index: 100;
    transform: translateY(-110%);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition:
        transform 0.45s cubic-bezier(0.22, 1, 0.36, 1),
        opacity 0.35s ease,
        visibility 0s linear 0.45s;
}

.achievement-overlay.is-visible {
    transform: translateY(0);
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition:
        transform 0.45s cubic-bezier(0.22, 1, 0.36, 1),
        opacity 0.35s ease,
        visibility 0s;
}

.achievement-overlay-card {
    width: 100%;
    padding: var(--safe-top) 16px 16px;
    border-radius: 0 0 20px 20px;
    /* All three banners share the app's own paper. Only the icon well and the
     * kicker carry the per-banner hue - the card is never tinted, so the three
     * read as one component in three states rather than three components. Set
     * here and nowhere else; do not reintroduce a per-overlay background. */
    background: var(--bg-deep);
    box-shadow: 0 10px 36px rgba(20, 16, 40, 0.16);
    display: flex;
    align-items: center;
    gap: 14px;
    text-align: left;
}

.achievement-overlay-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 60px;
    height: 60px;
    flex-shrink: 0;
    border-radius: 18px;
    background: var(--stat-gold-soft);
    color: var(--stat-gold);
}

.achievement-overlay-icon svg {
    width: 30px;
    height: 30px;
    stroke: currentColor;
    fill: none;
}

.achievement-overlay-icon.is-filled svg {
    fill: currentColor;
    stroke: none;
}

.achievement-overlay-body {
    min-width: 0;
    flex: 1;
}

.achievement-overlay-kicker {
    margin: 0;
    font-size: 12px;
    font-weight: 800;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--accent-2);
}

.achievement-overlay-title {
    margin: 2px 0 0;
    font-weight: 800;
    font-size: 17px;
    line-height: 1.25;
}

.achievement-overlay-caption {
    margin: 4px 0 0;
    font-size: 13px;
    line-height: 1.35;
    color: var(--text-soft);
}

.achievement-overlay-reward {
    font-size: 14px;
    font-weight: 800;
    color: var(--accent);
    margin: 4px 0 0;
}

/* Reward chip. Deliberately identical in construction to the card's
 * .ach-reward in its unlocked state (components.css ~3403) - the banner
 * celebrates exactly what the card then shows, so it carries the same gold
 * pill rather than plain orange text. The gold values are inlined because
 * .ach-reward is neutral until .achievement-card.unlocked recolours it, and
 * there is no unlocked ancestor here.
 *
 * Same specificity as the base rule above (0,1,0), so this must stay after it. */
.achievement-overlay-reward--coin {
    /* A card-level column, not a fourth line of text. The banner was reading as
     * four stacked rows; the reward is a value, not prose, so it sits beside the
     * copy and lets the stack stay at three. flex-shrink: 0 keeps a long title
     * from crushing it - the body has flex: 1 and min-width: 0 to yield first. */
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin: 0;
    padding: 5px 12px 5px 5px;
    border-radius: var(--radius-chip);
    background: var(--stat-gold-soft);
    font-family: var(--font-grid);
    font-size: 14px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: var(--stat-gold);
    white-space: nowrap;
}

/* The coin is a filled disc everywhere in the app, never a glyph in a tinted
 * square - mirrors .ach-reward-coin exactly. `coin` is the one Font Awesome
 * entry in icons.js, an <i>, so it needs font-size rather than the svg rules. */
.achievement-overlay-reward-coin {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--topbar-coin-bg);
    box-shadow: 0 0 0 2px var(--topbar-coin-ring);
    color: #fff;
    box-sizing: content-box;
}

.achievement-overlay-reward-coin svg {
    width: 100%;
    height: 100%;
}

.level-up-overlay {
    position: absolute;
    top: 0;
    left: var(--safe-left);
    right: var(--safe-right);
    z-index: 101;
    transform: translateY(-110%);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition:
        transform 0.45s cubic-bezier(0.22, 1, 0.36, 1),
        opacity 0.35s ease,
        visibility 0s linear 0.45s;
}

.level-up-overlay.is-visible {
    transform: translateY(0);
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition:
        transform 0.45s cubic-bezier(0.22, 1, 0.36, 1),
        opacity 0.35s ease,
        visibility 0s;
}

/* Teal, not gold: a level is a fact about the player, not a reward - the same
 * "this is simply true" register as the Daily hero. The well joins the shared
 * icon-well wave group above, whose band is a mask filled with currentColor,
 * so setting the colour here is all it takes to get a teal wave. */
.level-up-overlay-icon {
    background: var(--stat-teal-soft);
    color: var(--stat-teal);
}

/* The level itself replaces the star. Mono + tabular so 5 and 11 sit on the
 * same optical centre, per the numeric-badge convention. */
.level-up-overlay-level {
    font-family: var(--font-grid);
    font-size: 24px;
    font-weight: 800;
    line-height: 1;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    color: currentColor;
}

.level-up-overlay-kicker {
    color: var(--stat-teal);
}

/* The quest green is a family outside the two-hue system (§11). Defined once
 * here so the well, seal ring and kicker cannot drift apart. The card itself is
 * not tinted - see .achievement-overlay-card. */
.quest-complete-overlay {
    --quest-green: #3cb371;
    --quest-green-soft: rgba(76, 175, 120, 0.2);
}

.daily-bonus-overlay {
    --daily-bonus-gold: #d4a017;
    --daily-bonus-gold-soft: rgba(212, 160, 23, 0.22);
}

.quest-complete-overlay,
.daily-bonus-overlay {
    position: absolute;
    top: 0;
    left: var(--safe-left);
    right: var(--safe-right);
    z-index: 102;
    transform: translateY(-110%);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition:
        transform 0.45s cubic-bezier(0.22, 1, 0.36, 1),
        opacity 0.35s ease,
        visibility 0s linear 0.45s;
}

.quest-complete-overlay.is-visible,
.daily-bonus-overlay.is-visible {
    transform: translateY(0);
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition:
        transform 0.45s cubic-bezier(0.22, 1, 0.36, 1),
        opacity 0.35s ease,
        visibility 0s;
}

.daily-bonus-overlay-icon {
    background: var(--daily-bonus-gold-soft);
    color: var(--daily-bonus-gold);
}

.daily-bonus-overlay-glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
}

.daily-bonus-overlay-kicker {
    color: var(--daily-bonus-gold);
}

.quest-complete-overlay-icon {
    background: var(--quest-green-soft);
    color: var(--quest-green);
}

/* The glyph box, so the grid centres above the wave band. */
.quest-complete-overlay-glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
}

/* Reuses .icon-seal - literally the same component as the earned check on an
 * achievement card, bottom-left, ring punching it clear of the artwork. Two
 * things change: it scales for the 60px well (the card's is 42px), and the ring
 * takes this card's mint ground instead of the default surface.
 * The svg override is required, not cosmetic: `.achievement-overlay-icon svg`
 * sets 30px at equal specificity and later source order, so without this the
 * seal's glyph would render at full glyph size and burst the disc. */
.quest-complete-overlay-seal {
    width: 22px;
    height: 22px;
    left: -5px;
    bottom: -5px;
    background: var(--quest-green);
    box-shadow: 0 0 0 2.5px var(--bg-deep);
}

.quest-complete-overlay-seal svg {
    width: 13px;
    height: 13px;
    stroke-width: 2.6;
}

.quest-complete-overlay-kicker {
    color: var(--quest-green);
}

@media (prefers-reduced-motion: reduce) {
    .achievement-overlay,
    .level-up-overlay,
    .quest-complete-overlay,
    .daily-bonus-overlay {
        transition: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .spinner { animation-duration: 1.6s; }
    .app-splash-bar-fill { animation: none; width: 42%; transform: none; }
}

/* Puzzle unlock modal */
.puzzle-unlock-modal {
    position: absolute;
    inset: 0;
    z-index: 120;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.25s ease, visibility 0s linear 0.25s;
}

.puzzle-unlock-modal.show {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity 0.25s ease, visibility 0s;
}

.puzzle-unlock-backdrop {
    position: absolute;
    inset: 0;
    background: var(--overlay-scrim);
    backdrop-filter: blur(6px);
}

.puzzle-unlock-card {
    position: relative;
    width: 100%;
    max-width: 320px;
    padding: 26px 22px 22px;
    border-radius: var(--radius-card);
    /* The same warm cream as the solved overlay, so modals share one ground. */
    background: #fff6ec;
    box-shadow: 0 20px 50px rgba(20, 16, 40, 0.18);
    text-align: center;
    transform: scale(0.92);
    transition: transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.puzzle-unlock-modal.show .puzzle-unlock-card {
    transform: scale(1);
}

/* Was a solid gradient disc with a PURPLE drop shadow - pre-teal residue (§11),
 * and the most saturated thing in the modal. Now the same soft well the
 * achievement, level-up and solved overlays use, wave band included. */
.puzzle-unlock-icon {
    width: 60px;
    height: 60px;
    margin: 0 auto 12px;
    border-radius: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--stat-gold-soft);
    color: var(--stat-gold);
}

/* The glyph lives in its own span so the well can carry the corner seal -
 * renderIcons() assigns innerHTML to every [data-icon] node, so a well holding
 * data-icon itself would wipe the seal out. Same arrangement as the solved
 * overlay's trophy well and the achievement cards. */
.puzzle-unlock-glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
}

.puzzle-unlock-glyph svg {
    width: 100%;
    height: 100%;
    stroke: currentColor;
    fill: none;
}

/* The same .icon-seal component the achievement cards use, scaled for this 60px
 * well and in the coin gold. Its ring punches through the modal's cream. */
.puzzle-unlock-seal {
    width: 24px;
    height: 24px;
    left: -6px;
    bottom: -6px;
    background: var(--topbar-coin-bg);
    box-shadow: 0 0 0 3px #fff6ec;
}

.puzzle-unlock-seal svg {
    width: 14px;
    height: 14px;
    stroke-width: 2.4;
}

.puzzle-unlock-title {
    margin: 0 0 6px;
    font-size: 20px;
    font-weight: 800;
    line-height: 1.25;
    color: var(--text-primary);
}

.puzzle-unlock-meta {
    margin: 0 0 18px;
    font-size: 14px;
    font-weight: 600;
    line-height: 1.35;
    color: var(--text-muted);
}

/* Escaped, not a literal middle dot. components.css declares no @charset, so a
 * raw UTF-8 byte pair here gets read as Latin-1 and prints "Â·" - which is what
 * the meta line under the title was showing. The surrounding spaces are their
 * own strings: whitespace directly after a hex escape is eaten as its
 * terminator, which closed the gap to "Fruits ·Puzzle 6". */
.puzzle-unlock-number::before {
    content: ' ' '\0000B7' ' ';
}

.puzzle-unlock-rows {
    display: grid;
    gap: 10px;
    grid-auto-flow: column;
    justify-content: center;
    align-items: center;
    gap: 12px;
    /* No panel. Once this became a single short line, the well and its hairline
     * were framing almost nothing - the figures read better sitting straight on
     * the card, with the cost chip as the only container. */
    margin: 4px 0 16px;
    padding: 0;
}

.puzzle-unlock-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}

.puzzle-unlock-label {
    font-size: 13px;
    font-weight: 700;
    color: var(--text-muted);
}

.puzzle-unlock-value {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 15px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    /* Balance is context, not the transaction: a quiet figure. */
    color: var(--text-soft);
}

/* Read as a single "price / your funds" figure rather than two labelled rows:
 * the cost takes the gold reward chip used everywhere else, the balance is the
 * bare number beside it. The labels survive as accessible names only - "50 / 10"
 * means nothing read aloud. */
.puzzle-unlock-value--cost {
    gap: 9px;
    padding: 5px 15px 5px 5px;
    border-radius: var(--radius-chip);
    background: var(--stat-gold-soft);
    color: var(--stat-gold);
    font-size: 20px;
}

.puzzle-unlock-value:not(.puzzle-unlock-value--cost) .puzzle-unlock-coin {
    display: none;
}

.puzzle-unlock-value--cost .puzzle-unlock-coin {
    width: 24px;
    height: 24px;
    box-shadow: 0 0 0 3px var(--topbar-coin-ring);
}

.puzzle-unlock-coin {
    width: 18px;
    height: 18px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--topbar-coin-bg);
    box-shadow: 0 0 0 3px var(--topbar-coin-ring);
    color: #FFF8F0;
    box-sizing: content-box;
}

.puzzle-unlock-coin svg {
    width: 100%;
    height: 100%;
}

.puzzle-unlock-insufficient {
    margin: -4px 0 14px;
    font-size: 13px;
    font-weight: 700;
    line-height: 1.35;
    color: #c05621;
}

.puzzle-unlock-insufficient[hidden] {
    display: none !important;
}

.hint-buy-balance {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    margin: 2px 0 14px;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 15px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: var(--text-soft);
}

.hint-buy-balance .puzzle-unlock-coin {
    width: 16px;
    height: 16px;
}

.hint-buy-tiers {
    display: grid;
    gap: 8px;
    width: 100%;
    margin: 0 0 14px;
}

.hint-buy-tier {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    width: 100%;
    padding: 12px 14px;
    /* Gold rather than the neutral teal hairline: these rows are priced in
     * coins, and the border is what ties them to the coin glyph inside them.
     * Every tier gets it, affordable or not - :disabled already recedes the
     * whole row via opacity, so the border does not need to carry that too. */
    border: 1.5px solid #EFC64A;
    border-radius: 12px;
    background: #fff;
    color: var(--text-primary);
    font: inherit;
    cursor: pointer;
    box-shadow: 0 1px 0 rgba(40, 74, 81, 0.04);
    transition: border-color 0.15s ease, background 0.15s ease, transform 0.12s ease;
}

/* Hover deepens toward orange instead of lightening. Brightening a warm tint
 * clips red at 255 and slides the hue to cream - the same trap the secondary
 * button hit - so both the border and the wash step down in value. */
.hint-buy-tier:hover:not(:disabled) {
    border-color: #D89A26;
    background: #FFF6E3;
}

.hint-buy-tier:active:not(:disabled) {
    transform: scale(0.98);
}

.hint-buy-tier:disabled {
    opacity: 0.42;
    cursor: not-allowed;
}

.hint-buy-tier-label {
    font-size: 14px;
    font-weight: 700;
    color: var(--text-primary);
}

.hint-buy-tier-cost {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 14px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: var(--text-primary);
}

.hint-buy-tier-cost .puzzle-unlock-coin {
    width: 14px;
    height: 14px;
}

.puzzle-unlock-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
}

/* Unlock is the only button left in the unlock modal now that its Cancel is a
 * corner X, so stretch it rather than leaving it stranded in half a two-column
 * grid. Hint buy has no actions row at all any more: its packs are the actions
 * and its Cancel became a corner X on the same reasoning. */
#puzzle-unlock-modal .puzzle-unlock-actions {
    grid-template-columns: 1fr;
}

#push-enabled-modal .puzzle-unlock-actions {
    grid-template-columns: 1fr 1fr;
}

@media (prefers-reduced-motion: reduce) {
    .puzzle-unlock-modal,
    .puzzle-unlock-card,
    .apple-name-modal,
    .apple-name-card {
        transition: none;
    }
}

/* Apple name choice modal */
.apple-name-modal {
    position: absolute;
    inset: 0;
    z-index: 125;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.25s ease, visibility 0s linear 0.25s;
}

#apple-name-modal {
    z-index: 130;
}

#welcome-name-modal {
    z-index: 125;
}

.apple-name-modal.show {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity 0.25s ease, visibility 0s;
}

.apple-name-backdrop {
    position: absolute;
    inset: 0;
    background: var(--overlay-scrim);
    backdrop-filter: blur(6px);
}

/* Was --bg-surface-strong (92% white) + backdrop-filter. Over the teal scrim a
 * translucent card pulls the teal through, which is why this read cool grey
 * while every other surface in the app is warm. Opaque cream — the same ground
 * as .puzzle-unlock-card and the solved overlay, so the modals match. */
/* Symmetric inset on the title below, so the centred text cannot run under the
 * corner close. Both sides, or the title stops being centred in the card. */
.auth-form-card .apple-name-title,
.guest-welcome-card .apple-name-title,
.welcome-name-card .apple-name-title {
    padding-left: 30px;
    padding-right: 30px;
}

.apple-name-card {
    position: relative;
    width: 100%;
    max-width: 340px;
    padding: 26px 22px 22px;
    border-radius: var(--radius-card);
    background: #fff6ec;
    box-shadow: 0 20px 50px rgba(120, 90, 60, 0.22);
    transform: scale(0.92);
    transition: transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.apple-name-modal.show .apple-name-card {
    transform: scale(1);
}

/* Was a 52px solid-black disc with a black drop shadow: the highest-contrast
 * object in the modal, out-shouting its own title, and black is not in the
 * palette. What this modal confirms is a SUCCESS, so the well takes the teal of
 * "the given" (§2.1) and a check; Apple identifies the provider and drops to
 * the corner seal, where the mark stays solid black as its own guidelines
 * require. Structurally identical to .puzzle-unlock-icon. */
.apple-name-icon {
    position: relative;
    width: 60px;
    height: 60px;
    margin: 0 auto 12px;
    border-radius: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #000;
    color: #fff;
}

/* The glyph gets its own span: renderIcons() assigns innerHTML to every
 * [data-icon] node, so a well carrying data-icon itself wipes out the seal on
 * every render. Same arrangement as the achievement cards. */
.apple-name-glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
}

.apple-name-glyph svg {
    width: 100%;
    height: 100%;
    fill: currentColor;
    stroke: none;
}

.apple-name-icon.is-facebook {
    background: #1877f2;
    color: #fff;
}

.apple-name-icon.is-google {
    background: #fff;
    color: #1f1f1f;
    border: 1px solid #747775;
}

.apple-name-icon.is-google .apple-name-glyph svg {
    fill: none;
}

.apple-name-icon.is-google .apple-name-glyph svg path:nth-child(1) { fill: #EA4335; }
.apple-name-icon.is-google .apple-name-glyph svg path:nth-child(2) { fill: #4285F4; }
.apple-name-icon.is-google .apple-name-glyph svg path:nth-child(3) { fill: #FBBC05; }
.apple-name-icon.is-google .apple-name-glyph svg path:nth-child(4) { fill: #34A853; }

/* .icon-seal, scaled for this 60px well, in --accent-2 rather than the stock
 * teal: this modal runs one hue throughout. §2.1 warns against ALTERNATING the
 * two hues for rhythm, so a single-hue modal is the more consistent reading -
 * and everything here is a choice, which is orange's job. White check on
 * --accent-2 measures 3.42:1, over the 3:1 required of a graphical object. */
.apple-name-seal {
    width: 24px;
    height: 24px;
    left: -6px;
    bottom: -6px;
    background: var(--accent-2);
    box-shadow: 0 0 0 3px #fff6ec;
}

.apple-name-seal svg {
    width: 14px;
    height: 14px;
}

.apple-name-title {
    margin: 0 0 6px;
    font-size: 20px;
    font-weight: 800;
    line-height: 1.25;
    text-align: center;
    color: var(--text-primary);
}

.apple-name-subtitle {
    margin: 0 0 16px;
    font-size: 14px;
    font-weight: 600;
    line-height: 1.4;
    text-align: center;
    color: var(--text-muted);
}

.apple-name-options {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

/* §1.3 — nothing outlines. The 2px border becomes the inset hairline the stat
 * tiles and achievement cards use (§3). Padding grows by the 2px the border
 * used to occupy so the content sits where it did. */
.apple-name-option {
    display: flex;
    align-items: flex-start;
    gap: 14px;
    padding: 16px 18px;
    border-radius: 16px;
    border: none;
    background: rgba(255, 255, 255, 0.62);
    box-shadow: inset 0 0 0 1px rgba(40, 74, 81, 0.06);
    cursor: pointer;
    transition:
        background 0.18s ease,
        box-shadow 0.18s ease,
        transform 0.12s ease;
}

.apple-name-option:active:not(.is-disabled) {
    transform: scale(0.992);
}

/* Orange is the thing you opt into (§2.1), so selection is correctly accent.
 * The ring is inset, so a selected row still gains no border. */
.apple-name-option:has(input:checked) {
    background: var(--accent-soft);
    box-shadow: none;
}

/* opacity + grayscale on the whole row also dimmed the "already taken" note —
 * the one thing in that row that has to stay readable. Dim the choice, not the
 * reason it is unavailable. */
.apple-name-option.is-disabled {
    cursor: not-allowed;
    background: transparent;
    box-shadow: inset 0 0 0 1px rgba(40, 74, 81, 0.05);
}

.apple-name-option.is-disabled .apple-name-option-label {
    opacity: 0.5;
}

/* The chip is exempt from the dimming: "This name is already taken" is about
 * this exact name, so it has to stay readable. It drops the teal instead -
 * an identity you cannot take is not shown as one - and keeps full ink.
 * Fading it with the row measured 2.2:1; this is 11.5:1. */
.apple-name-option.is-disabled .apple-name-use-value {
    background: rgba(40, 74, 81, 0.08);
    color: var(--text-primary);
}

.apple-name-option.is-disabled .apple-name-radio-mark {
    opacity: 0.4;
}

.apple-name-radio-wrap {
    position: relative;
    flex-shrink: 0;
    width: 26px;
    height: 26px;
    margin-top: 1px;
}

.apple-name-radio-input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    opacity: 0;
    cursor: pointer;
    z-index: 2;
}

/* The unchecked ring was --text-muted x 28% — about 0.17 alpha, barely a ring
 * at all. At 45% it reads as an empty control without shouting. */
.apple-name-radio-mark {
    position: relative;
    display: block;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    border: 2.5px solid color-mix(in srgb, var(--text-muted) 45%, transparent);
    background: #fff;
    transition:
        border-color 0.18s ease,
        background 0.18s ease,
        box-shadow 0.18s ease,
        transform 0.12s ease;
    pointer-events: none;
}

.apple-name-option:has(input:checked) .apple-name-radio-mark {
    border-color: var(--accent);
    background: #fff;
}

.apple-name-option:has(input:checked) .apple-name-radio-mark::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: linear-gradient(145deg, var(--accent-light), var(--accent-2));
    transform: translate(-50%, -50%) scale(1);
}

.apple-name-option-body {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
    flex: 1;
    padding-top: 2px;
}

.apple-name-option-label {
    font-size: 15px;
    font-weight: 800;
    line-height: 1.25;
    color: var(--text-primary);
    letter-spacing: -0.01em;
}

.apple-name-option-label--apple {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
}

.apple-name-use-prefix {
    flex-shrink: 0;
}

/* The name is the most important content in the modal. White failed on both
 * grounds: on the selected peach row it read as a hole punched in the fill, and
 * on an unselected row it was white on near-white.
 *
 * --accent-light, solid, so the chip renders identically on the peach selected
 * row and the near-white unselected one - the soft-fill reward-chip pattern
 * would go muddy over the peach. Ink is --text-primary at 6.90:1; white on
 * --accent-light would be 1.97:1, so this chip must carry dark text. */
.apple-name-use-value,
.apple-name-option-value {
    display: inline-flex;
    max-width: 100%;
    padding: 6px 13px;
    border-radius: var(--radius-chip);
    font-size: 14px;
    font-weight: 700;
    line-height: 1.2;
    color: var(--text-primary);
    background: var(--accent-light);
    border: none;
    word-break: break-word;
}

/* Only the stacked variant self-aligns — the inline "Use <name>" chip has to
 * stay centred against its prefix. */
.apple-name-option-value {
    align-self: flex-start;
}

.apple-name-option-note {
    font-size: 12.5px;
    color: var(--error);
    font-weight: 700;
    line-height: 1.35;
}

/* The create page's recessed-field treatment: a field you type INTO should read
 * as a well in the card, not as a lighter panel floating on top of it. */
.apple-name-input {
    width: 100%;
    min-height: 46px;
    margin-top: 2px;
    padding: 12px 14px;
    border-radius: var(--radius-btn);
    border: none;
    background: var(--bg-deep);
    color: var(--text-primary);
    font: inherit;
    font-size: 15px;
    font-weight: 700;
    letter-spacing: -0.01em;
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 30%, transparent);
    transition:
        background 0.18s ease,
        box-shadow 0.18s ease;
}

/* Was opacity 0.55 stacked on top of an already 43%-alpha placeholder — about
 * 1.5:1, which is why the unselected field looked empty rather than optional.
 * The inactive field is now quieter by hairline alone; its text keeps contrast.
 * border-style: dashed is gone; nothing else in the app uses a dashed edge. */
.apple-name-option--custom:not(:has(input:checked)) .apple-name-input {
    pointer-events: none;
    background: color-mix(in srgb, var(--bg-deep) 70%, transparent);
    box-shadow: inset 0 0 0 1px rgba(40, 74, 81, 0.08);
}

/* --text-soft, measured at 4.55:1 on the white focused field — --text-muted
 * only reached 3.88:1. Still clearly lighter than a real value (--text-primary
 * measures 10.9:1), so an empty field never reads as a filled one. opacity:1
 * because Firefox dims placeholders by default, which is how the original
 * 1.5:1 stack got there in the first place. */
.apple-name-input::placeholder {
    color: var(--text-soft);
    font-weight: 600;
    opacity: 1;
}

/* Selected and focused both lift the field to white, so the active input is the
 * brightest thing on the card — the inverse of its resting state. */
.apple-name-option--custom:has(input:checked) .apple-name-input {
    background: #fff;
    box-shadow:
        inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent),
        0 0 0 3px var(--accent-soft);
}

.apple-name-input:focus {
    outline: none;
    background: #fff;
    box-shadow:
        inset 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent),
        0 0 0 3px var(--accent-soft);
}

.apple-name-error {
    margin: 12px 0 0;
    font-size: 13px;
    font-weight: 700;
    color: var(--error);
    text-align: center;
}

.apple-name-actions {
    margin-top: 16px;
}

.welcome-name-apple-wrap {
    margin-top: 0;
    padding-top: 0;
    border-top: none;
}

/* border-top removed: the separation is now .auth-divider, which says *why*
 * the rule is there ("or") instead of leaving an unlabelled line. */
.welcome-name-auth-actions {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-top: 16px;
}

.welcome-name-auth-actions .welcome-name-apple-wrap {
    margin-top: 0;
}

.welcome-name-auth-actions .social-btn {
    width: 100%;
}

.welcome-name-apple-wrap .social-btn {
    width: 100%;
}

.welcome-name-card .apple-name-title {
    margin-top: 0;
}

/* Delete account confirmation modal */
.delete-account-modal {
    position: absolute;
    inset: 0;
    z-index: 126;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.25s ease, visibility 0s linear 0.25s;
}

.delete-account-modal.show {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity 0.25s ease, visibility 0s;
}

.delete-account-backdrop {
    position: absolute;
    inset: 0;
    background: var(--overlay-scrim);
    backdrop-filter: blur(6px);
}

.delete-account-card {
    position: relative;
    width: 100%;
    max-width: 360px;
    padding: 24px 20px 20px;
    border-radius: var(--radius-card);
    background: var(--bg-surface-strong);
    backdrop-filter: blur(var(--glass-blur));
    box-shadow: var(--shadow-card);
    transform: scale(0.92);
    transition: transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.delete-account-modal.show .delete-account-card {
    transform: scale(1);
}

/* An icon well like every other one in the app - soft tint, hue glyph, wave
 * band - built from --error rather than the gradient of #e74c3c/#c0392b it
 * carried, which predated the token and matched nothing else. Same recipe
 * .btn.danger already uses for an error surface. */
.delete-account-icon {
    width: 60px;
    height: 60px;
    margin: 0 auto 14px;
    border-radius: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: color-mix(in srgb, var(--error) 16%, var(--bg-surface));
    color: var(--error);
}

/* A character standing in for a glyph, as .level-up-overlay-level does with the
 * level number: mono, so it sits on the same optical centre as a real icon. */
.delete-account-glyph {
    font-family: var(--font-grid);
    font-size: 26px;
    font-weight: 800;
    line-height: 1;
    color: currentColor;
}

.delete-account-title {
    margin: 0 0 8px;
    font-size: 20px;
    font-weight: 800;
    line-height: 1.25;
    text-align: center;
    color: var(--text-primary);
}

.delete-account-lead {
    margin: 0 0 14px;
    font-size: 14px;
    font-weight: 600;
    line-height: 1.45;
    text-align: center;
    color: var(--text-muted);
}

/* Same error surface as the well and .btn.danger, off one token instead of two
 * more hardcoded reds. --radius-card so it is a panel like any other. */
.delete-account-list {
    margin: 0 0 14px;
    padding: 12px 14px;
    border-radius: var(--radius-card);
    background: color-mix(in srgb, var(--error) 8%, var(--bg-surface));
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--error) 20%, transparent);
    color: var(--text-primary);
    font-size: 13px;
    font-weight: 600;
    line-height: 1.5;
    list-style: none;
}

/* The browser's bullet was the only list marker in the app. This is a flex row
 * with its own dot, so the marker takes the error hue and wrapped lines align
 * under the text rather than under the bullet. */
.delete-account-list li {
    display: flex;
    align-items: baseline;
    gap: 9px;
}

.delete-account-list li::before {
    content: '';
    flex: 0 0 5px;
    width: 5px;
    height: 5px;
    margin-top: 1px;
    border-radius: 50%;
    background: var(--error);
}

.delete-account-list li + li {
    margin-top: 7px;
}

.delete-account-note {
    margin: 0 0 16px;
    font-size: 13px;
    line-height: 1.45;
    text-align: center;
    color: var(--text-muted);
}

/* Not 1fr 1fr: "Delete permanently" wrapped onto two lines while Cancel sat on
 * one. The label is deliberately explicit for a destructive action, so the
 * column gives way instead of the wording. Measured at 375px: the row has 277px,
 * the label needs 148px and "Endgultig loschen" 134px, so 1.8fr plus the tighter
 * inline padding below clears both with room. */
.delete-account-actions {
    display: grid;
    grid-template-columns: 1fr 1.8fr;
    gap: 10px;
}

/* The stock 22px side padding is sized for a button that owns its width. Here
 * the grid sets the width, so the padding is only stealing room from the label. */
.delete-account-actions .btn {
    padding-left: 14px;
    padding-right: 14px;
}

@media (prefers-reduced-motion: reduce) {
    .delete-account-modal,
    .delete-account-card {
        transition: none;
    }
}

/* --- Bottom navigation --- */
.bottom-nav {
    --bottom-nav-wave-height: 32px;
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 30;
    display: none;
    align-items: stretch;
    justify-content: space-around;
    gap: 2px;
    padding: 0 8px calc(6px + var(--safe-bottom));
    overflow: visible;
    /* Opaque, not --bg-surface-strong. The crest above stacks that token twice
     * to reach ~99%, so a 92% bar read visibly warmer than the wave it joins -
     * and that mismatch is what looked like a border along the top edge. Both
     * are the same solid white now, so there is nothing to see where they meet.
     * The backdrop blur went with it: nothing shows through an opaque surface,
     * so it was buying a compositing layer and no pixels. */
    background: var(--bg-surface-solid);
}

/* The nav's top edge IS the wave, rather than a straight edge with a wave drawn
 * under it. ::before is the surface itself, in --bg-surface-strong, extended
 * above the bar and masked to a crest - so the chrome rises out of the shell on
 * a curve. Its shadow is a drop-shadow, not a box-shadow, so it follows that
 * curve instead of tracing the rectangle underneath.
 *
 * The mask path fills toward the bottom of its viewBox, so the crest's whole
 * lower edge joins the bar and only the top contour is visible. Nothing is
 * drawn inside the bar - which is why the wave can never touch the active pill,
 * whatever the top padding is. */
.bottom-nav::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: calc(-1 * var(--bottom-nav-wave-height));
    height: var(--bottom-nav-wave-height);
    z-index: 0;
    /* ONE crest, never two - two contours on different phases cross, and a
     * crossing contour reads as a rendering error rather than as depth. (The
     * hero waves layer two paths, but on the same phase, offset - they never
     * intersect.) Plain white: no tint layer, so the crest reads as the chrome
     * surface rising out of the shell rather than as a coloured band.
     *
     * One solid layer. This used to stack --bg-surface-strong twice to fake its
     * way from 92% to ~99% opacity, because the crest carries no backdrop-filter
     * (mask + backdrop-filter is unreliable in WebKit) and a card scrolling
     * underneath showed through. With a genuinely opaque token there is nothing
     * left to fake, and the crest matches the bar exactly. */
    background-image: linear-gradient(var(--bg-surface-solid), var(--bg-surface-solid));
    filter: drop-shadow(0 -3px 10px rgba(120, 90, 60, 0.10));
    pointer-events: none;
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 100' preserveAspectRatio='none'%3E%3Cpath d='M0,42 C76,8 150,16 226,46 C298,74 352,66 400,32 L400,100 L0,100 Z' fill='%23000'/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 100' preserveAspectRatio='none'%3E%3Cpath d='M0,42 C76,8 150,16 226,46 C298,74 352,66 400,32 L400,100 L0,100 Z' fill='%23000'/%3E%3C/svg%3E");
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
}

.app-shell.nav-visible .bottom-nav {
    display: flex;
}

.app-shell.nav-visible .screen.active .screen-scroll {
    padding-bottom: calc(104px + var(--safe-bottom));
}

.bottom-nav-item {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    position: relative;
    z-index: 1;
    min-height: 52px;
    padding: 6px 4px;
    border: 0;
    background: transparent;
    border-radius: var(--radius-btn);
    color: var(--grid-letter-on-highlight);
    font-family: var(--font-ui);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.01em;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color 0.18s ease, background 0.18s ease, transform 0.12s ease;
}

.bottom-nav-item:active {
    transform: scale(0.94);
}

/* The UA ring picks up --accent and draws a hard 1.5px rectangle that fights
 * the pill. Inset it so it traces the pill instead of boxing it. */
.bottom-nav-item:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

.bottom-nav-icon {
    display: inline-flex;
    width: 24px;
    height: 24px;
}

.bottom-nav-icon svg {
    width: 100%;
    height: 100%;
    stroke: currentColor;
    fill: none;
}

.bottom-nav-item[data-nav-key="leaderboard"] .bottom-nav-icon svg {
    stroke: none;
    fill: currentColor;
}

.bottom-nav-label {
    line-height: 1;
}

.app-shell[data-nav="home"] .bottom-nav-item[data-nav-key="home"],
.app-shell[data-nav="achievements"] .bottom-nav-item[data-nav-key="achievements"],
.app-shell[data-nav="leaderboard"] .bottom-nav-item[data-nav-key="leaderboard"],
.app-shell[data-nav="statistics"] .bottom-nav-item[data-nav-key="statistics"] {
    /* --accent-2, not --accent: the active pill sits on --accent-soft, which is
     * that same orange at 16%, so the lighter tone left label and icon short of
     * contrast against their own background. */
    color: var(--accent-2);
    background: var(--accent-soft);
}

@media (prefers-reduced-motion: reduce) {
    .bottom-nav-item {
        transition: none;
    }
}

/* --- iOS browser App Store banner (below bottom nav) --- */
:root {
    --ios-app-banner-height: 0px;
}

html.ios-browser-banner {
    --ios-app-banner-height: calc(64px + var(--safe-bottom));
}

/* Same bottom slot and the same --ios-app-banner-height reservation as
 * .ios-app-banner - only one of the two is ever visible, so they can share the
 * layout offset html.ios-browser-banner already drives. */
.app-open-banner {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 29;
    display: flex;
    align-items: center;
    justify-content: center;
    height: var(--ios-app-banner-height);
    padding: 6px 10px calc(var(--safe-bottom) - 10px);
    background: #fff;
    border-top: 1px solid rgba(40, 74, 81, 0.08);
    box-shadow: 0 -4px 16px rgba(40, 74, 81, 0.08);
}

.app-open-banner.hidden {
    display: none;
}

.app-open-banner-main {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
    /* Clears the absolutely positioned close button - the same 56px reservation
     * the App Store banner uses. */
    max-width: calc(100% - 56px);
}

.app-open-banner-icon {
    flex: 0 0 auto;
    width: 36px;
    height: 36px;
    border-radius: 9px;
}

.app-open-banner-text {
    min-width: 0;
    overflow: hidden;
    color: var(--text-strong);
    font-size: 14px;
    font-weight: 700;
    line-height: 1.25;
}

.app-open-banner-cta {
    flex: 0 0 auto;
    /* .btn-sm already clears 44px, so no height is declared here - see the note
     * in buttons.css about contexts re-declaring button heights. */
    padding-inline: 18px;
}

.app-open-banner-close {
    position: absolute;
    top: 6px;
    right: 8px;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: rgba(40, 74, 81, 0.08);
    color: var(--text-muted);
    font-family: inherit;
    font-size: 18px;
    font-weight: 400;
    line-height: 1;
}

/* Grows the 24px dot to the 44px minimum hit area without moving it. No other
 * control sits within 44px of this centre, so the boxes cannot overlap. */
.app-open-banner-close::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}

.ios-app-banner {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 29;
    display: none;
    align-items: center;
    gap: 6px;
    height: var(--ios-app-banner-height);
    padding: 8px 10px calc(8px + var(--safe-bottom));
    /* Brand teal, same hue as daily hero / --fill-dark. White type and the
     * white App Store badge sit on it; the Play badge is a self-contained chip. */
    background: var(--fill-dark);
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    box-shadow: 0 -4px 16px rgba(40, 74, 81, 0.18);
}

.ios-app-banner-main {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    flex: 1;
}

.ios-app-banner-icon {
    flex: 0 0 auto;
    width: 40px;
    height: 40px;
    border-radius: 10px;
    object-fit: cover;
}

.ios-app-banner-copy {
    flex: 1 1 auto;
    min-width: 0;
}

.ios-app-banner-title,
.ios-app-banner-sub {
    display: block;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.ios-app-banner-title {
    color: var(--text-on-accent);
    font-size: 14px;
    font-weight: 800;
    line-height: 1.2;
}

.ios-app-banner-sub {
    margin-top: 1px;
    color: rgba(255, 255, 255, 0.78);
    font-size: 11px;
    font-weight: 600;
    line-height: 1.25;
}

.ios-app-banner-close {
    position: relative;
    z-index: 1;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--text-on-accent);
    font-family: inherit;
    font-size: 20px;
    font-weight: 400;
    line-height: 1;
    cursor: pointer;
}

.ios-app-banner-close::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}

.ios-app-banner-close:active {
    opacity: 0.7;
}

html.ios-browser-banner .ios-app-banner {
    display: flex;
}

html.ios-browser-banner .bottom-nav {
    bottom: var(--ios-app-banner-height);
    padding-bottom: 6px;
}

html.ios-browser-banner .app-shell.nav-visible .screen.active .screen-scroll {
    padding-bottom: calc(104px + var(--safe-bottom) + var(--ios-app-banner-height));
}

html.ios-browser-banner .screen {
    bottom: calc(var(--safe-bottom) + var(--ios-app-banner-height));
}

html.ios-browser-banner .app-shell.topbar-visible .screen {
    top: calc(var(--safe-top) + var(--global-topbar-height));
    bottom: calc(var(--safe-bottom) + var(--ios-app-banner-height));
}

#screen-loading.active ~ .ios-app-banner,
#screen-offline.active ~ .ios-app-banner {
    display: none !important;
}

/* Official store artwork, unmodified. Sized by height so App Store and Play
 * keep their native aspect; ~30px leaves room for title + subtitle. */
.ios-app-banner-badge {
    flex: 0 0 auto;
    display: block;
    line-height: 0;
    margin: 0;
}

.ios-app-banner-badge img {
    display: block;
    width: auto;
    height: 30px;
    max-width: 110px;
}

.ios-app-banner-badge:active {
    opacity: 0.8;
}

/* --- Create: board preview and options disclosure --- */
/* The preview is the answer to "what am I building?" - the page used to make you
 * configure a board you never saw. It shows size and fullness only; see
 * updateCustomBoardPreview() for why it must never show placement. */
/* The board is the subject of this screen, so it carries the hue - teal, the
 * "this is the thing" register (§2.1). Every other screen opens with a coloured
 * block; this page had none, which is what made it read as white on white. */
.custom-preview {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-top: 14px;
    padding: 14px;
    border-radius: var(--radius-card);
    background: var(--stat-teal-soft);
    color: var(--stat-teal);
}

.custom-preview-row {
    display: flex;
    align-items: center;
    gap: 16px;
}

.custom-preview-grid {
    display: grid;
    grid-template-columns: repeat(var(--pv-w, 8), 1fr);
    gap: 2px;
    width: 72px;
    aspect-ratio: var(--pv-w, 8) / var(--pv-h, 11);
    flex-shrink: 0;
}

/* Uniform white cells on the teal panel - shape only, never placement. */
.pv-cell {
    border-radius: 1.5px;
    background: rgba(255, 255, 255, 0.72);
}

.custom-preview-controls {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 8px;
}

.custom-preview-size-label {
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: color-mix(in srgb, var(--stat-teal) 72%, transparent);
}

.custom-preview-size {
    margin: 0;
    font-family: var(--font-grid);
    font-size: 15px;
    font-weight: 800;
    color: var(--stat-teal);
}

.custom-preview-fit {
    margin: 0;
    font-size: 13px;
    line-height: 1.35;
    color: var(--text-soft);
}

/* A word that cannot physically be placed is a blocking problem, so it takes the
 * muted rose - never a harsh red (§2.8). Tight is a warning, not a failure. */
.custom-preview-fit[data-state="error"] {
    color: var(--error);
    font-weight: 700;
}

.custom-preview-fit[data-state="tight"] {
    color: var(--warning);
}

.custom-preview-fit[data-state="checking"] {
    color: var(--text-muted);
    font-style: italic;
}

.custom-preview-suggest {
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px solid color-mix(in srgb, var(--text-muted) 18%, transparent);
}

.custom-preview-suggest-label {
    margin: 0 0 8px;
    font-size: 12px;
    line-height: 1.35;
    color: var(--text-muted);
}

.custom-preview-suggest-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 8px;
}

.custom-preview-suggest-chips .chip {
    cursor: pointer;
}

.custom-preview-suggest-btn {
    width: 100%;
    min-height: 40px;
    padding: 10px 14px;
    font-size: 14px;
}

/* The panel itself takes the state colour, so a problem is visible from across
 * the screen rather than only in the sentence. */
.custom-preview[data-state="error"] {
    background: color-mix(in srgb, var(--error) 14%, transparent);
}

.custom-preview[data-state="tight"] {
    background: color-mix(in srgb, var(--warning) 16%, transparent);
}

/* Type, directions and grid all have sensible defaults, so they start collapsed:
 * most people only want a title and some words. */
.custom-options {
    margin-top: 14px;
}

.custom-options-summary {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 14px 10px 10px;
    border-radius: var(--radius-btn);
    background: var(--bg-deep);
    color: var(--text-primary);
    font-size: 14px;
    font-weight: 700;
    cursor: pointer;
    list-style: none;
}

.custom-options-label {
    flex: 1;
}

/* The same tinted well the achievement and stat rows use, so this bar belongs to
 * that family instead of being a blank white strip. */
.custom-options-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    border-radius: 10px;
    background: var(--accent-soft);
    color: var(--accent-2);
}

.custom-options-icon svg {
    width: 18px;
    height: 18px;
    stroke: currentColor;
    fill: none;
}

.custom-options-summary::-webkit-details-marker {
    display: none;
}

.custom-options-chevron {
    display: inline-flex;
    width: 18px;
    height: 18px;
    color: var(--text-muted);
    transition: transform 0.18s ease;
}

.custom-options-chevron svg {
    width: 100%;
    height: 100%;
    stroke: currentColor;
    fill: none;
}

.custom-options[open] .custom-options-chevron {
    transform: rotate(90deg);
}

/* All eight directions in one row; buttons flex to fit narrow cards. */
.direction-picker {
    width: 100%;
}

.direction-row {
    display: flex;
    flex-wrap: nowrap;
    gap: 6px;
    width: 100%;
}

#custom-direction-picker .direction-option {
    flex: 1 1 0;
    min-width: 0;
    width: auto;
    height: auto;
    aspect-ratio: 1;
    max-width: 44px;
    padding: 5px;
}

#custom-direction-picker .direction-option .direction-svg {
    width: min(22px, 100%);
    height: min(22px, 100%);
}

/* --- Grid steppers, inside the preview --- */
/* These used to live in the collapsed options while the preview above told you
 * to "enlarge the board" - the advice and the control were in two places, one of
 * them hidden. Now the panel that shows the board is the panel that sets it. */
.custom-preview-size {
    display: flex;
    align-items: center;
    gap: 6px;
}

/* The two pills split the row rather than hugging their content. They used to
 * be width: fit-content, which left ~45px of the panel unused at 390px - and
 * still overflowed at 320px, because fit-content cannot shrink either. Sharing
 * the row fixes both ends: the buttons take the slack when there is some, and
 * give it back when there is not. */
.pv-step {
    display: inline-flex;
    align-items: center;
    justify-content: space-between;
    gap: 0;
    flex: 1 1 0;
    min-width: 0;
    padding: 2px;
    border-radius: var(--radius-chip);
    background: rgba(255, 255, 255, 0.55);
}

.pv-times {
    font-family: var(--font-grid);
    font-size: 13px;
    font-weight: 800;
    color: var(--stat-teal);
    opacity: 0.6;
}

/* Width is flexible, not fixed: the panel it sits in is sized by the board
 * preview beside it, so the space available changes with the viewport. The
 * buttons absorb it, capped so they stay a sane shape on wide screens and
 * floored so a narrow one degrades rather than overflowing. Radius is a pill,
 * not 50% - at a flexible width, 50% would draw an ellipse. */
.custom-preview-size .size-step-btn {
    position: relative;
    flex: 1 1 0;
    width: auto;
    min-width: 22px;
    max-width: 46px;
    height: 36px;
    border: none;
    border-radius: var(--radius-chip);
    background: transparent;
    color: var(--stat-teal);
    font-size: 19px;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    transition: background 0.15s ease, transform 0.1s ease;
}

/* The panel is width-constrained by the board preview beside it, so the visible
 * button often cannot reach the 44px minimum touch target (iOS HIG) on its own.
 * This extends the hit area past the paint without moving anything.
 *
 * The horizontal growth is a fixed inset, NOT a fixed 44px width. Once the
 * buttons became flexible their spacing became flexible too: at 320px their
 * centres are only ~36px apart, so a hard 44px box would have overlapped its
 * neighbour and sent taps near the boundary to the wrong button. 6px a side is
 * the widest expansion that still clears the ~24px minimum edge separation -
 * between the buttons of one pill, and across the gap between the two pills. */
.custom-preview-size .size-step-btn::after {
    content: "";
    position: absolute;
    top: 50%;
    left: -6px;
    right: -6px;
    height: 44px;
    transform: translateY(-50%);
}

.custom-preview-size .size-step-btn:hover:not(:disabled) {
    background: rgba(255, 255, 255, 0.9);
}

.custom-preview-size .size-step-btn:active:not(:disabled) {
    transform: scale(0.9);
}

/* At a bound the control stays in place but stops inviting a press - the board
 * has a real minimum and maximum and silently doing nothing would be worse. */
.custom-preview-size .size-step-btn:disabled {
    opacity: 0.3;
    cursor: not-allowed;
}

.custom-preview-size .size-step-value {
    flex: 0 0 auto;
    min-width: 20px;
    padding: 0 2px;
    text-align: center;
    font-family: var(--font-grid);
    font-size: 14px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: var(--stat-teal);
}

/* --- Word input with the Add button inside the field --- */
/* Scoped to .custom-word-row: Settings uses the same .name-edit-row for its
 * name + Save pair, where a button sitting inside the field would be wrong. */
.custom-word-row {
    position: relative;
    display: block;
}

/* A glyph is the same width in every language, so the field's right padding is
 * a constant again - this replaced a measured --add-w that existed purely
 * because "Add" and "Hinzufügen" are very different widths. */
.custom-word-row .name-input {
    width: 100%;
    padding-right: 52px;
}

.custom-word-row .btn {
    position: absolute;
    top: 50%;
    right: 5px;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    flex: none;
    min-height: 0;
    padding: 0;
    border-radius: 50%;
}

.custom-word-row .btn svg {
    width: 20px;
    height: 20px;
    stroke: currentColor;
    stroke-width: 2.4;
    fill: none;
}

.custom-word-row .btn:active {
    transform: translateY(-50%) scale(0.92);
}

/* --- Cascade time limit --- */
/* The same compact pill as the grid steppers in the preview: it is the same kind
 * of control, so it reads as one. It was the last user of the old 40px-button
 * .size-stepper block, which went unused when Grid size moved into the preview.
 * Cream well + accent hairline matches the inputs on this page rather than the
 * teal the preview steppers sit on. */
.time-stepper {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    padding: 3px;
    border-radius: var(--radius-chip);
    background: var(--bg-deep);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent);
}

.time-stepper .size-step-btn {
    flex: 0 0 28px;
    width: 28px;
    height: 28px;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: var(--accent-2);
    font-size: 18px;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    transition: background 0.15s ease, transform 0.1s ease;
}

.time-stepper .size-step-btn:hover:not(:disabled) {
    background: rgba(255, 255, 255, 0.9);
}

.time-stepper .size-step-btn:active:not(:disabled) {
    transform: scale(0.9);
}

.time-stepper .size-step-btn:disabled {
    opacity: 0.3;
    cursor: not-allowed;
}

/* --font-ui, inherited - not the mono the grid steppers use. tabular-nums stays
 * so the digits do not jitter as the value steps. */
.time-stepper .size-step-value {
    flex: 0 0 auto;
    min-width: 54px;
    text-align: center;
    font-size: 15px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: var(--text-primary);
}

.puzzle-unlock-sep {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 20px;
    font-weight: 700;
    color: var(--text-dim);
}

/* Mono, like the cost beside it - the line is one figure over another, so both
 * halves share a type treatment. */
.puzzle-unlock-value--balance {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 20px;
    color: var(--text-primary);
}

/* Below the cost: the number itself turns rose, so affordability is readable
 * where people actually look. Muted rose, never a harsh red (§2.8). */
.puzzle-unlock-value--balance.is-short {
    color: var(--error);
}

/* --- Daily: link out to the rankings page --- */
/* Reuses .my-puzzle-tile so the row matches the custom-puzzle rows exactly.
 * The `leaderboard` glyph is filled via fill="currentColor" on the <svg> itself,
 * so the shared .my-puzzle-icon rule's `fill: none` would blank it — the same
 * inversion .mode-card[data-mode="easy"] already needs. */
.daily-lb-link {
    margin-top: 18px;
    /* Solid, not the .my-puzzle-tile default of --bg-surface (72% white): at
     * that alpha the cream ground reads straight through and the card looks
     * tinted rather than white. */
    background: var(--bg-surface-solid);
}

.daily-lb-link .my-puzzle-icon svg {
    stroke: none;
    fill: var(--accent);
}

/* --- Daily leaderboard: day picker --- */
/* The heading is the control: it names the day you are looking at and opens a
 * short menu of the days that actually have a puzzle. A 7-wide strip of chips
 * was a lot of permanent chrome for something you change rarely. */
.daily-day-select {
    position: relative;
}

.daily-day-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin: 18px 4px 10px;
    padding: 0;
    border: none;
    background: none;
    font-family: inherit;
    cursor: pointer;
}

.daily-day-toggle .section-title {
    margin: 0;
}

.daily-day-caret {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 13px;
    height: 13px;
    color: var(--text-muted);
    /* chevron points right at rest; rotate it to a caret. */
    transform: rotate(90deg);
    transition: transform 0.16s ease;
}

.daily-day-caret svg {
    width: 100%;
    height: 100%;
    stroke: currentColor;
    fill: none;
}

.daily-day-toggle[aria-expanded="true"] .daily-day-caret {
    transform: rotate(-90deg);
}

/* Single day of history: nothing to pick, so it reads as a plain heading. */
.daily-day-select--single .daily-day-toggle {
    cursor: default;
}

.daily-day-select--single .daily-day-caret {
    display: none;
}

.daily-day-menu {
    position: absolute;
    z-index: 20;
    top: 100%;
    left: 4px;
    min-width: 190px;
    margin-top: -6px;
    padding: 6px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    border-radius: var(--radius-card);
    /* Solid, not --bg-surface: that token is rgba(255,255,255,0.72), which is
     * right for cards resting on the page but lets the podium read straight
     * through a menu floating over it. */
    background: #fff;
    box-shadow: 0 10px 24px rgba(120, 90, 60, 0.20);
}

.daily-day-menu[hidden] {
    display: none;
}

.daily-day-option {
    display: block;
    width: 100%;
    padding: 9px 12px;
    border: none;
    border-radius: var(--radius-chip);
    background: none;
    color: var(--text-primary);
    font-family: inherit;
    font-size: 14px;
    font-weight: 700;
    text-align: left;
    white-space: nowrap;
    cursor: pointer;
}

.daily-day-option.is-active {
    background: var(--accent-soft);
    color: var(--accent-2);
}

/* --- Guest welcome modal --- */
.guest-welcome-actions {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-top: 18px;
}

.auth-btn-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
}

.auth-btn-row[hidden],
.auth-btn-row .btn[hidden] {
    display: none !important;
}

.auth-btn-row--single {
    grid-template-columns: 1fr;
}

/* When the pair collapses to one - only Log in is offered, or only Register -
 * the survivor is the sole account action in the dialog, so it stops being the
 * recessive half of a pair that is no longer there and takes the primary
 * treatment. Same declarations as .btn-primary, referencing the same tokens.
 *
 * Scoped to .btn-dark-secondary on purpose: .btn-dark is a deliberate choice
 * where it is used, and must keep its own colour even when it stands alone. */
.auth-btn-row--single .btn-dark-secondary {
    background: var(--accent-2);
    color: var(--text-on-accent);
    font-size: 17px;
}

.welcome-name-auth-actions .auth-btn-row {
    width: 100%;
}


/* ── Auth dialogs: one button layout for all of them ───────────────────── */
/* These dialogs legitimately need four or five actions. What made them read as
 * a pile was that nothing said which actions were alternatives to which, and
 * the two kinds of button were different sizes.
 *
 * The order is the industry-standard one, and Apple's: account actions first,
 * a labelled "or", then the identity providers, with Sign in with Apple above
 * the others - Apple asks that it be at least as prominent as any other sign-in
 * option. The way out stays a text link, not a fourth button. */
.auth-actions {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* .social-btn ships at 52px/16px and .btn at 48px/15px. Side by side in one
 * column that 4px mismatch is exactly what breaks the rhythm. Matched here
 * only - Settings uses .social-btn on its own, where nothing sits next to it
 * to disagree with. */
.auth-actions .social-btn {
    min-height: 48px;
    font-size: 15px;
}

.auth-divider {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 4px 0;
    color: var(--text-muted);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.auth-divider::before,
.auth-divider::after {
    content: '';
    flex: 1 1 0;
    height: 1px;
    background: var(--line);
}

/* The divider only earns its place when there is something on BOTH sides of
 * it. The account row and each provider hide independently (guestAuthOptions),
 * so an "or" can otherwise end up above nothing, or below nothing. Both hide
 * mechanisms are in play here - the [hidden] attribute on the row, the .hidden
 * class on the providers - hence both tests. */
.guest-welcome-actions .auth-divider {
    display: none;
}

.guest-welcome-actions:has(.auth-btn-row:not([hidden]):not(.hidden)):has(.social-btn:not([hidden]):not(.hidden)) .auth-divider {
    display: flex;
}

/* The way out: a word, not a third button competing with the two real actions. */
.guest-welcome-skip {
    display: block;
    margin: 16px auto 0;
    padding: 6px 10px;
    border: none;
    background: none;
    color: var(--text-muted);
    font-family: inherit;
    font-size: 14px;
    font-weight: 700;
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
}

/* Native iPad / tablets: home and chrome type scale with the play screen. */
@media (min-width: 600px) and (min-height: 600px) {
    html.native-shell .guest-welcome-skip {
        font-size: 16px;
    }
}
