/*
 * Global visual theme for the public site, ported from the old SvelteKit app's
 * src/routes/layout.css + src/lib/components/{layout/SiteHeader,layout/SimpleLayout,auth/AuthControls}.svelte.
 * The TZ has no visual spec (word-only wireframe), so this follows how the old app looked and
 * behaved rather than the letter of the TZ. Linked unconditionally from base.html.twig's <head>,
 * outside {% block stylesheets %}, so per-page overrides of that block can't drop it.
 */

:root {
    --font-body: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --font-display: 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Georgia, serif;
    --color-bg-0: #f7f2e9;
    --color-bg-1: #f1e8da;
    --color-surface-0: rgba(255, 251, 244, 0.9);
    --color-surface-1: rgba(255, 248, 236, 0.88);
    --color-theme-1: #a66e35;
    --color-theme-2: #5f8b82;
    --color-text: rgba(41, 30, 22, 0.94);
    --color-text-soft: rgba(41, 30, 22, 0.68);
    --color-border: rgba(90, 64, 42, 0.14);
    --color-border-strong: rgba(90, 64, 42, 0.22);
    --color-shadow: rgba(78, 57, 35, 0.14);
    --color-error: #b3432b;
    --content-width: min(1120px, calc(100vw - 2rem));
    color-scheme: light;
    font-family: var(--font-body);
    color: var(--color-text);
}

* {
    box-sizing: border-box;
}

html {
    background: var(--color-bg-0);
}

body {
    min-height: 100vh;
    margin: 0;
    background-attachment: fixed;
    background-color: var(--color-bg-0);
    background-size: cover;
    background-image:
        radial-gradient(circle at 14% 8%, rgba(166, 110, 53, 0.12), transparent 32%),
        radial-gradient(circle at 90% 2%, rgba(95, 139, 130, 0.1), transparent 28%),
        linear-gradient(180deg, #fbf7f0 0%, #f2eadc 46%, #ece0cf 100%);
}

h1, h2, h3 {
    font-family: var(--font-display);
    font-weight: 400;
    letter-spacing: -0.02em;
    color: var(--color-text);
}

h1 {
    font-size: clamp(1.6rem, 2.4vw, 2.1rem);
}

h2 {
    font-size: 1.15rem;
}

p {
    line-height: 1.5;
}

a {
    color: var(--color-theme-1);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

.site-header,
.site-content,
.site-footer {
    width: var(--content-width);
    margin: 0 auto;
}

.site-header {
    padding: 1rem 0 0.5rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

.site-logo {
    font-family: var(--font-display);
    font-size: 1.3rem;
    font-weight: 600;
    color: var(--color-text);
    text-decoration: none;
    letter-spacing: -0.01em;
}

.site-logo:hover {
    color: var(--color-theme-1);
    text-decoration: none;
}

.site-content {
    padding: 1.25rem 0 2.5rem;
    min-height: 60vh;
}

.site-footer {
    padding: 1.25rem 0 2rem;
    border-top: 1px solid var(--color-border);
}

.site-nav,
.site-footer-nav {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    flex-wrap: wrap;
}

.site-footer-nav {
    font-size: 0.875rem;
    color: var(--color-text-soft);
}

.site-nav a,
.site-footer-nav a {
    padding: 0.4rem 1rem;
    border-radius: 999px;
    border: 1px solid var(--color-border-strong);
    background: var(--color-surface-0);
    color: var(--color-text);
    font-size: 0.875rem;
    font-weight: 500;
    box-shadow: 0 2px 8px var(--color-shadow);
    white-space: nowrap;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}

.site-nav a:hover,
.site-footer-nav a:hover {
    background: var(--color-bg-1);
    border-color: var(--color-theme-1);
    color: var(--color-theme-1);
    text-decoration: none;
}

.flash {
    padding: 0.75rem 1rem;
    border-radius: 0.6rem;
    margin: 0 0 1rem;
    font-size: 0.9rem;
}

.flash-success {
    background: rgba(95, 139, 130, 0.14);
    border: 1px solid rgba(95, 139, 130, 0.35);
    color: #2f5850;
}

.flash-error,
.form-error {
    background: rgba(179, 67, 43, 0.1);
    border: 1px solid rgba(179, 67, 43, 0.35);
    color: var(--color-error);
}

/* "Already at the limit" on the user-image upload page (templates/user_image_upload.html.twig) —
   informational/blocking, not a failure, so it gets its own tone rather than reusing
   .flash-error's red (same box shape as every other .flash variant, via the shared .flash rule). */
.flash-warning {
    background: rgba(201, 162, 39, 0.16);
    border: 1px solid rgba(201, 162, 39, 0.4);
    color: #7a5b0e;
}

.flash-error,
.form-error {
    padding: 0.75rem 1rem;
    border-radius: 0.6rem;
}

/* Forms — registration, login, profile edit, admin album/user/image forms. */
form {
    display: grid;
    gap: 0.9rem;
    max-width: 32rem;
}

label {
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--color-text-soft);
    display: block;
    margin-bottom: 0.3rem;
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="search"],
select,
textarea {
    width: 100%;
    padding: 0.55rem 0.75rem;
    font-size: inherit;
    font-family: inherit;
    color: var(--color-text);
    background: var(--color-surface-1);
    border: 1px solid var(--color-border-strong);
    border-radius: 0.6rem;
}

input:focus,
select:focus,
textarea:focus {
    outline: 2px solid var(--color-theme-2);
    outline-offset: 1px;
}

button,
input[type="submit"] {
    font-size: inherit;
    font-family: inherit;
    padding: 0.55rem 1.3rem;
    border-radius: 999px;
    border: 1px solid var(--color-theme-1);
    background: var(--color-theme-1);
    color: #fff9f2;
    font-weight: 500;
    cursor: pointer;
    box-shadow: 0 4px 12px var(--color-shadow);
    transition: background 0.15s, transform 0.1s;
}

button:hover,
input[type="submit"]:hover {
    background: #8f5c2b;
}

button:active,
input[type="submit"]:active {
    transform: translateY(1px);
}

button.muted {
    background: transparent;
    color: var(--color-text);
    border-color: var(--color-border-strong);
}

button.muted:hover {
    background: var(--color-bg-1);
    color: var(--color-theme-1);
    border-color: var(--color-theme-1);
}

/* Google/Facebook sign-in on the login page — same pill shape as button/input[type=submit]
   above, but an <a> (a GET redirect into /connect/{provider}, not a form submit) styled like
   button.muted's secondary/outline treatment, since they're alternatives to the primary
   email+password submit rather than another instance of it. */
.oauth-divider {
    text-align: center;
    color: var(--color-text-soft);
    margin: 1rem 0;
}

.oauth-buttons {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    margin: 1rem 0;
}

.oauth-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: inherit;
    font-family: inherit;
    padding: 0.55rem 1.3rem;
    border-radius: 999px;
    border: 1px solid var(--color-border-strong);
    background: transparent;
    color: var(--color-text);
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}

.oauth-button:hover {
    background: var(--color-bg-1);
    color: var(--color-theme-1);
    border-color: var(--color-theme-1);
    text-decoration: none;
}

/* Album / image thumbnail grids (home page columns, admin lists). */
img.thumb {
    border-radius: 0.5rem;
}

/* The one place .thumb is used (docs/tz-photo-gallery.md, "Главная страница" left column: album
   thumbnail + name side by side) — a small fixed icon, not a full-bleed card image like
   .gallery-link img, since it shares a horizontal row with the album name text next to it. */
.home-albums img.thumb {
    width: 4.5rem;
    height: 4.5rem;
    object-fit: cover;
    flex: 0 0 auto;
    display: block;
}

/*
 * Home page (docs/tz-photo-gallery.md, "Главная страница"): three columns, same shell pattern as
 * .album-columns below — left = album list, center = Puzzle of the Day, right = latest images.
 * Puzzle of the Day and Latest both render through partials/_puzzle_tile.html.twig now (the same
 * card used on /gallery, editorial albums and a user's own virtual album) instead of the
 * homepage's own plain-link markup that used to live here — that older, home-only card style
 * (.home-potd-item) is gone; see that partial's own docblock for why this project keeps exactly
 * one card implementation rather than a page-specific fork. Both grids stack single-column (one
 * image per row): Puzzle of the Day per the TZ's own "full width of the column" wording, Latest
 * because its column (home-latest, 16rem) is too narrow for more than one card across.
 */
.home-columns {
    display: flex;
    gap: 2rem;
    align-items: flex-start;
}

.home-albums {
    flex: 0 0 16rem;
}

.home-albums ul {
    list-style: none;
    margin: 0;
    padding: 0;
}

.home-albums li {
    margin-bottom: 1rem;
}

.home-albums a {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    text-decoration: none;
}

.home-album-name {
    font-weight: 600;
    color: var(--color-text);
}

.home-potd {
    flex: 1 1 auto;
    min-width: 0;
}

.home-latest {
    flex: 0 0 16rem;
}

.home-potd-grid,
.home-latest-grid {
    display: flex;
    flex-direction: column;
    gap: 2rem;
}

/*
 * The one shared image-grid card (partials/_puzzle_tile.html.twig) used on /gallery, editorial
 * albums and a user's own virtual album — one source of markup and one source of styling, no
 * per-page variation. Static layout (thumbnail, then caption lines below it) follows
 * thejigsawpuzzles.com, the reference named in docs/tz-photo-gallery.md itself; the hover style/
 * piece-count picker is this project's own addition on top of that, not something the reference
 * has. Left/right side columns (.album-columns et al) below are the same shell on all three pages
 * too, previously duplicated three times as inline <style> blocks per template.
 *
 * .gallery-* class names predate this being shared across three pages (this was /gallery's own
 * markup originally) — kept rather than renamed, since renaming here would just be churn with no
 * behavioral difference.
 */
.album-columns {
    display: flex;
    gap: 2rem;
    align-items: flex-start;
}

.album-meta {
    flex: 0 0 16rem;
}

.album-grid-col {
    flex: 1 1 auto;
    min-width: 0;
}

/* Fixed 3-column grid (not responsive auto-fill): matches the reference's dense grid, and a known
   column count is what the hover picker's own containment relies on being predictable — see
   body.gallery-layout below for why the page's overall content width was widened to fit this at
   the same per-card size the grid had before a left metadata column existed. */
.gallery-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1.2rem;
}

