/* Game Template Styles — Matched to ColorifyMe website design */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&display=swap');

/* ─── Icons: Material Symbols Rounded (self-hosted) ───
   Served from our own origin rather than fonts.gstatic.com: no third-party
   request on a child-directed site, and no dependency on Google being
   reachable. The file is subsetted to the 18 icons actually used here.
   font-display:block matters for icon fonts specifically — with 'swap' the
   browser paints the raw ligature text ("drag_pan", "format_color_fill")
   across every button until the font arrives. */
@font-face {
    font-family: 'Material Symbols Rounded';
    font-style: normal;
    font-weight: 400;
    font-display: block;
    src: url('../fonts/material-symbols-rounded.woff2') format('woff2');
}

.msr {
    font-family: 'Material Symbols Rounded';
    font-weight: normal;
    font-style: normal;
    line-height: 1;
    letter-spacing: normal;
    text-transform: none;
    display: inline-block;
    white-space: nowrap;
    word-wrap: normal;
    direction: ltr;
    font-feature-settings: 'liga';
    -webkit-font-feature-settings: 'liga';
    -webkit-font-smoothing: antialiased;
    user-select: none;
}

:root {
    --ink: #2f2a45;
    --ink-soft: #6b6486;
    --card: #ffffff;
    --line: #ece6fb;
    --accent: #FF4B91;
    --violet: #8b5cf6;
    --radius: 20px;
}

* {
    box-sizing: border-box;
    user-select: none;
}

/* Every control a child taps. `manipulation` keeps panning and pinch-zoom but
   drops double-tap-to-zoom, and with it the ~300ms the browser otherwise waits
   after a tap to see whether a second one is coming. That pause is invisible
   as a cause and unmistakable as a symptom: the button "sometimes" does
   nothing, then fires late. Also kills the grey tap flash on Android. */
button,
.tool-btn,
.view-btn,
.big-btn,
.color-btn,
.pattern-btn,
.sticker-btn,
.gallery-item,
.palette-scroll-arrow,
.close-btn,
.cp-shade {
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}

body {
    margin: 0;
    padding: 0;
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    color: var(--ink);
    background: linear-gradient(160deg, #f4f0ff 0%, #fdf3fb 45%, #eef4ff 100%);
    height: 100vh;
    overflow: hidden;
    overscroll-behavior: none;
}

#app-shell {
    height: 100%;
    display: flex;
    flex-direction: column;
    padding: clamp(6px, 1vh, 14px);
    gap: clamp(6px, 1vh, 12px);
}

#app-row {
    flex: 1;
    display: flex;
    flex-direction: row;
    gap: clamp(6px, 0.8vw, 14px);
    min-height: 0;
}

/* ─── Sidebars ─── */
.sidebar {
    display: flex;
    z-index: 10;
    min-height: 0;
}

/* Save/New Page live inside the sidebar now (below palette+tools) instead of
   a separate full-width footer, so the canvas can use that reclaimed height. */
#right-sidebar {
    width: clamp(280px, 30vw, 400px);
    flex-direction: column;
    gap: clamp(6px, 0.7vw, 12px);
    flex-shrink: 0;
}

#palette-tools-row {
    display: flex;
    flex-direction: row;
    flex: 1;
    min-height: 0;
    gap: clamp(6px, 0.7vw, 12px);
}

#sidebar-actions {
    display: flex;
    flex-direction: row;
    gap: 8px;
    flex-shrink: 0;
}

#sidebar-actions .big-btn {
    flex: 1;
    justify-content: center;
}

.size-lbl {
    font-size: clamp(0.6rem, 0.62vw, 0.72rem);
    font-weight: 800;
    letter-spacing: 0.4px;
    line-height: 1.1;
    text-transform: uppercase;
    color: var(--ink-soft);
    flex-shrink: 0;
}

/* ─── Floating Brush Size ─── */
/* A frosted pill docked to the TOP of the canvas area, deliberately matching
   the view-controls pill at the bottom so the two read as one family framing
   the artwork. Sizing belongs over the canvas for the same reason panning
   does: it is judged against the drawing, so the child watches the picture
   rather than the sidebar while dragging.
   `display` stays none here — setTool() flips it to flex for the tools that
   actually have a size. */
#brush-size-container {
    position: absolute;
    top: clamp(12px, 2vh, 22px);
    left: 50%;
    transform: translateX(-50%);
    z-index: 40;
    display: none;
    flex-direction: row;
    align-items: center;
    gap: 12px;
    max-width: min(92%, 360px);
    padding: 7px 16px 7px 10px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.82);
    -webkit-backdrop-filter: blur(14px) saturate(1.4);
    backdrop-filter: blur(14px) saturate(1.4);
    border: 1px solid rgba(255, 255, 255, 0.9);
    box-shadow:
        0 10px 30px rgba(96, 68, 190, 0.18),
        0 2px 6px rgba(96, 68, 190, 0.08);
}

/* Fixed-size slot so the scaled preview circle can never paint over the
   buttons above/below it, no matter how large brushSize gets. */
.size-preview {
    width: clamp(38px, 3.8vw, 46px);
    height: clamp(38px, 3.8vw, 46px);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    flex-shrink: 0;
}