.puzzle-tile-title {
    margin: 0.65rem 0 0;
    font-size: 1.05rem;
    font-weight: 700;
    line-height: 1.25;
    color: var(--color-text);
}

.puzzle-tile-cut,
.puzzle-tile-author {
    margin: 0.2rem 0 0;
    font-size: 0.82rem;
    color: var(--color-text-soft);
}

.puzzle-tile-actions {
    margin: 0.35rem 0 0;
    font-size: 0.85rem;
}

/* Editorial-image admin actions (content_manager/admin only — partials/
   _editorial_image_admin_actions.html.twig): same compact row as .puzzle-tile-actions above.
   Edit and Delete are both plain <a> tags (see that partial's own doc for why Delete isn't a
   <button> — every browser's native push-button chrome couldn't be fully reset away), so no
   extra rule is needed to make them match each other: this is just .puzzle-tile-actions's own
   margin/font-size, reused under a different class so neither block depends on the other's
   markup ever staying identical. */
.puzzle-tile-admin-actions {
    margin: 0.35rem 0 0;
    font-size: 0.85rem;
}

/* .gallery-card wraps only the image + hover picker, deliberately not the caption below it — the
   picker must show strictly on hovering the image, never on hovering the caption text, and must
   never itself cover that caption (it can't, being a sibling positioned only inside this box). */
.gallery-card {
    position: relative;
}

.gallery-link {
    position: relative;
    display: block;
    aspect-ratio: 4 / 3;
    overflow: hidden;
    border-radius: 1rem;
    border: 1px solid var(--color-border);
    background: linear-gradient(180deg, rgba(255, 251, 245, 0.9), rgba(246, 236, 222, 0.95));
    box-shadow: 0 10px 24px var(--color-shadow);
    text-decoration: none;
}

.gallery-link img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* Piece-count corner badge (thejigsawpuzzles.com shows this as a yellow jigsaw-piece shape with
   the count on it — a plain rounded tag in the site's own accent color reads the same way without
   needing an actual piece-shaped clip-path). Purely decorative: pointer-events:none so it can
   never block a click reaching .gallery-link underneath, and it sits in a corner specifically
   because the hover picker (.gallery-hover-overlay) is centered and smaller than the card, leaving
   corners exposed both at rest and on hover. */
.puzzle-tile-badge {
    position: absolute;
    top: 0.5rem;
    left: 0.5rem;
    z-index: 3;
    min-width: 1.8rem;
    padding: 0.25rem 0.45rem;
    border-radius: 999px;
    background: var(--color-theme-1);
    color: #fff9f2;
    font-size: 0.72rem;
    font-weight: 800;
    text-align: center;
    box-shadow: 0 4px 10px var(--color-shadow);
    pointer-events: none;
}

/* docs/tz-photo-gallery.md, "Визуальный статус 'пройден'" — a flat white wash over the photo
   itself (10% opacity), never a change to the resize file on disk. Lives inside .gallery-link (not
   .gallery-card) specifically so it inherits that link's own border-radius/overflow:hidden clip,
   same rounded corners as the photo it's washing out. z-index below the piece-count badge (3) and
   the completed ribbon (see below) so neither gets dimmed along with the photo. Supplementary to
   the ribbon — on its own (no text) it was confirmed too subtle to read as a status indicator. */
.puzzle-tile-completed-wash {
    position: absolute;
    inset: 0;
    z-index: 1;
    background: rgba(255, 255, 255, 0.1);
    pointer-events: none;
}

/* Diagonal corner ribbon — replaces an earlier small checkmark badge that rendered correctly but
   was too subtle to notice. Classic ribbon technique: a fixed-width band rotated 45°, positioned
   so it crosses the top-right corner and gets clipped flush by .gallery-link's own
   overflow:hidden + border-radius, the same way a physical ribbon wraps a box corner. Text comes
   from ui_string('puzzle.solved_ribbon_label') (EN "SOLVED", RU "Пройдено") — sized/spaced to fit
   both without wrapping. */
.puzzle-tile-completed-ribbon {
    position: absolute;
    top: 0.85rem;
    right: -2.6rem;
    z-index: 3;
    width: 10rem;
    padding: 0.3rem 0;
    text-align: center;
    background: var(--color-theme-2);
    color: #fff9f2;
    font-size: 0.68rem;
    font-weight: 800;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    white-space: nowrap;
    transform: rotate(45deg);
    box-shadow: 0 4px 10px var(--color-shadow);
    pointer-events: none;
}

/* A compact floating window, same visual language as .gallery-count-list (the size picker) —
   NOT a full-bleed scrim stretched to the card's own edges the way this used to be. Sized to its
   own content (5 style rows) and centered over the image, so most of the thumbnail stays visibly
   uncovered on hover, same as the size picker never covers its own card. pointer-events stays
   none on this container FOREVER, even while visible: only the actual buttons below
   (.gallery-style-link, .gallery-count-list) re-enable it — belt-and-suspenders with the window
   now being small anyway, so a click on the (now mostly exposed) thumbnail still reaches
   .gallery-link underneath even for the sliver the window itself still overlaps. */
.gallery-hover-overlay {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -46%);
    z-index: 4;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    width: min(12rem, calc(100% - 1.6rem));
    padding: 0.7rem;
    border-radius: 1rem;
    background: rgba(250, 244, 235, 0.97);
    border: 1px solid rgba(255, 255, 255, 0.5);
    box-shadow: 0 16px 36px rgba(8, 10, 14, 0.28);
    opacity: 0;
    pointer-events: none;
    transition: opacity 160ms ease, transform 160ms ease;
}

/* Hovering (or keyboard-focusing) the image specifically — not the card's caption, which lives
   outside .gallery-card entirely — is what reveals this. Nothing else can hold it open: no click-
   toggled class, no JS state, so it can't get stuck showing once the pointer actually leaves.
   pointer-events is deliberately NOT re-enabled here (see comment above) — only the buttons inside
   opt back in, on their own hover/focus rules below. */
.gallery-card:hover .gallery-hover-overlay,
.gallery-card:focus-within .gallery-hover-overlay {
    opacity: 1;
    transform: translate(-50%, -50%);
}

.gallery-style-group {
    position: relative;
    flex: 0 0 auto;
}

.gallery-style-link {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.65rem;
    width: 100%;
    min-height: 2.15rem;
    padding: 0.45rem 0.6rem 0.45rem 0.7rem;
    border-radius: 10px;
    border: 1px solid var(--color-border);
    background: rgba(255, 255, 255, 0.78);
    color: var(--color-text);
    text-decoration: none;
    transition: background 150ms linear, transform 150ms ease;
}

/* Re-enables pointer-events only while the card is actually hovered/focused — inherited "none"
   from .gallery-hover-overlay the rest of the time, so an invisible button can never sit on top of
   the plain thumbnail intercepting a click before the picker has even faded in. */
.gallery-card:hover .gallery-style-link,
.gallery-card:focus-within .gallery-style-link {
    pointer-events: auto;
}

.gallery-style-name {
    min-width: 0;
    font-size: 0.88rem;
    font-weight: 700;
}

.gallery-style-meta {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    flex: 0 0 auto;
}

.gallery-style-link em {
    font-size: 0.62rem;
    font-weight: 700;
    font-style: normal;
    text-transform: uppercase;
    color: var(--color-text-soft);
    border: 1px solid var(--color-border-strong);
    border-radius: 6px;
    padding: 2px 4px;
}

.gallery-style-arrow {
    display: inline-flex;
    align-items: center;
    color: var(--color-text-soft);
}

.gallery-style-link:hover,
.gallery-style-link:focus-visible {
    background: #fff;
    text-decoration: none;
}

/* Flies out sideways from its own style-link, not downward — .gallery-style-group above is its
   positioned anchor. The gap you SEE between the button and this panel is padding-left on this
   same element, not an outer margin/offset: padding is still part of this box's hoverable area, so
   dragging the mouse across that visual gap never leaves .gallery-style-group and the flyout can't
   vanish mid-move (the previous calc(100% + 10px) outer-margin gap did exactly that). visibility,
   not display:none, so a keyboard/focus-visible flip still works; pointer-events opts back in only
   while shown, same reasoning as .gallery-style-link above.
   top:0 (not centered on the button) is what the next rule's bottom-anchor flip needs to mirror —
   see the last-style rule below for why the last row in the stack anchors the opposite way. */
.gallery-count-list {
    position: absolute;
    top: 0;
    left: 100%;
    display: grid;
    /* Single column, not a 2-up grid: the grid's own gutter (1.2rem) is narrower than any multi-
       column flyout could ever be, so a single narrow column is what keeps the overlap onto a
       neighboring card's thumbnail as small as it can be while the numbers stay legible. */
    grid-template-columns: minmax(0, 1fr);
    gap: 0.3rem;
    min-width: 4rem;
    padding: 0.5rem 0.5rem 0.5rem 0.6rem;
    border-radius: 0.9rem;
    background: rgba(250, 244, 235, 0.97);
    border: 1px solid rgba(255, 255, 255, 0.15);
    box-shadow: 0 16px 36px rgba(8, 10, 14, 0.16);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    z-index: 6;
    transition: opacity 140ms ease;
}

.gallery-style-group:hover .gallery-count-list,
.gallery-style-group:focus-within .gallery-count-list {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
}

/* Which style this is, or which card, says nothing about whether a downward-opening list would
   actually run past the bottom of the browser window — that depends on scroll position and window
   height, which only JS can know. assets/js/gallery-picker.js measures the real distance to the
   bottom of the viewport on hover/focus and toggles this class; it is never applied by CSS alone
   (unlike the structural left/right flip below, which a fixed 3-column grid makes predictable
   without measuring anything). */
.gallery-count-list.gallery-count-list--flip-up {
    top: auto;
    bottom: 0;
}