.size-indicator {
    width: clamp(22px, 2.2vw, 28px);
    height: clamp(22px, 2.2vw, 28px);
    min-width: 8px;
    min-height: 8px;
    background: linear-gradient(135deg, #FF4B91, #FF8F3D);
    border-radius: 50%;
    border: 3px solid #fff;
    box-shadow: 0 3px 8px rgba(255, 75, 145, 0.35);
    flex-shrink: 0;
    transition: transform 0.1s ease;
}

/* Large, kid-friendly touch target: thick track + big thumb.
   This used to be a horizontal slider rotated -90deg to read vertically in the
   left rail (vertical range inputs are unreliable across browsers, so rotation
   was the only safe way). The rail is gone and the control now sits in a
   horizontal bar, so the rotation hack goes with it. */
/* The pill is shrink-to-fit, so the track needs a real width of its own —
   `flex: 1` inside an auto-width box would collapse to the thumb. */
.size-track {
    flex: 0 0 auto;
    width: clamp(130px, 20vw, 190px);
}

#brush-size {
    -webkit-appearance: none;
    appearance: none;
    display: block;
    width: 100%;
    height: 34px;
    background: transparent;
    outline: none;
    cursor: pointer;
    margin: 0;
    /* The drag is handled in script.js via pointer capture. Claim the gesture
       so a sideways swipe on the thumb can never be handed to a scroll
       container instead — that hand-off fires pointercancel and drops the
       drag mid-slide. */
    touch-action: none;
}

#brush-size::-webkit-slider-runnable-track {
    height: 14px;
    background: linear-gradient(90deg, #ffd3e4, #e4d9fb);
    border-radius: 12px;
    box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.1);
}

#brush-size::-moz-range-track {
    height: 14px;
    background: linear-gradient(90deg, #ffd3e4, #e4d9fb);
    border-radius: 12px;
}

#brush-size::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 24px;
    height: 24px;
    margin-top: -5px;
    border-radius: 50%;
    background: linear-gradient(150deg, #5aa9ff, #2f7ef0);
    border: none;
    box-shadow: 0 3px 8px rgba(47, 126, 240, 0.45);
    cursor: pointer;
}

#brush-size::-moz-range-thumb {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: linear-gradient(150deg, #5aa9ff, #2f7ef0);
    border: none;
    box-shadow: 0 3px 8px rgba(47, 126, 240, 0.45);
    cursor: pointer;
}

/* ─── Palette Card ───
   #palette-outer is the visual card AND the "disabled" overlay's positioning
   context; #palette-container is just the scrolling body inside it. Putting
   the overlay on the non-scrolling outer element means it always covers the
   full visible area, no matter how far the inner content is scrolled — an
   overlay living on the scrolling element itself would scroll away with the
   content and stop blocking clicks past the first screenful. */
#palette-outer {
    flex: 1;
    position: relative;
    min-height: 0;
    border-radius: var(--radius);
    background: var(--card);
    border: 1px solid var(--line);
    box-shadow: 0 8px 26px rgba(96, 68, 190, 0.08);
}

#palette-container {
    width: 100%;
    height: 100%;
    padding: clamp(10px, 1.2vw, 18px);
    display: flex;
    flex-direction: column;
    gap: 12px;
    overflow-y: auto;
}

#palette-outer::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(246, 243, 255, 0.82);
    border-radius: var(--radius);
    z-index: 50;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s ease;
}

#palette-outer.disabled-palette::after {
    opacity: 1;
    pointer-events: all;
}

/* Tap arrows over the horizontal color/pattern/sticker strip. Desktop
   scrolls vertically (wheel/scrollbar) so these stay hidden until the
   mobile media query below turns them on. */
.palette-scroll-arrow {
    display: none;
    position: absolute;
    z-index: 60;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.9);
    border: 1px solid var(--line);
    box-shadow: 0 2px 8px rgba(96, 68, 190, 0.15);
    cursor: pointer;
    padding: 0;
    color: var(--violet);
    opacity: 1;
    transition: opacity 0.2s ease;
}

/* Nothing left to scroll that direction — e.g. the left arrow before the
   user has scrolled right at all. */
.palette-scroll-arrow.at-limit {
    opacity: 0;
    pointer-events: none;
}

.palette-scroll-arrow .msr {
    font-size: 22px;
}

.palette-scroll-arrow.left {
    left: 0;
    border-radius: 16px 0 0 16px;
}

.palette-scroll-arrow.right {
    right: 0;
    border-radius: 0 16px 16px 0;
}

/* ─── Current colour + rainbow header ─── */
#color-head {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    padding-bottom: 12px;
    border-bottom: 1px solid var(--line);
    flex-shrink: 0;
}

.color-head-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
}

.color-head-item.grow {
    flex: 1;
    min-width: 0;
}

.head-label {
    font-size: 0.68rem;
    font-weight: 700;
    color: var(--ink-soft);
    white-space: nowrap;
}

#active-color-preview {
    width: clamp(38px, 3.4vw, 52px);
    height: clamp(38px, 3.4vw, 52px);
    border-radius: 50%;
    background-color: #FF0000;
    border: 4px solid #fff;
    box-shadow: 0 0 0 2px var(--line), 0 4px 10px rgba(0, 0, 0, 0.12);
    flex-shrink: 0;
    transition: background-color 0.2s;
}