/* Rightmost column of .gallery-grid's fixed 3-column layout has no room to its right (that's
   exactly what the old live bug report screenshots showed: the panel bleeding into a neighboring
   card, or the size flyout landing on top of one) — flip it to open toward the left of the button
   instead. Direct child of .gallery-grid is the <article class="puzzle-tile">, so nth-child(3n)
   targets that, not .gallery-card, which is nested one level deeper. */
.gallery-grid > .puzzle-tile:nth-child(3n) .gallery-count-list {
    left: auto;
    right: 100%;
    padding: 0.5rem 0.6rem 0.5rem 0.5rem;
}

.gallery-count-list a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 1.85rem;
    padding: 0.3rem 0.4rem;
    border-radius: 0.6rem;
    background: rgba(255, 255, 255, 0.78);
    border: 1px solid var(--color-border);
    color: var(--color-text);
    font-size: 0.75rem;
    font-weight: 700;
    text-decoration: none;
    transition: transform 140ms ease, background 140ms ease;
}

.gallery-count-list a:hover,
.gallery-count-list a:focus-visible {
    background: #fff;
    transform: translateY(-1px);
    text-decoration: none;
}

/* Restores the ~360px-per-card size the grid had before a left metadata column existed (see
   GalleryController's own docblock for where that number comes from) — the left column took
   width away from the same fixed --content-width every other page shares, so this widens just
   this page's own content area instead of shrinking the grid back to 2 columns or narrowing the
   left column. Scoped to body.gallery-layout so no other page's max-width changes. */
body.gallery-layout .site-content {
    width: min(1440px, calc(100vw - 2rem));
    max-width: none;
    margin: 0 auto;
}

/*
 * Puzzle-solving page (image/editorial.html.twig, image/user.html.twig — extend base.html.twig
 * like every other page, opting into this fullscreen layout via {% block body_class %}puzzle-body
 * {% endblock %}). Ported from the old SvelteKit app's PuzzleLayout.svelte + PuzzleCanvasPage.svelte
 * (topbar/title layout) and PuzzleCanvasControls.svelte (the four overlaid control groups).
 * Positions/proportions follow the reference screenshot (puzzle-web/docs/original.jpg); colors use
 * this site's own palette instead of the old app's --control-* variables, since the two apps
 * deliberately use different palettes overall.
 *
 * Scoped entirely under `body.puzzle-body` so it can never affect any other page: the shared
 * header/footer/content region only becomes a fixed-height flex column (instead of the normal
 * scrolling document every other page uses) when this class is present. The header's own logo
 * link stays visible and normally laid out (matching every other page); the footer is left empty
 * by the puzzle templates and collapses to zero height (padding/border explicitly zeroed here —
 * an *empty* footer block should mean zero space, not an invisible strip still eating room from
 * the canvas). `.puzzle-page` itself fills 100% of `.site-content` (its flex-constrained parent)
 * rather than the raw viewport directly, since it's no longer guaranteed to be body's only child.
 */