#custom-color-wrapper {
    width: 100%;
    height: clamp(38px, 3.4vw, 52px);
    border-radius: 14px;
    background: linear-gradient(to right,
            #ff2d55, #ff8f3d, #ffe14b, #4ade80, #38bdf8, #6c4bf5, #ff2d55);
    position: relative;
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.12);
    flex-shrink: 0;
    border: 3px solid #fff;
    outline: 2px solid var(--line);
}

#custom-color-btn {
    -webkit-appearance: none;
    appearance: none;
    background: none;
    border: none;
    padding: 0;
    width: 100%;
    height: 100%;
    cursor: pointer;
    position: absolute;
    top: 0;
    left: 0;
    border-radius: inherit;
}

/* ─── Colour / Pattern / Sticker Grids ─── */
#color-grid {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

.palette-section {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* 4 columns, not 6. Small hands miss small targets — a wider cell buys a
   noticeably bigger swatch out of the same narrow palette panel, at the cost
   of a little more scrolling. */
.grid-section {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: clamp(5px, 0.5vw, 9px);
    justify-items: center;
}

/* Patterns are picture previews like stickers, not flat colours — same 3 wide
   columns so the texture is actually readable before a child commits to it. */
#pattern-grid {
    grid-template-columns: repeat(3, 1fr);
}

/* Stickers get the widest cells of all — the emoji IS the label here, so a
   child has to recognise it at a glance, not squint at it. */
#sticker-grid {
    grid-template-columns: repeat(3, 1fr);
}

.section-title {
    font-weight: 800;
    color: var(--ink);
    font-size: 0.78rem;
    letter-spacing: 0.2px;
}

.color-btn,
.pattern-btn,
.sticker-btn {
    width: 100%;
    aspect-ratio: 1;
    max-width: clamp(44px, 4.6vw, 60px);
    border-radius: 50%;
    border: 2px solid rgba(0, 0, 0, 0.06);
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.12);
    cursor: pointer;
    transition: transform 0.15s ease, box-shadow 0.15s ease;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: clamp(0.9rem, 1.1vw, 1.3rem);
    background-color: white;
    background-position: center;
    background-size: cover;
    line-height: 1;
}

/* Stickers are previews, not swatches, so they don't inherit the colour-chip
   sizing above. A colour reads fine in a 28px dot; an emoji does not — at that
   size a 3-8 year old can't tell a puppy from a kitten. Bigger box, much bigger
   glyph, and a rounded square instead of a circle so the artwork isn't
   visually cropped at the corners. */
.sticker-btn {
    max-width: clamp(58px, 6vw, 78px);
    font-size: clamp(2.2rem, 3.4vw, 3rem);
    border-radius: 18px;
}

/* A circle crops a repeating texture badly — the corners are where a tartan or
   polka dot reads as itself. Square it off and give it the same room as a
   sticker. */
.pattern-btn {
    max-width: clamp(58px, 6vw, 78px);
    border-radius: 18px;
}

/* Hover is a mouse affordance, and on a touch screen it is a trap: a tap
   leaves :hover applied until something else is tapped, so the last button
   pressed stays enlarged/lifted and reads as jammed. Gated on a real pointer
   throughout this file for that reason. */
@media (hover: hover) {

    .color-btn:hover,
    .pattern-btn:hover,
    .sticker-btn:hover {
        transform: scale(1.12);
    }
}

/* Touch gets its feedback from the press instead, which ends with the finger. */
.color-btn:active,
.pattern-btn:active,
.sticker-btn:active {
    transform: scale(1.12);
}

.color-btn.active,
.pattern-btn.active,
.sticker-btn.active {
    transform: scale(1.1);
    box-shadow: 0 0 0 3px #fff, 0 0 0 5px var(--accent);
}

/* ─── Tools rail ─── */
#tools-container {
    /* The coloured tile is sized FROM the rail rather than from the viewport.
       Both used to carry their own vw clamp, and because they bottomed out at
       different ratios the tile could pin to its 26px floor inside a 66px
       button — a small chip of colour marooned in white. Deriving it here keeps
       the proportion fixed at every width. */
    --rail-w: clamp(66px, 6vw, 86px);
    --ico-size: calc(var(--rail-w) * 0.62);

    width: var(--rail-w);
    display: flex;
    flex-direction: column;
    gap: clamp(5px, 0.8vh, 12px);
    align-items: stretch;
    flex-shrink: 0;
    overflow-y: auto;
    scrollbar-width: none;
}

#tools-container::-webkit-scrollbar {
    display: none;
}

#tools-container .tool-btn {
    background: var(--card);
    border: 2px solid var(--line);
    border-radius: 16px;
    padding: clamp(5px, 0.8vh, 9px) 2px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    cursor: pointer;
    box-shadow: 0 4px 12px rgba(96, 68, 190, 0.08);
    transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
    flex-shrink: 0;
}

.tool-ico {
    width: var(--ico-size, 30px);
    height: var(--ico-size, 30px);
    border-radius: calc(var(--ico-size, 30px) * 0.32);
    background: linear-gradient(150deg, var(--c1, #8f7bff), var(--c2, #6c4bf5));
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    box-shadow: 0 3px 8px rgba(60, 40, 130, 0.2);
}

/* SVG glyphs instead of emoji: emoji render as a different picture on every
   OS/browser (Apple vs Android vs Windows), so these tools use plain vector
   icons that look identical everywhere. */
.tool-ico .msr {
    font-size: calc(var(--ico-size, 30px) * 0.72);
    color: #fff;
}

.tool-lbl {
    font-size: clamp(0.56rem, 0.6vw, 0.7rem);
    font-weight: 800;
    color: var(--ink);
    line-height: 1.1;
    text-align: center;
}

@media (hover: hover) {
    #tools-container .tool-btn:hover {
        transform: translateY(-2px);
        box-shadow: 0 8px 16px rgba(96, 68, 190, 0.15);
    }
}

#tools-container .tool-btn:active {
    transform: scale(0.95);
}