body.puzzle-body {
    height: 100dvh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

body.puzzle-body .site-header {
    flex: 0 0 auto;
}

body.puzzle-body .site-footer {
    flex: 0 0 auto;
    padding: 0;
    border-top: none;
}

body.puzzle-body .site-content {
    width: 100%;
    max-width: none;
    margin: 0;
    padding: 0;
    flex: 1 1 auto;
    min-height: 0;
    /* column, not the flex default row: base.html.twig's flash-message <p> loop sits here too,
       ahead of {% block body %}'s .puzzle-page div — they need to stack, not sit side by side. */
    display: flex;
    flex-direction: column;
}

.puzzle-page {
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: 1.5rem;
    overflow: auto;
    /* min-content, not the bare flex default: this element is itself a flex item of
       body.puzzle-body .site-content, and its own overflow (auto, right above) is what's
       supposed to turn into scrollbars below the floor — but that same non-visible overflow is
       exactly what disables the implicit automatic-minimum-size protection a flex item normally
       gets, per the flex spec's own carve-out for that case. An explicit keyword isn't subject to
       that carve-out (it only applies when the computed value is literally `auto`), so this keeps
       .puzzle-page itself from being shrunk below what its children (the 730x550 floor among
       them) need, before its own scrollbars ever get a chance to appear. */
    min-height: min-content;
}

.puzzle-topbar {
    display: flex;
    align-items: center;
    gap: 1rem;
    flex: 0 0 auto;
}

.puzzle-topbar-left {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.puzzle-topbar-left a,
.puzzle-topbar-left button.puzzle-topbar-button {
    padding: 0.7rem 1.1rem;
    border-radius: 999px;
    border: 1px solid var(--color-border-strong);
    background: var(--color-surface-0);
    color: var(--color-text);
    font-size: 0.875rem;
    font-weight: 500;
    box-shadow: 0 2px 8px var(--color-shadow);
    white-space: nowrap;
    font-family: inherit;
    cursor: pointer;
}

.puzzle-topbar-left a:hover,
.puzzle-topbar-left button.puzzle-topbar-button:hover {
    background: var(--color-bg-1);
    border-color: var(--color-theme-1);
    color: var(--color-theme-1);
    text-decoration: none;
}

/*
 * Three-column body (docs/tz-photo-gallery.md, "Страница изображения": left = image metadata,
 * center = the image itself, right = actions) — added after an audit found the puzzle page had
 * never actually built this structure, just a fullscreen canvas. Proportions/general idea
 * ("narrow-wide-narrow", compact side columns not competing with the canvas) follow a competitor
 * reference (thejigsawpuzzles.com's solve-screen layout); content is strictly the TZ's own literal
 * field list, not that reference's actual features (no "related puzzles", no promo content).
 *
 * Same flex structure as album/editorial.html.twig's .album-columns/.album-meta/.album-grid-col
 * (fixed-width side column(s) + one flexible center) for consistency — note that pattern itself
 * has no responsive breakpoint anywhere in this codebase (checked: zero @media rules existed
 * before this one). The stacking fallback below is new, not a reuse of an existing convention.
 */
.puzzle-columns {
    flex: 1 1 auto;
    display: flex;
    gap: 1.5rem;
    align-items: stretch;
}

.puzzle-meta-left,
.puzzle-actions-right {
    flex: 0 0 16rem;
    overflow-y: auto;
}

.puzzle-meta-left h1 {
    margin: 0 0 0.4rem;
    font-family: var(--font-display);
    font-size: clamp(1.5rem, 2.6vw, 2.2rem);
    line-height: 1.05;
    letter-spacing: -0.03em;
    color: var(--color-text);
}

.puzzle-image-author {
    margin: 0 0 0.6rem;
    font-size: 0.9rem;
    color: var(--color-text-soft);
}

.puzzle-subtitle {
    margin: 0;
    font-size: 0.8rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-text-soft);
}

.puzzle-actions-right {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    align-items: flex-start;
}

/*
 * .puzzle-board-column is now THE single bordered/background "box" the game lives in — the frame
 * used to belong to .puzzle-board-wrap alone; it moved up here so the completion panel renders
 * *inside the same frame*, pinned to its bottom edge, rather than as a visually separate second box
 * underneath it (an earlier version did that — wrong per docs/tz-photo-gallery.md's "Экран
 * завершения пазла": "та же коробка", not a new one). .puzzle-board-wrap below is now just the
 * canvas's own flex region inside that frame, no border/background of its own.
 *
 * Why the always-on-top toolbar controls (.puzzle-controls-*) still end up in the right place: they
 * anchor (position:absolute) to .puzzle-board-wrap, not to this outer box — when the panel appears
 * and .puzzle-board-wrap's flex:1 1 auto shrinks to make room for it, those controls shrink along
 * with their own positioning context and end up sitting just above the panel, never on top of it.
 */
/*
 * min-width here is explicit, not left to the flex default: a flex item's *automatic* minimum
 * width (the implicit protection a plain `min-width: auto` gives a shrinkable flex item) is
 * specified to collapse to 0 whenever that item's own `overflow` is not `visible` — and this
 * element's overflow is `hidden` (kept for the rounded-corner clip around the canvas). Relying on
 * the implicit default here is exactly the kind of spec corner real engines have disagreed on
 * historically; an explicit length sidesteps the ambiguity instead of hoping every engine treats
 * it the same way.
 */
.puzzle-board-column {
    flex: 1 1 auto;
    min-width: 800px;
    display: flex;
    flex-direction: column;
    border-radius: 1rem;
    border: 1px solid var(--color-border);
    background: var(--color-bg-0);
    box-shadow: 0 18px 40px var(--color-shadow);
    overflow: hidden;
}

/*
 * Floor for the canvas's own region. Height (550px) still comes from the client's legacy template
 * (Z:\Puzzle\tpl\tpl\_theme_puzzle.tpl, the classic/html5-v1 game container's inline
 * `style="position:relative;width:730px;height:550px;"`) — the only concrete game-area size on
 * record for this site; re-checked against Z:\Puzzle\tpl\tpl for the modern (html5v2) engine and
 * found nothing more specific — its #gameClientContainer has an empty inline style, and its own
 * CSS bundle (rukukijigsaw) isn't part of this tree. Width (800px) was raised from that same
 * 730px by explicit request, no longer a direct port. Below this floor, .puzzle-page's own
 * overflow: auto takes over (scrollbars) instead of the canvas shrinking further;
 * .puzzle-board-column above no longer resets min-width to 0, so this floor isn't squeezed away
 * by the surrounding flex chain before it ever overflows anything. The three-column row layout is
 * now the only layout at every width — the earlier narrow-viewport fallback that switched
 * .puzzle-columns to a stacked column (board moving below the title/metadata instead of staying
 * in place) was removed: that relocation was itself reported as the bug ("canvas становится
 * меньше и перемещается влево под имя пазла"), not a fix for one.
 */
.puzzle-board-wrap {
    position: relative;
    flex: 1 1 auto;
    min-width: 800px;
    min-height: 550px;
}

/*
 * This is now the Fullscreen API's target element (see puzzle-page.js's fullscreenElement config)
 * precisely because every control panel/overlay below is already its DOM child — fullscreen-ing
 * only #puzzle-board (its own child, canvas host alone) would leave every one of them behind as a
 * sibling outside the fullscreen element, which is the bug this exists to not have.
 *
 * The browser's own fullscreen UA styles already make this element cover the viewport (replacing
 * its normal flex-item sizing) — min-width/min-height are reset to 0 here only so that floor from
 * the non-fullscreen layout can never visibly constrain anything mid-transition. Background is set
 * explicitly (both on the element and its ::backdrop) because a fullscreen element's normal
 * ancestors — here .puzzle-board-column, which actually owns the board's visible background — are
 * no longer painted behind it once it's promoted to the top layer.
 */
.puzzle-board-wrap:fullscreen {
    width: 100%;
    height: 100%;
    min-width: 0;
    min-height: 0;
    background: var(--color-bg-0);
}

.puzzle-board-wrap::backdrop {
    background: var(--color-bg-0);
}

/* Hidden by default (the [hidden] attribute, not a class — puzzle-page.js just toggles it) until
   the puzzle is known to be completed, either just now or already on a resumed visit. No heading/
   banner text and no "announcement" treatment — just a row of buttons styled identically to the
   HINT/PULL/EDGE/GHOST toolbar group below it (.puzzle-complete-actions a/button below matches
   .puzzle-controls-bottom-left button's size/font/padding exactly), so it reads as another control
   row rather than a modal-style notification.
   .puzzle-complete-panel[hidden] below is NOT redundant with the plain [hidden] attribute: a bare
   `display: flex` rule on an author stylesheet always wins over the browser's own UA-stylesheet
   `[hidden] { display: none }` default, regardless of specificity — cascade origin beats selector
   specificity here. Without this override the panel showed on every puzzle, solved or not,
   completely ignoring the `hidden` attribute puzzle-page.js was correctly toggling the whole time. */
.puzzle-complete-panel {
    position: absolute;
    z-index: 20;
    flex: 0 0 auto;
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 0.15rem;
    padding: 0.15rem;
    bottom: 1rem;
    left: 50%;
    transform: translateX(-50%);
}

.puzzle-complete-panel[hidden] {
    display: none;
}

.puzzle-complete-actions {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    flex-wrap: wrap;
    justify-content: center;
    padding: 0.35rem;
    border: 1px solid var(--color-border-strong);
    border-radius: 999px;
    background: var(--color-surface-0);
    box-shadow: 0 4px 14px var(--color-shadow);
}

/* Same selector list, same declared properties, as .puzzle-controls-bottom-left button in the
   toolbar group above — kept as a literal duplicate rather than a shared class so the toolbar's
   own rule isn't forced to also match an <a> element it doesn't otherwise need to know about. */
.puzzle-complete-actions a,
.puzzle-complete-actions button {
    height: 2.45rem;
    padding: 0 1.1rem;
    display: inline-flex;
    align-items: center;
    border: 1px solid var(--color-border-strong);
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.5);
    color: var(--color-text);
    font-size: 0.72rem;
    font-weight: 800;
    letter-spacing: 0.12em;
    font-family: inherit;
    text-decoration: none;
    cursor: pointer;
    transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s;
}

.puzzle-complete-actions a:hover,
.puzzle-complete-actions button:hover {
    transform: translateY(-1px);
    background: rgba(255, 255, 255, 0.85);
}

/*
 * position: absolute + inset: 0, not width/height: 100% — found live in Firefox (not Chromium):
 * when .puzzle-board-wrap's own height comes from its min-height floor overriding a smaller
 * flex-computed size (narrow window height, wide-enough window width to stay in the 3-column,
 * non-stacked layout), Firefox does not treat that height as "definite" for a plain block
 * child's height: 100% to resolve against — #puzzle-board measured 0px tall there, even though
 * .puzzle-board-wrap correctly measured the full 550px floor right next to it. Sizing via
 * absolute positioning against the padding box of the nearest positioned ancestor (.puzzle-
 * board-wrap, already position: relative for its other children below) sidesteps that
 * percentage-definiteness question entirely — the same technique already used for every other
 * child here (#puzzle-status, .puzzle-controls-*) and for the canvas element itself.
 */
#puzzle-board {
    position: absolute;
    inset: 0;
}

/* Pause overlay (thejigsawpuzzles.com reference) — same rect as #puzzle-board above (inset: 0
   against the same positioned ancestor, .puzzle-board-wrap), stacked above the canvas (z-index 15
   beats its implicit auto stacking) but explicitly below the control panels (z-index 20 — see
   .puzzle-controls-top-left's own block below), so the pause/timer panel stays visible and
   clickable on top of it regardless of DOM order. The veil is deliberately faint — just enough to
   read as "something is overlaid" without dimming the white pieces underneath it, matching the
   reference screenshot where the board looks essentially unchanged except for the badge.
   puzzle-page.js's setPaused() toggles [hidden]; the [hidden] override below exists for the same
   cascade-origin reason as .puzzle-complete-panel[hidden] above. */
.puzzle-pause-overlay {
    position: absolute;
    inset: 0;
    z-index: 15;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(20, 20, 24, 0.08);
    cursor: pointer;
}

.puzzle-pause-overlay[hidden] {
    display: none;
}

.puzzle-pause-overlay-badge {
    padding: 0.9rem 1.6rem;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    font-size: 1.05rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    text-align: center;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}

#puzzle-status {
    position: absolute;
    top: 1rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 25;
    padding: 0.4rem 0.9rem;
    border-radius: 999px;
    background: var(--color-surface-0);
    border: 1px solid var(--color-border);
    box-shadow: 0 4px 12px var(--color-shadow);
    font-size: 0.8rem;
    color: var(--color-text-soft);
}

/* Nothing is shown once the board is actually playable — matching the original, which has no
   persistent status text at all once loaded. */
#puzzle-status.is-ready {
    display: none;
}

.puzzle-controls-top-left,
.puzzle-controls-bottom-left,
.puzzle-controls-bottom-right {
    position: absolute;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.35rem;
    border: 1px solid var(--color-border-strong);
    border-radius: 999px;
    background: var(--color-surface-0);
    box-shadow: 0 4px 14px var(--color-shadow);
    backdrop-filter: blur(14px) saturate(1.2);
}

.puzzle-controls-top-left {
    top: 1rem;
    left: 1rem;
}

.puzzle-controls-bottom-left {
    left: 1rem;
    bottom: 1rem;
}

.puzzle-controls-bottom-right {
    right: 1rem;
    bottom: 1rem;
}

.puzzle-controls-top-left button,
.puzzle-controls-bottom-left button,
.puzzle-controls-bottom-right button {
    border: 1px solid var(--color-border-strong);
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.5);
    color: var(--color-text);
    cursor: pointer;
    font-family: inherit;
    transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s;
}

.puzzle-controls-top-left button:hover:not(:disabled),
.puzzle-controls-bottom-left button:hover:not(:disabled),
.puzzle-controls-bottom-right button:hover:not(:disabled) {
    transform: translateY(-1px);
    background: rgba(255, 255, 255, 0.85);
}

.puzzle-controls-top-left button:disabled,
.puzzle-controls-bottom-left button:disabled,
.puzzle-controls-bottom-right button:disabled {
    cursor: not-allowed;
    opacity: 0.42;
}

.puzzle-zoom-out,
.puzzle-zoom-in,
.puzzle-zoom-reset {
    height: 2.45rem;
    min-width: 2.75rem;
    font-size: 1.05rem;
    font-weight: 700;
}

.puzzle-zoom-reset {
    min-width: 3.35rem;
    font-size: 0.78rem;
    letter-spacing: 0.06em;
}

.puzzle-controls-bottom-left button {
    display: grid;
    width: 2.45rem;
    height: 2.45rem;
    padding: 0;
    place-items: center;
}

.puzzle-controls-bottom-left button.is-active {
    color: #fff9f2;
    background: var(--color-theme-2);
    border-color: var(--color-theme-2);
}

.puzzle-controls-bottom-right button {
    display: grid;
    width: 2.45rem;
    height: 2.45rem;
    place-items: center;
    padding: 0;
}

.puzzle-controls-bottom-right button.is-active {
    color: #fff9f2;
    background: var(--color-theme-2);
    border-color: var(--color-theme-2);
}

/* Applied by puzzle-page.js's applyCompletedControlsState() after a real completion (or reopening
   one already solved) — pause and timer-toggle no longer make sense once the puzzle is done, so
   they're hidden; the time value and fullscreen stay, matching the reference. A class on the panel
   container, not element removal, so "Start over" un-hiding them again is just removing this class. */
.puzzle-controls-bottom-right.puzzle-completed #puzzle-timer-pause,
.puzzle-controls-bottom-right.puzzle-completed #puzzle-timer-toggle {
    display: none;
}

/*
 * Same .puzzle-completed class name as above, deliberately — applyCompletedControlsState() in
 * puzzle-page.js toggles it on TWO different elements (see its own comment): .puzzle-board-wrap
 * here, unconditionally on completion, vs. .puzzle-controls-bottom-right above, gated by the
 * timer-was-on/admin-setting check. Scoped by ancestor selector, so the two never collide — this
 * block only matches descendants of .puzzle-board-wrap.puzzle-completed, not of the bottom-right
 * panel's own (differently-gated) .puzzle-completed state.
 *
 * HINT/PULL/EDGE/GHOST, MENU+zoom, and the image-preview toggle all stop making sense once the
 * puzzle is solved — hidden by class (not removed) so "Start over" un-hiding them is just removing
 * this class; .puzzle-controls-bottom-right (time/pause/mute/fullscreen) is untouched by this rule.
 */
.puzzle-board-wrap.puzzle-completed .puzzle-controls-top-left,
.puzzle-board-wrap.puzzle-completed .puzzle-controls-bottom-left,
.puzzle-board-wrap.puzzle-completed .puzzle-preview-toggle {
    display: none;
}

/*
 * The completed puzzle's title (thejigsawpuzzles.com reference) — centered at the board's own top
 * edge, same position #puzzle-status uses while loading (the two never show at once: this only
 * appears once the puzzle is ready and solved, long after the loading status has hidden itself).
 * The assembled picture can reach all the way to the board's top edge, so this deliberately sits
 * ON TOP of that edge with its own shadow for separation, rather than trying to avoid overlapping
 * it entirely — moving it elsewhere would mean repositioning the canvas image itself, out of scope
 * here. -webkit-line-clamp (not an ordinary 1-line ellipsis): long titles wrap to 2 lines before
 * truncating, widely supported despite the vendor prefix.
 */