#tools-container .tool-btn.active {
    border-color: var(--c2, #6c4bf5);
    box-shadow: 0 6px 16px color-mix(in srgb, var(--c2, #6c4bf5) 30%, transparent);
    transform: translateY(-1px);
}

/* ─── Main Canvas Area ─── */
/* main is just a positioning frame; the visible "card" is sized to the
   image itself via #canvas-wrapper below, not to the leftover space here. */
main {
    flex: 1;
    position: relative;
    display: flex;
    justify-content: center;
    align-items: center;
    overflow: hidden;
    touch-action: none;
    background: transparent;
    min-width: 0;
}

#canvas-wrapper {
    position: absolute;
    top: 0;
    left: 0;
    transform-origin: 0 0;
    background: white;
    border-radius: var(--radius);
    outline: 2px solid #ddd0ff;
    box-shadow: 0 8px 26px rgba(96, 68, 190, 0.08);
    /* #vector-overlay is intentionally drawn at full natural size and shifted
       to align a cropped region with this box's origin — clip the rest. */
    overflow: hidden;
}

canvas {
    position: absolute;
    top: 0;
    left: 0;
    border-radius: 12px;
}

#line-layer {
    opacity: 0;
    pointer-events: none;
}

#vector-overlay {
    position: absolute;
    top: 0;
    left: 0;
    pointer-events: none;
    mix-blend-mode: multiply;
    border-radius: 12px;
}

/* ─── Floating View Controls ─── */
/* A frosted pill docked to the bottom of the canvas area holding pan, zoom
   and fit. Sits above the artwork (main is the positioning context) but is
   only as wide as its buttons, so it never blocks the picture. */
#view-controls {
    position: absolute;
    bottom: clamp(12px, 2vh, 22px);
    left: 50%;
    transform: translateX(-50%);
    z-index: 40;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 7px 10px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.82);
    -webkit-backdrop-filter: blur(14px) saturate(1.4);
    backdrop-filter: blur(14px) saturate(1.4);
    border: 1px solid rgba(255, 255, 255, 0.9);
    box-shadow:
        0 10px 30px rgba(96, 68, 190, 0.18),
        0 2px 6px rgba(96, 68, 190, 0.08);
}

.view-sep {
    width: 1px;
    height: 22px;
    background: var(--line);
    flex-shrink: 0;
}

.view-btn {
    width: 42px;
    height: 42px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: #6a5aa6;
    cursor: pointer;
    transition: background 0.18s ease, color 0.18s ease, transform 0.12s ease;
}

.view-btn .msr {
    font-size: 24px;
    pointer-events: none;
}

@media (hover: hover) {
    .view-btn:hover {
        background: var(--surface, #f6f3ff);
        color: var(--brand, #7b3fe4);
    }

    /* Clear throws away the whole drawing — it should not look like its
       neighbours on hover. */
    .view-btn-danger:hover {
        background: #ffe9ec;
        color: #ec3f3f;
    }
}

.view-btn:active {
    transform: scale(0.92);
}

/* Pan is a mode, not a one-shot action, so it needs a persistent "on" state —
   the others fire and are done. */
.view-btn.tool-btn.active {
    background: linear-gradient(135deg, #ffc555, #f5a524);
    color: #fff;
    box-shadow: 0 4px 12px rgba(245, 165, 36, 0.4);
}

/* ─── Bottom Bar ─── */
.big-btn {
    -webkit-appearance: none;
    appearance: none;
    font-family: inherit;
    display: flex;
    align-items: center;
    gap: 8px;
    border: none;
    border-radius: 999px;
    padding: clamp(10px, 1.4vh, 15px) clamp(14px, 1.6vw, 26px);
    font-size: clamp(0.75rem, 0.9vw, 0.95rem);
    font-weight: 800;
    color: #fff;
    cursor: pointer;
    transition: transform 0.15s ease, box-shadow 0.15s ease, filter 0.15s ease;
    white-space: nowrap;
}

.big-btn .msr {
    font-size: 20px;
    flex-shrink: 0;
}

.btn-save {
    background: linear-gradient(150deg, #ffc555, #f5a524);
    box-shadow: 0 6px 16px rgba(245, 165, 36, 0.35);
}

.btn-new {
    background: linear-gradient(150deg, #6ddc8b, #2fb85f);
    box-shadow: 0 6px 16px rgba(47, 184, 95, 0.35);
}

@media (hover: hover) {
    .big-btn:hover {
        transform: translateY(-2px);
        filter: brightness(1.04);
    }
}

.big-btn:active {
    transform: translateY(1px);
}

/* ─── Ripple Effect ─── */
/* ─── Glitter burst ─── */
/* Two nested elements per particle, on purpose. The outer one only travels,
   the inner one only spins and twinkles. Packing both into a single transform
   forces them to share one easing curve, which is what makes a burst read as
   stiff — the flight wants a fast-out/slow-settle ease while the spin wants to
   turn steadily and the twinkle wants to pulse. Separated, they overlap into
   something that looks alive.
   Each particle also carries its own delay, so the burst scatters into
   existence over ~130ms instead of appearing as one solid ring. */
.sparkle {
    position: absolute;
    width: var(--size, 16px);
    height: var(--size, 16px);
    pointer-events: none;
    z-index: 110;
    will-change: transform;
    animation: sparkleTravel var(--dur, 760ms) cubic-bezier(0.12, 0.72, 0.24, 1) var(--delay, 0ms) both;
}

/* Spins and twinkles; carries the outline star and the white star together so
   they stay locked to each other. */
.sparkle .sp {
    position: absolute;
    inset: 0;
    will-change: transform, opacity;
    animation: sparkleTwinkle var(--dur, 760ms) ease-out var(--delay, 0ms) both;
}

/* The outline. A white star spends half its life on white paper, so it needs a
   genuinely dark edge to exist at all.
   This is a second, slightly larger star sitting behind the white one rather
   than a drop-shadow: a chain of drop-shadows on a clip-path'd element failed
   to rasterise here — the particles disappeared entirely, filter and all —
   and stacked backgrounds also give a crisp edge instead of a blurred one. */
.sparkle b {
    position: absolute;
    inset: -16%;
    background: #3a2a8c;
    clip-path: polygon(50% 0%, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0% 50%, 40% 40%);
    opacity: 0.72;
}

/* Per-particle halo. A plain radial gradient rather than a blur filter, for
   the same rasterisation reason as the outline. */
.sparkle u {
    position: absolute;
    inset: -70%;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(214, 232, 255, 0.9) 0%, rgba(178, 206, 255, 0.45) 35%, rgba(178, 206, 255, 0) 70%);
}

.sparkle i {
    position: absolute;
    inset: 0;
    /* White core fading to pale blue at the tips, so the star has shape of its
       own rather than reading as a flat silhouette. */
    background: radial-gradient(circle at 50% 50%, #fff 0%, #fff 45%, #e4f0ff 74%, #b9d2ff 100%);
    clip-path: polygon(50% 0%, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0% 50%, 40% 40%);
}

/* Flight only — no spin, no scale. */
@keyframes sparkleTravel {
    from {
        transform: translate(-50%, -50%) translate(0, 0);
    }

    to {
        transform: translate(-50%, -50%) translate(var(--dx), var(--dy));
    }
}

/* Spin + twinkle only. The scale dips at 55% then lifts again, which reads as
   a star catching the light rather than simply shrinking away. */
@keyframes sparkleTwinkle {
    0% {
        transform: scale(0) rotate(0deg);
        opacity: 0;
    }

    14% {
        transform: scale(1.35) rotate(calc(var(--spin) * 0.16));
        opacity: 1;
    }

    55% {
        transform: scale(0.78) rotate(calc(var(--spin) * 0.58));
        opacity: 1;
    }

    78% {
        transform: scale(1.06) rotate(calc(var(--spin) * 0.8));
        opacity: 0.9;
    }

    100% {
        transform: scale(0) rotate(var(--spin));
        opacity: 0;
    }
}

/* A single soft flash under the stars, giving the burst a centre to come from
   instead of starting as bare points. */
.sparkle-flash {
    position: absolute;
    width: 110px;
    height: 110px;
    border-radius: 50%;
    pointer-events: none;
    z-index: 109;
    background: radial-gradient(circle, rgba(255, 255, 255, 0.9) 0%, rgba(226, 242, 255, 0.45) 38%, rgba(255, 255, 255, 0) 70%);
    animation: sparkleFlash 460ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
}

@keyframes sparkleFlash {
    0% {
        transform: translate(-50%, -50%) scale(0.15);
        opacity: 0.95;
    }

    100% {
        transform: translate(-50%, -50%) scale(1.7);
        opacity: 0;
    }
}

/* Some children are motion-sensitive, and this fires on every fill. */
@media (prefers-reduced-motion: reduce) {

    .sparkle,
    .sparkle-flash {
        display: none;
    }
}

.ripple {
    position: absolute;
    width: 30px;
    height: 30px;
    background: rgba(255, 75, 145, 0.15);
    border: 2px solid #FF4B91;
    border-radius: 50%;
    transform: translate(-50%, -50%) scale(1);
    animation: rippleAnim 0.4s ease-out forwards;
    pointer-events: none;
    z-index: 100;
}

@keyframes rippleAnim {
    0% {
        transform: translate(-50%, -50%) scale(0.5);
        opacity: 1;
    }

    100% {
        transform: translate(-50%, -50%) scale(3);
        opacity: 0;
    }
}

/* ─── Loading Indicator ─── */
#loading {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(246, 243, 255, 0.94);
    backdrop-filter: blur(8px);
    z-index: 999;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 1.5rem;
    font-weight: 800;
    color: var(--accent);
    letter-spacing: 1px;
}

/* ─── Palette Tabs ─── */
.palette-tab {
    display: none;
    flex-direction: column;
    width: 100%;
    gap: 12px;
}

.palette-tab.active-tab {
    display: flex;
}

/* ─── Gallery & Confirm Popups ─── */
#gallery-popup,
#confirm-popup,
#color-pop {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(30, 20, 60, 0.55);
    backdrop-filter: blur(6px);
    z-index: 1000;
    display: flex;
    justify-content: center;
    align-items: center;
}

.gallery-content {
    background: white;
    padding: 28px;
    border-radius: 24px;
    border: 2px solid var(--line);
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
    width: 90%;
    height: 90vh;
    max-width: 90%;
    display: flex;
    flex-direction: column;
    position: relative;
}

.gallery-content h2 {
    margin-top: 0;
    color: var(--accent);
    text-align: center;
    text-transform: uppercase;
    font-weight: 800;
    letter-spacing: 1px;
    font-size: 1.4rem;
}

/* Small confirm dialog (e.g. "Clear Game?") sizes to its content instead of
   inheriting the gallery's tall 90vh box, which left a huge empty area.
   Qualified as .gallery-content.confirm-dialog (two classes) so it always
   outranks the plain .gallery-content rules, including the ones inside the
   mobile media query further down the file — otherwise source order would
   let that later, equal-specificity rule win back the 90vh height. */
.gallery-content.confirm-dialog {
    width: 90%;
    max-width: 400px;
    height: auto;
    padding: 30px;
    text-align: center;
}

.confirm-dialog h2 {
    font-size: 1.6rem;
}

.confirm-dialog p {
    font-weight: 700;
    color: var(--ink-soft);
    font-size: 1.05rem;
    margin: 0 0 24px;
    line-height: 1.4;
}

/* ─── Make-your-own-colour picker ─── */
/* Two choices, in words a child can follow: which colour, then how light.
   No hex field, no R/G/B boxes, no saturation square — every control is a
   thing you drag or tap, sized for a fingertip rather than a mouse. */
.gallery-content.color-pop-card {
    width: 90%;
    max-width: 420px;
    height: auto;
    padding: 26px 24px 24px;
    text-align: center;
    gap: 18px;
}

#cp-preview {
    width: 104px;
    height: 104px;
    border-radius: 50%;
    margin: 2px auto 0;
    background: #ff0000;
    border: 6px solid #fff;
    box-shadow: 0 0 0 3px var(--line), 0 10px 24px rgba(0, 0, 0, 0.18);
    transition: background-color 0.12s ease;
}

.cp-step {
    display: flex;
    flex-direction: column;
    gap: 9px;
}

.cp-label {
    font-size: 0.95rem;
    font-weight: 800;
    color: var(--ink);
}

/* Chunky enough to hit without aiming. */
#cp-hue {
    position: relative;
    height: 40px;
    border-radius: 999px;
    cursor: pointer;
    touch-action: none;
    border: 3px solid #fff;
    box-shadow: 0 0 0 2px var(--line), 0 4px 12px rgba(0, 0, 0, 0.12);
    background: linear-gradient(to right,
            hsl(0, 90%, 55%), hsl(60, 90%, 55%), hsl(120, 90%, 55%),
            hsl(180, 90%, 55%), hsl(240, 90%, 55%), hsl(300, 90%, 55%),
            hsl(360, 90%, 55%));
}

#cp-hue-thumb {
    position: absolute;
    top: 50%;
    left: 0;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    transform: translate(-50%, -50%);
    background: #fff;
    border: 5px solid #fff;
    box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.18), 0 4px 10px rgba(0, 0, 0, 0.25);
    pointer-events: none;
}