.puzzle-completed-title {
    display: none;
    position: absolute;
    z-index: 18;
    top: 1rem;
    left: 50%;
    transform: translateX(-50%);
    max-width: calc(100% - 2rem);
    padding: 0.5rem 1.1rem;
    border-radius: 999px;
    border: 1px solid var(--color-border);
    background: var(--color-surface-0);
    box-shadow: 0 6px 18px var(--color-shadow);
    color: var(--color-text);
    font-weight: 700;
    font-size: 0.95rem;
    text-align: center;
    overflow: hidden;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

.puzzle-board-wrap.puzzle-completed .puzzle-completed-title {
    display: -webkit-box;
}

.puzzle-timer-value {
    display: grid;
    height: 2.45rem;
    min-width: 4.2rem;
    padding: 0 0.65rem;
    place-items: center;
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
    font-size: 0.9rem;
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    color: var(--color-text);
}

.puzzle-control-icon {
    display: block;
    width: 1.15rem;
    height: 1.15rem;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Background picker, FPS pill, ghost preview panel — ported from PuzzleCanvasControls.svelte /
   PuzzleCanvasPreview.svelte (backend/public/assets/js/puzzle-page.js builds the DOM for these). */
.puzzle-background-control {
    position: relative;
}

.puzzle-background-button {
    display: grid;
    width: 2.45rem;
    height: 2.45rem;
    padding: 0;
    place-items: center;
    border: 1px solid var(--color-border-strong);
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.5);
    cursor: pointer;
}

.puzzle-background-button:hover {
    background: rgba(255, 255, 255, 0.85);
}

.puzzle-background-swatch {
    display: block;
    width: 1.4rem;
    height: 1.4rem;
    border: 1px solid rgba(255, 255, 255, 0.65);
    border-radius: 999px;
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
}

.swatch-linen {
    background: linear-gradient(135deg, #fffaf3, #e7d2b7);
}

.swatch-charcoal {
    background: linear-gradient(135deg, #202829, #5a4a38);
}

.swatch-blueprint {
    background:
        linear-gradient(rgba(255, 255, 255, 0.16) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255, 255, 255, 0.16) 1px, transparent 1px), #124362;
    background-size: 8px 8px;
}

.swatch-sand-grid {
    background: linear-gradient(45deg, rgba(104, 72, 31, 0.16) 25%, transparent 25%), #e4c99c;
    background-size: 8px 8px;
}

.swatch-sage-fabric {
    background: repeating-linear-gradient(90deg, rgba(33, 65, 48, 0.18) 0 1px, transparent 1px 4px), #9db693;
}

.puzzle-background-menu {
    position: absolute;
    top: calc(100% + 0.5rem);
    left: 0;
    z-index: 30;
    display: grid;
    width: 11.5rem;
    gap: 0.3rem;
    padding: 0.4rem;
    border: 1px solid var(--color-border-strong);
    border-radius: 1rem;
    background: var(--color-surface-0);
    box-shadow: 0 12px 26px var(--color-shadow);
    backdrop-filter: blur(14px) saturate(1.2);
}

.puzzle-background-option {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.4rem 0.5rem;
    border: 1px solid transparent;
    border-radius: 0.7rem;
    background: transparent;
    color: var(--color-text);
    font-size: 0.8rem;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
}

.puzzle-background-option:hover {
    background: rgba(255, 255, 255, 0.6);
}

.puzzle-fps-pill {
    position: absolute;
    z-index: 20;
    right: 1rem;
    bottom: 5.1rem;
    padding: 0.32rem 0.7rem;
    border: 1px solid var(--color-border-strong);
    border-radius: 999px;
    background: var(--color-surface-0);
    box-shadow: 0 4px 12px var(--color-shadow);
    color: var(--color-text-soft);
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
    font-size: 0.72rem;
    font-weight: 700;
}

.puzzle-preview-toggle {
    position: absolute;
    z-index: 25;
    top: 1rem;
    right: 1rem;
    display: grid;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    place-items: center;
    border: 1px solid var(--color-border-strong);
    border-radius: 999px;
    background: var(--color-surface-0);
    box-shadow: 0 4px 14px var(--color-shadow);
    color: var(--color-text);
    font-size: 1.2rem;
    cursor: pointer;
}

.puzzle-preview-toggle:hover {
    background: rgba(255, 255, 255, 0.85);
}

.puzzle-preview-panel {
    position: absolute;
    z-index: 30;
    top: 1rem;
    right: 1rem;
    border: 2px solid var(--color-border-strong);
    border-radius: 0.6rem;
    background: rgba(255, 255, 255, 0.2);
    box-shadow: 0 12px 28px var(--color-shadow);
    overflow: hidden;
}

.puzzle-preview-image {
    position: absolute;
    inset: -1px;
    background-position: center;
    background-repeat: no-repeat;
    background-size: cover;
    transition: opacity 100ms linear;
}

.puzzle-preview-resize-handle {
    position: absolute;
    z-index: 1;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    box-shadow: none;
    -webkit-appearance: none;
    appearance: none;
    outline: none;
    cursor: default;
}
.puzzle-preview-resize-handle:hover,
.puzzle-preview-resize-handle:active {
    background: transparent;
    box-shadow: none;
    transform: none;
}

.puzzle-preview-resize-handle-left {
    top: 0;
    bottom: 0;
    left: -7px;
    width: 14px;
    cursor: ew-resize;
}
.puzzle-preview-resize-handle-left:hover {
    cursor: ew-resize;
}

.puzzle-preview-resize-handle-bottom {
    right: 0;
    bottom: -7px;
    left: 0;
    height: 14px;
    cursor: ns-resize;
}
.puzzle-preview-resize-handle-bottom:hover {
    cursor: ns-resize;
}

.puzzle-preview-controls {
    position: absolute;
    left: 0.4rem;
    bottom: 0.4rem;
    z-index: 2;
    display: flex;
    gap: 0.3rem;
}

.puzzle-preview-control {
    display: grid;
    width: 1.9rem;
    height: 1.9rem;
    padding: 0;
    place-items: center;
    border: 1px solid var(--color-border-strong);
    border-radius: 0.5rem;
    background: rgba(255, 252, 246, 0.9);
    color: var(--color-text);
    box-shadow: 0 4px 12px var(--color-shadow);
    font-size: 1.1rem;
    font-weight: 700;
    cursor: pointer;
}

.puzzle-preview-control:disabled {
    cursor: not-allowed;
    opacity: 0.42;
}

/* ─── theme2024 layout fixes ──────────────────────────────────────────────────
 *
 * .gbNoFeature — used by _theme_puzzle.tpl to hide unimplemented buttons.
 * app-theme.css does not define it; we own this rule.
 *
 * .theme2024 #gameClientContainer — the flex:1 child of #tjp-client-and-da-container.
 * app-theme.css only gives it `position: relative` and `overflow: hidden` (via
 * .tjp-client-container). Without `display: flex; flex-direction: column`, its
 * child .puzzle-board-wrap cannot use `flex: 1 1 auto` to fill the available
 * height, and the canvas stays at the min-height floor (550 px) even when the
 * viewport is taller.
 *
 * Responsive nav override — always-visible working buttons:
 * app-theme.css hides .rightnav at ≤1690 px and substitutes a compact
 * .combined-rightnav-container popup. We override this so every working button
 * (Back, Edit, Delete, Create Puzzle, My Puzzles, My Profile, Sign Out) is
 * always in the nav bar. When the bar is too narrow for a single row it wraps
 * to a second row; the game canvas adjusts via the existing ResizeObserver.
 *
 * Overridden etalon rules (all in app-theme.css):
 *   @media (max-width:1690px) { .rightnav { display:none } }
 *   @media (max-width:1690px) { .combined-rightnav-container { display:block } }
 *   @media (max-width:1890px) { .hamburger-container { display:flex } }
 *   .theme2024 main { position:absolute; top:var(--body-margin-top) }
 *   .theme2024-mainnav { height:var(--mainnav-height) }  (fixed 40 px)
 */

.gbNoFeature {
    display: none !important;
}

.theme2024 #gameClientContainer {
    display: flex;
    flex-direction: column;
    container-type: inline-size;
}

/*
 * Landscape cap for the board: its height is the smaller of the space left under the nav and
 * width × 550/730 (the original game's 730×550 playfield ratio), so narrowing the window can no
 * longer stretch the field into a portrait shape. flex-shrink lets it give way to a shorter
 * window down to .puzzle-board-wrap's own 550px floor; any height left over stays below the
 * board. Fullscreen is untouched: there the board covers the whole screen as before.
 */
.theme2024 #gameClientContainer > .puzzle-board-wrap {
    flex: 0 1 auto;
    height: calc(100cqw * 550 / 730);
}