#cp-hue:focus-visible {
    outline: 3px solid var(--violet);
    outline-offset: 3px;
}

#cp-shades {
    display: flex;
    justify-content: center;
    gap: 8px;
}

.cp-shade {
    -webkit-appearance: none;
    appearance: none;
    flex: 1;
    min-width: 0;
    aspect-ratio: 1;
    max-width: 52px;
    padding: 0;
    border-radius: 50%;
    cursor: pointer;
    border: 4px solid #fff;
    box-shadow: 0 0 0 2px var(--line), 0 3px 8px rgba(0, 0, 0, 0.12);
    transition: transform 0.12s ease, box-shadow 0.12s ease;
}

@media (hover: hover) {
    .cp-shade:hover {
        transform: translateY(-3px) scale(1.06);
    }
}

/* The chosen bubble grows and takes a dark ring, so "which one am I on?" is
   answerable at a glance rather than by reading a value. */
.cp-shade.selected {
    transform: translateY(-3px) scale(1.14);
    box-shadow: 0 0 0 3px var(--ink), 0 6px 14px rgba(0, 0, 0, 0.25);
}

#cp-confirm {
    -webkit-appearance: none;
    appearance: none;
    font-family: inherit;
    margin-top: 2px;
    padding: 15px 24px;
    border: none;
    border-radius: 18px;
    font-size: 1.15rem;
    font-weight: 800;
    color: #fff;
    cursor: pointer;
    background: linear-gradient(135deg, #FF4B91, #ff3377);
    box-shadow: 0 6px 16px rgba(255, 75, 145, 0.35);
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}

@media (hover: hover) {
    #cp-confirm:hover {
        transform: translateY(-2px);
        box-shadow: 0 10px 22px rgba(255, 75, 145, 0.45);
    }
}

#cp-confirm:active {
    transform: translateY(0);
}

.close-btn {
    position: absolute;
    top: -12px;
    right: -12px;
    background: linear-gradient(135deg, #FF4B91, #FF8F3D);
    color: white;
    border: 3px solid white;
    border-radius: 50%;
    width: 40px;
    height: 40px;
    font-size: 1.2rem;
    font-weight: bold;
    cursor: pointer;
    box-shadow: 0 4px 12px rgba(255, 75, 145, 0.3);
    display: flex;
    justify-content: center;
    align-items: center;
    transition: transform 0.15s ease;
}

@media (hover: hover) {
    .close-btn:hover {
        transform: scale(1.1);
    }
}

.close-btn:active {
    transform: scale(0.95);
}

#gallery-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
    gap: 0.625rem;
    overflow-y: auto;
    padding: 0.25rem;
    flex: 1;
    min-height: 0;
    align-content: start;
    /* Claim the vertical drag for this grid alone: `pan-y` tells the compositor
       up front that a downward swipe scrolls here, so it can start moving on
       the first frame instead of waiting to find out. `contain` stops the
       fling from chaining into the page behind the popup once the list ends —
       that hand-off is what made the flick stop dead partway. */
    touch-action: pan-y;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
}

/* Placeholder while the picture library is fetched on first gallery open.
   Spans the grid so it reads as one message, not a stray first tile. */
.gallery-loading {
    grid-column: 1 / -1;
    padding: 2.5rem 0;
    text-align: center;
    font-weight: 700;
    color: var(--accent);
}

.gallery-item {
    background: #faf7ff;
    border: 0.125rem solid var(--line);
    border-radius: 0.75rem;
    cursor: pointer;
    /* Named properties, not `all`: `all` also animates the layout properties,
       so every tile in a several-hundred-tile library is a transition
       candidate on any change. */
    transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
    display: flex;
    flex-direction: column;
    aspect-ratio: 0.85 / 1;
    position: relative;
    /* The library runs to a thousand pictures, and laying out and painting
       every one of them is what makes a phone stutter through the list. This
       skips that work for tiles nowhere near the viewport.
       `none` for the placeholder size, deliberately: a guessed height (11rem
       was the first attempt) is a height the tile then CHANGES on render, and
       1000 of those corrections drag the scroll position around under the
       finger — measured 94000px of guessed grid against 77289px of real one.
       With `none` the aspect-ratio above sizes the skipped tiles instead, so
       the grid is exactly as tall as it will be once painted. */
    content-visibility: auto;
    contain-intrinsic-size: auto none;
}

@media (hover: hover) {
    .gallery-item:hover {
        transform: translateY(-0.125rem);
        border-color: var(--accent);
        box-shadow: 0 0.375rem 1rem rgba(255, 75, 145, 0.15);
    }
}

.gallery-item:active {
    border-color: var(--accent);
    transform: scale(0.97);
}

.gallery-item img {
    width: 100%;
    flex: 1;
    min-height: 0;
    object-fit: contain;
    pointer-events: none;
    padding: 0.375rem;
}

.gallery-label {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    background: rgba(255, 255, 255, 0.92);
    text-align: center;
    padding: 0.3rem 0.375rem;
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--accent);
    border-top: 1px solid var(--line);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    line-height: 1.2;
    min-width: 0;
    box-sizing: border-box;
}

/* ─── Gallery Pagination ─── */
.gallery-pagination {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.75rem 0 0.25rem;
    flex-shrink: 0;
}

.gallery-pagination button {
    background: linear-gradient(135deg, #FF4B91, #FF8F3D);
    color: white;
    border: none;
    border-radius: 0.5rem;
    padding: 0.5rem 1rem;
    font-size: 0.8rem;
    font-weight: 700;
    cursor: pointer;
    font-family: inherit;
    box-shadow: 0 0.125rem 0.5rem rgba(255, 75, 145, 0.25);
    transition: all 0.15s ease;
}

@media (hover: hover) {
    .gallery-pagination button:hover:not(:disabled) {
        transform: translateY(-1px);
        box-shadow: 0 0.25rem 0.75rem rgba(255, 75, 145, 0.35);
    }
}

.gallery-pagination button:disabled {
    opacity: 0.4;
    cursor: not-allowed;
    transform: none;
}

.gallery-pagination .page-info {
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--accent);
    min-width: 5rem;
    text-align: center;
}