.theme2024 #gameClientContainer > .puzzle-board-wrap:fullscreen {
    height: 100%;
}

/*
 * Overridden etalon rules (app-theme.css): #tjp-daright-container { width: 356px } and, at
 * @media (max-width: 1386px), { width: 180px }. That step halves the banner column at once and
 * the board jumps wider by the same ~176px mid-resize. Here the column narrows linearly instead:
 * 356px at a 1386px-wide window, down to 180px at about 1030px, so the board's width changes as
 * smoothly as the window's. The dablocked box keeps its own 1386px height switch — it changes
 * nothing about the board.
 */
.theme2024 #tjp-daright-container {
    width: clamp(180px, calc(100vw * 176 / 356 - 329px), 356px);
}

/* Admin nav buttons in the theme2024 mainnav */
.theme2024-mainnav .puzzle-admin-nav {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-left: 8px;
}

.theme2024-mainnav .puzzle-admin-nav a {
    color: #627cb2;
    font-weight: bold;
    font-size: 13px;
    padding: 3px 10px;
    border: 1px solid #b8c6d5;
    border-radius: 14px;
    background: #f5f8ff;
    white-space: nowrap;
    text-decoration: none;
}

/* ─── Responsive nav: always-visible buttons ──────────────────────────────── */

/* Body as flex column: mainnav's actual rendered height determines where
 * main starts, so the canvas is never hidden behind a wrapped nav bar. */
body.theme2024 {
    display: flex !important;
    flex-direction: column !important;
    height: 100% !important;
}

/* Mainnav: remove the fixed 40 px height so it can grow when rows wrap. */
.theme2024 .theme2024-mainnav {
    height: auto !important;
    min-height: var(--mainnav-height) !important;
    flex-wrap: wrap !important;
}

/* Main: flex child filling all remaining space after header + mainnav.
 * Replaces position:absolute;top:150px, which breaks when the nav grows. */
.theme2024 main {
    position: relative !important;
    top: auto !important;
    bottom: auto !important;
    left: auto !important;
    right: auto !important;
    flex: 1 1 0% !important;
    min-height: 0 !important;
    overflow: hidden !important;
}

/* Game container still fills main absolutely, unchanged. */
.theme2024 main #tjp-client-and-da-container {
    position: absolute !important;
    inset: 0 !important;
}

/* Override: @media (max-width:1690px) { .rightnav { display:none } }.
 * Items may wrap within the rightnav itself at very narrow widths. */
.theme2024 .theme2024-mainnav .rightnav {
    display: flex !important;
    flex-wrap: wrap !important;
    gap: 8px !important;
}

/* Override: @media (max-width:1690px) { .combined-rightnav-container { display:block } }.
 * The compact popup is no longer needed as the sole access point. */
.theme2024 .theme2024-mainnav .combined-rightnav-container {
    display: none !important;
}

/* Override: @media (max-width:1890px) { .hamburger-container { display:flex } }.
 * hamburger-container is already gbNoFeature in the template; belt-and-braces. */
.theme2024 .theme2024-mainnav .hamburger-container {
    display: none !important;
}

/* Reduce back-button's right margin (etalon: 49 px) to save nav bar space. */
.theme2024 .theme2024-mainnav #back-button {
    margin-right: 12px !important;
}

.ccb { position: relative; }

.ccb-toggle {
    display: flex;
    align-items: center;
    width: 100%;
    padding: 2px 5px;
    gap: 4px;
    border: 1px solid #ccc;
    border-radius: 2px;
    background: #fff;
    color: #333;
    font-size: 11px;
    font-family: inherit;
    font-weight: normal;
    cursor: pointer;
    text-align: left;
    box-shadow: none;
    transition: none;
}

.ccb-toggle:hover { border-color: #888; background: #f8f8f8; transform: none; }
.ccb-toggle:active { transform: none; }

.ccb-display {
    flex: 1 1 auto;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    text-align: left;
}

.ccb-arrow { flex: 0 0 auto; font-size: 8px; color: #666; line-height: 1; }

.ccb-panel {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 999;
    max-height: 220px;
    overflow-y: auto;
    border: 1px solid #bbb;
    border-top: none;
    background: #fff;
    box-shadow: 0 3px 8px rgba(0,0,0,.18);
}

.ccb-item {
    display: flex;
    align-items: center;
    gap: 5px;
    padding: 3px 6px;
    font-size: 11px;
    font-weight: normal;
    color: #333;
    cursor: pointer;
    margin: 0;
}

.ccb-item:hover { background: #eef2ff; }

.ccb-item input[type="checkbox"] {
    flex: 0 0 auto;
    width: auto;
    padding: 0;
    margin: 0;
    border: 1px solid #aaa;
    border-radius: 2px;
    background: #fff;
    box-shadow: none;
    font-size: inherit;
}