/* ─── Responsive / Mobile Layout ─── */
@media (max-width: 900px),
(orientation: portrait) {
    #app-shell {
        padding: 6px;
        gap: 6px;
    }

    #app-row {
        flex-direction: column;
        gap: 6px;
    }

    .tool-lbl,
    .size-lbl {
        display: none;
    }

    main {
        order: 2;
        flex: 1;
        min-height: 0;
    }

    #right-sidebar {
        order: 3;
        width: 100%;
        height: auto;
        flex-direction: column;
        gap: 6px;
        flex-shrink: 0;
    }

    /* #tools-container is the last DOM child, #palette-outer the first —
       column-reverse keeps the tool row visually above the color strip,
       matching the desktop reading order. */
    #palette-tools-row {
        flex-direction: column-reverse;
        gap: 6px;
    }

    #tools-container {
        width: 100%;
        flex-direction: row;
        overflow-x: auto;
        overflow-y: hidden;
        /* `safe` is doing real work here. Once six buttons no longer fit — a
           320px phone, or a larger system font — plain `center` splits the
           overflow evenly and pushes the first button past the container's
           LEFT edge, into space scrollLeft cannot reach (it stops at 0). The
           Brush button was clipped and untappable with no way to scroll to
           it. `safe` drops back to flex-start exactly when that would happen,
           so the strip stays centred while it fits and scrolls when it does
           not. Browsers without `safe` ignore the whole declaration and get
           flex-start, which is also reachable. */
        justify-content: safe center;
        gap: 6px;
        padding: 0;
        -webkit-overflow-scrolling: touch;
        overscroll-behavior-x: contain;
    }

    #tools-container .tool-btn {
        padding: 6px 8px;
        border-radius: 13px;
    }

    /* The rail turns into a horizontal strip here, so the tile takes a flat
       size instead of a share of the rail width — six buttons have to share the
       screen width, which is the opposite squeeze from the desktop rail. Set
       the variable rather than the width so the corner radius and glyph stay in
       proportion. */
    #tools-container {
        --ico-size: 30px;
    }

    #palette-outer {
        width: 100%;
        min-height: 68px;
        border-radius: 16px;
        flex: 0 0 auto;
    }

    #palette-container {
        padding: 8px;
        flex-direction: row;
        flex-wrap: nowrap;
        overflow-x: auto;
        overflow-y: hidden;
        min-height: 68px;
        -webkit-overflow-scrolling: touch;
        overscroll-behavior-x: contain;
    }

    /* Tap arrows for the horizontal color/pattern/sticker strip */
    .palette-scroll-arrow {
        display: flex;
        top: 0;
        bottom: 0;
        width: 26px;
    }

    .palette-tab {
        flex-direction: row;
        gap: 10px;
        width: max-content;
        align-items: center;
        overflow: visible;
    }

    #color-head {
        flex-direction: row;
        border-bottom: none;
        border-right: 1px solid var(--line);
        padding: 0 10px 0 0;
        gap: 8px;
        align-items: center;
    }

    .head-label {
        display: none;
    }

    #custom-color-wrapper {
        width: 56px;
        height: 42px;
    }

    #active-color-preview {
        width: 42px;
        height: 42px;
    }

    #color-grid {
        flex-direction: row;
        gap: 10px;
    }

    .palette-section {
        gap: 4px;
    }

    .section-title {
        display: none;
    }

    .grid-section,
    #pattern-grid,
    #sticker-grid {
        display: flex;
        flex-wrap: nowrap;
        justify-content: flex-start;
        gap: 8px;
    }

    .color-btn,
    .pattern-btn,
    .sticker-btn {
        width: 48px;
        max-width: none;
        height: 48px;
        flex-shrink: 0;
        font-size: 1.4rem;
    }

    /* Same reasoning as desktop: on the horizontal mobile strip the preview
       still has to be recognisable, and small fingers need a bigger target. */
    .sticker-btn,
    .pattern-btn {
        width: 64px;
        height: 64px;
        border-radius: 16px;
    }

    .sticker-btn {
        font-size: 2.4rem;
    }

    /* Tighter pill on small screens — the canvas area is short here, so the
       controls hug the bottom edge and give back vertical room. */
    #view-controls {
        bottom: 8px;
        gap: 3px;
        padding: 5px 8px;
    }

    .view-btn {
        width: 40px;
        height: 40px;
    }

    .view-btn .msr {
        font-size: 23px;
    }

    #sidebar-actions {
        width: 100%;
    }

    .big-btn {
        padding: 10px 14px;
        font-size: 0.72rem;
        gap: 6px;
    }

    .big-btn .msr {
        font-size: 18px;
    }

    /* A full-screen backdrop blur has to be re-composited while the grid
       scrolls over it, which is most of the stutter on a mid-range phone.
       Drop the blur and darken the dim slightly to make up the separation —
       the popup still reads as floating over the artwork. */
    #gallery-popup {
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
        background: rgba(30, 20, 60, 0.62);
    }

    /* Gallery responsive on mobile */
    .gallery-content {
        width: 96%;
        height: 94vh;
        max-width: 96%;
        padding: 1rem;
        border-radius: 1rem;
    }

    .gallery-content h2 {
        font-size: 1.1rem;
        margin-bottom: 0.625rem;
    }

    #gallery-grid {
        grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
        gap: 0.5rem;
    }

    .gallery-label {
        font-size: 0.7rem;
        padding: 0.1875rem 0.25rem;
    }

    .gallery-pagination button {
        padding: 0.375rem 0.75rem;
        font-size: 0.7rem;
    }

    .gallery-pagination .page-info {
        font-size: 0.7rem;
    }
}

/* Very small screens: tighten button padding */
@media (max-width: 560px) {
    .big-btn {
        padding: 10px 12px;
    }
}

/* ─── Scrollbar Styling ─── */
#palette-container::-webkit-scrollbar,
#gallery-grid::-webkit-scrollbar {
    width: 6px;
    height: 6px;
}

#palette-container::-webkit-scrollbar-track,
#gallery-grid::-webkit-scrollbar-track {
    background: transparent;
}

#palette-container::-webkit-scrollbar-thumb,
#gallery-grid::-webkit-scrollbar-thumb {
    background: rgba(139, 92, 246, 0.25);
    border-radius: 10px;
}

#palette-container::-webkit-scrollbar-thumb:hover,
#gallery-grid::-webkit-scrollbar-thumb:hover {
    background: rgba(139, 92, 246, 0.45);
}
