/* assets/css/site.css — The Puzzle Shelf
   One stylesheet, all of it. Colours live in :root; change them here and the
   whole site follows. */

:root {
    color-scheme: light;

    --board:    #16261F;   /* masthead, hero, footer — the felt the puzzle sits on */
    --board-2:  #0F1B16;   /* holes in the board, footer base */
    --paper:    #EFF1EC;   /* the page, and the face of a piece */
    --card:     #FFFFFF;
    --ink:      #14201B;   /* body text */
    --ink-soft: #4C5B54;   /* secondary text */
    --line:     #D2D9D2;   /* seams between cards, hairlines */
    --piece:    #E8C547;   /* the loose piece — the one accent */
    --piece-dk: #C9A521;
    --link:     #1F5E45;

    /* Everything below used to be written into the rules that used it. It is up
       here now because a second palette has to be able to reach all of it — a
       theme is only as good as the one colour that was left hard-coded. */
    --ink-onboard: #C6D1C9;               /* secondary text on the dark bands */
    --band-ink:    #EFF1EC;               /* primary text ON a band - NOT the page ground.

       These two used to be one token: --paper was both the page and the light
       colour written on a dark band, which works only while the page is the
       light one. In the dark theme --paper becomes the near-black ground, and
       every masthead link, hero heading and Buy button written in it went to
       near-black on a dark band. Same value as --paper here, so the light theme
       is exactly as it was; the dark palette gives it a light one. */
    --faint:       #9AA79F;               /* "photo to come" in an empty frame */
    --out:         #F6F7F4;               /* calendar days outside the month */
    --hover:       #EDF3EF;               /* a quiet hover ground */
    --veil:        rgba(15, 27, 22, .80); /* the hero card over the artwork */
    --veil-edge:   rgba(239, 241, 236, .10);
    --ghost-edge:  #47584F;               /* .btn-ghost, which only sits on board */
    --jig-line:    #2C3F36;               /* the holes in the hero jigsaw */
    --foot-ink:    #AFBCB3;
    --foot-faint:  #8D9A92;

    /* The tags. Each is a border, a ground and an ink, because a pale ground
       with dark text has to become a dark ground with pale text - inverting a
       single value would leave them unreadable. */
    --shots-bd:  #B9C6BD;
    --amz-bg:    #FDF6DC;  --amz-ink:    #6B5407;
    --easy-bd:   #9DBBA6;  --easy-bg:    #EDF5EF;  --easy-ink:   #2F5A3C;
    --med-bd:    #D8C68A;  --med-bg:     #FBF6E8;  --med-ink:    #6B5407;
    --hard-bd:   #D3A08F;  --hard-bg:    #FBEFEB;  --hard-ink:   #7A2E18;
    --poster-bd: #A9BCCB;  --poster-bg:  #EDF2F6;  --poster-ink: #2E4A5E;

    /* Which texture the page ground wears. Dark seams on light paper; the
       light-seam file on a dark page. Both already exist - the dark theme
       needs no new artwork. */
    --grid-body: url("/assets/img/puzzle-grid-ink.svg");

    --shelf-max: 1120px;
    --step: clamp(1rem, 0.6rem + 1.6vw, 1.6rem);

    --grid-w:     340px;                    /* one board across; smaller reads busier */
    --grid-h:     calc(var(--grid-w) * .6); /* the grid file is 500x300 */
    --grid-ink:   .06;                      /* seam strength on paper */
    --grid-paper: .10;                      /* seam strength on the dark board */

    /* The grid files carry no opacity of their own - these two are the only
       dials, so a lighter or heavier board is a one-line change. */
}

/* ------------------------------------------------------------- dark theme

   Two ways in, and they cannot be merged: a media query cannot be part of a
   selector list. Auto follows the device; data-theme on <html> is the admin's
   override and is written by the server, so there is no script and no flash of
   the wrong theme before it settles.

   :root:not([data-theme="light"]) is what makes "always light" beat a device
   set to dark. Keep the two blocks below identical. */

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        color-scheme: dark;

        --board:    #1B2C24;
        --board-2:  #142019;
        --paper:    #0F1713;
        --card:     #17211C;
        --ink:      #E4EAE5;
        --ink-soft: #A2B0A8;
        --line:     #2C3A33;
        --link:     #7FC9A5;

        --ink-onboard: #C6D1C9;
        --band-ink:    #E4EAE5;
        --faint:       #6E7C74;
        --out:         #131B16;
        --hover:       #1F2E27;
        --veil:        rgba(8, 14, 11, .82);
        --veil-edge:   rgba(239, 241, 236, .08);
        --ghost-edge:  #4A5C53;
        --jig-line:    #35493F;
        --foot-ink:    #A9B7AE;
        --foot-faint:  #7C8A82;

        --shots-bd:  #3A4C43;
        --amz-bg:    #33290B;  --amz-ink:    #F0D98A;
        --easy-bd:   #35563F;  --easy-bg:    #16261C;  --easy-ink:   #9ED8B2;
        --med-bd:    #58481D;  --med-bg:     #262009;  --med-ink:    #EBD693;
        --hard-bd:   #6B3527;  --hard-bg:    #2A1611;  --hard-ink:   #F0A78F;
        --poster-bd: #35505F;  --poster-bg:  #13212A;  --poster-ink: #A8CBE0;

        --grid-body: url("/assets/img/puzzle-grid-paper.svg");
        --grid-ink:  .05;
    }
}

:root[data-theme="dark"] {
    color-scheme: dark;

    --board:    #1B2C24;
    --board-2:  #142019;
    --paper:    #0F1713;
    --card:     #17211C;
    --ink:      #E4EAE5;
    --ink-soft: #A2B0A8;
    --line:     #2C3A33;
    --link:     #7FC9A5;

    --ink-onboard: #C6D1C9;
    --band-ink:    #E4EAE5;
    --faint:       #6E7C74;
    --out:         #131B16;
    --hover:       #1F2E27;
    --veil:        rgba(8, 14, 11, .82);
    --veil-edge:   rgba(239, 241, 236, .08);
    --ghost-edge:  #4A5C53;
    --jig-line:    #35493F;
    --foot-ink:    #A9B7AE;
    --foot-faint:  #7C8A82;

    --shots-bd:  #3A4C43;
    --amz-bg:    #33290B;  --amz-ink:    #F0D98A;
    --easy-bd:   #35563F;  --easy-bg:    #16261C;  --easy-ink:   #9ED8B2;
    --med-bd:    #58481D;  --med-bg:     #262009;  --med-ink:    #EBD693;
    --hard-bd:   #6B3527;  --hard-bg:    #2A1611;  --hard-ink:   #F0A78F;
    --poster-bd: #35505F;  --poster-bg:  #13212A;  --poster-ink: #A8CBE0;

    --grid-body: url("/assets/img/puzzle-grid-paper.svg");
    --grid-ink:  .05;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    background: var(--paper);
    color: var(--ink);
    font: 400 17px/1.6 Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
    letter-spacing: -0.005em;
}

h1, h2, h3, .wordmark {
    font-family: "Bricolage Grotesque", Inter, system-ui, sans-serif;
    font-weight: 700;
    letter-spacing: -0.03em;
    line-height: 1.05;
    margin: 0;
}

a { color: var(--link); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--ink); }

img { max-width: 100%; display: block; }

:focus-visible {
    outline: 3px solid var(--piece);
    outline-offset: 2px;
}

.wrap {
    max-width: var(--shelf-max);
    margin-inline: auto;
    padding-inline: var(--step);
}

.skip {
    position: absolute; left: -9999px;
}
.skip:focus {
    left: var(--step); top: .5rem; z-index: 10;
    background: var(--piece); color: var(--board);
    padding: .5rem .9rem; border-radius: 3px;
}

/* ----------------------------------------------------------------- texture

   The whole site sits on a board. Both grid files are the hero jigsaw's own
   geometry, stroked rather than filled, and they tile: the board is a 500x300
   rectangle with straight outer edges, so it repeats with no visible seam.
   Two tints only - dark seams for the paper, light seams for the dark bands.

   Every layer is a pseudo-element sitting behind its own content, so no
   markup carries decoration and none of it is reachable by a pointer or a
   screen reader. */

body::before,
.masthead::before,
.hero::before,
.colophon::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image: url("/assets/img/puzzle-grid-paper.svg");
    background-size: var(--grid-w) var(--grid-h);
    opacity: var(--grid-paper);
}

/* The page layer is fixed, so content slides over a board that stays put. It
   drifts by exactly one tile, which is why the loop closes without a jump -
   at 180s that is about 2px a second: felt rather than seen. */
body::before {
    position: fixed;
    z-index: -1;
    background-image: var(--grid-body);
    opacity: var(--grid-ink);
}

/* Each dark band holds its own seams behind its own content. */
.masthead, .hero, .colophon { position: relative; }
.masthead > .wrap, .hero > .wrap, .colophon > .wrap { position: relative; }

/* ---------------------------------------------------------------- masthead */

.masthead {
    background: var(--board);
    color: var(--band-ink);
}

.masthead .wrap {
    display: flex;
    align-items: center;
    gap: 1.5rem;
    min-height: 44px;
    flex-wrap: wrap;
}

.wordmark {
    color: var(--band-ink);
    text-decoration: none;
    font-size: .98rem;
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    margin-right: auto;
}

.wordmark svg { width: 17px; height: 17px; flex: none; }
.wordmark .the { color: var(--piece); }

.nav {
    display: flex;
    gap: 1rem;
    font-size: .89rem;
}

.nav a {
    color: var(--band-ink);
    text-decoration: none;
    padding: .25rem 0;
    border-bottom: 2px solid transparent;
}
.nav a:hover { color: var(--piece); }
.nav a[aria-current="page"] { border-bottom-color: var(--piece); color: var(--piece); }

/* -------------------------------------------------------- settings menu */

/* A <details> in the masthead. Everything below is presentation only: with the
   stylesheet gone it is still a working disclosure with three buttons in it.
   Every colour is a token, because this menu appears on the dark band in the
   light theme and on the dark band in the dark theme, and those are two
   different darks. */

.prefs { position: relative; font-size: .89rem; }

.prefs > summary {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .25rem 0;
    border-bottom: 2px solid transparent;
    color: var(--band-ink);
    cursor: pointer;
    list-style: none;               /* the default triangle, in two dialects */
}
.prefs > summary::-webkit-details-marker { display: none; }
.prefs > summary:hover { color: var(--piece); }
.prefs[open] > summary { color: var(--piece); border-bottom-color: var(--piece); }
.prefs > summary svg { width: 15px; height: 15px; flex: none; }

.prefs-menu {
    position: absolute;
    right: 0;
    top: calc(100% + .55rem);
    z-index: 30;
    min-width: 12.5rem;
    padding: .7rem;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--card);
    color: var(--ink);
    box-shadow: 0 12px 30px rgba(0, 0, 0, .22);
}

.prefs-head {
    margin: 0 0 .45rem;
    font-size: .72rem;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.prefs-set { display: grid; gap: .1rem; }

.prefs-set button {
    display: flex;
    align-items: center;
    gap: .45rem;
    width: 100%;
    padding: .4rem .45rem;
    border: 0;
    border-radius: 7px;
    background: none;
    color: var(--ink);
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.prefs-set button:hover { background: var(--hover); }
.prefs-set button[aria-current] { color: var(--link); font-weight: 600; }

/* Held open whether or not there is a tick in it, so the three labels line up
   with each other rather than shuffling as the choice moves. */
.prefs-tick { width: .85rem; flex: none; }

.prefs-note {
    margin: .55rem 0 0;
    font-size: .74rem;
    color: var(--ink-soft);
}

/* On a narrow screen the masthead wraps and the menu would hang off the edge
   from its own line. Anchored to the bar instead, so it opens under the whole
   header rather than beside it. */
@media (max-width: 30rem) {
    .prefs { position: static; }
    .prefs-menu { right: 0; left: auto; }   /* .masthead .wrap is the anchor */
}

/* -------------------------------------------------------------------- hero */

.hero {
    position: relative;
    background: var(--board);
    color: var(--band-ink);
    padding-block: clamp(2.75rem, 1rem + 5vw, 5rem);
    overflow: hidden;
}

/* The hero board is currently switched off - index.php no longer calls
   jigsaw(). Everything it needs is still here and still correct, so putting it
   back is one line in the template rather than a rebuild. */
.jig-bg {
    position: absolute;
    inset: -6%;
    z-index: 0;
    opacity: .17;
    pointer-events: none;
}
.jig-bg .jig { width: 100%; height: 100%; }

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

/* Text over artwork needs its own ground or it is simply hard to read. */
.hero-card {
    max-width: 44rem;
    background: var(--veil);
    border: 1px solid var(--veil-edge);
    border-radius: 4px;
    padding: clamp(1.4rem, 1rem + 2vw, 2.4rem);
    backdrop-filter: blur(3px);
}

/* One line, always. The clamp is sized so the longest it gets still fits the
   card at every width rather than spilling out of it. */
.hero h1 {
    font-size: clamp(1.4rem, 5.2vw, 3.1rem);
    white-space: nowrap;
}

.hero p {
    color: var(--ink-onboard);
    font-size: 1.06rem;
    max-width: 48ch;
    margin: 1rem 0 0;
}

.hero-actions {
    display: flex;
    gap: .75rem;
    flex-wrap: wrap;
    margin-top: 1.5rem;
}

/* the jigsaw */

.jig { width: 100%; height: auto; }
.jig .j-p    { fill: var(--band-ink); stroke: var(--board); stroke-width: 2; }
.jig .j-hole { fill: var(--board-2); stroke: var(--jig-line); stroke-width: 2; }
.jig .j-p-loose { fill: var(--piece); stroke: var(--board); stroke-width: 2; }

/* the board coming together

   jigsaw() writes the three loose pieces seated, so each keyframe only has to
   say where its piece came from - the landing is wherever the piece already
   is, which is exactly its hole. Every rule below is therefore additive: strip
   the animation and the board is simply finished, which is what a reduced
   motion visitor and anyone whose CSS failed to load both get.

   Rotation is about each piece's own centre (transform-box: fill-box), so no
   rotation origin is spelled out. `backwards` holds the piece off-board
   through its delay instead of flashing it home first. */

.jig .j-loose {
    transform-box: fill-box;
    transform-origin: 50% 50%;
    animation: seat .85s cubic-bezier(.22, .8, .3, 1) backwards;
}

/* Where each piece falls in from, and the order they land in. */
.jig .j-loose-1 { animation-delay: .50s; --from-x:  120px; --from-y: -52px; --from-r: -13deg; }
.jig .j-loose-2 { animation-delay: .20s; --from-x: -108px; --from-y:  34px; --from-r:   9deg; }
.jig .j-loose-3 { animation-delay: .80s; --from-x:  104px; --from-y:  38px; --from-r:  21deg; }

@keyframes seat {
    from {
        opacity: 0;
        transform: translate(var(--from-x), var(--from-y)) rotate(var(--from-r));
    }
}

/* The board itself settles first, so the pieces land onto something. */
@keyframes board-in {
    from { opacity: 0; transform: translateY(12px); }
}

/* ---------------------------------------------------------------- buttons */

.btn {
    display: inline-block;
    font: 600 .98rem/1 Inter, system-ui, sans-serif;
    padding: .82rem 1.35rem;
    border-radius: 2px;
    text-decoration: none;
    border: 2px solid transparent;
}

.btn-piece {
    background: var(--piece);
    color: var(--board);
}
.btn-piece:hover { background: var(--piece-dk); color: var(--board); }

/* Pale text and a dim border: built for the hero, which sits on the dark board.
   Do not reach for this on a paper background — its colour IS the paper. */
.btn-ghost {
    border-color: var(--ghost-edge);
    color: var(--band-ink);
}
.btn-ghost:hover { border-color: var(--piece); color: var(--piece); }

/* The way back, at the foot of a puzzle's page. Its own class rather than
   .btn-ghost, which put --paper text on the --paper background and left nothing
   but a faint border to read. Solid card, ink text: it sits on the page instead
   of dissolving into it. */
.btn-back {
    background: var(--card);
    color: var(--ink);
    border-color: var(--line);
}
.btn-back:hover {
    border-color: var(--ink);
    background: var(--card);
    color: var(--ink);
}

.btn-buy {
    background: var(--board);
    color: var(--band-ink);
    width: 100%;
    text-align: center;
}
.btn-buy:hover { background: var(--link); color: var(--band-ink); }

.btn-out {
    background: transparent;
    color: var(--ink);
    border-color: var(--line);
    width: 100%;
    text-align: center;
}
.btn-out:hover { border-color: var(--ink); color: var(--ink); }

/* ---------------------------------------------------------------- sections */

.band { padding-block: clamp(2.5rem, 1rem + 5vw, 4.5rem); }

.band-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1.75rem;
}

.band-head h1,
.band-head h2 { font-size: clamp(1.6rem, 1.1rem + 1.8vw, 2.3rem); }
.band-head p { margin: 0; color: var(--ink-soft); max-width: 52ch; }

.lede {
    font-size: 1.12rem;
    max-width: 60ch;
    color: var(--ink-soft);
    margin: .75rem 0 0;
}

/* ------------------------------------------------------------ product grid

   The cards butt together with a 1px seam showing the ground through, so a
   full shelf reads as one assembled board rather than a row of floating
   cards. */

.shelf {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1px;
    background: var(--line);
    border: 1px solid var(--line);
}

/* ------------------------------------------------------------------ events

   Same butt-together construction as the product shelf, so the page reads as
   one system rather than two. Dates carry the accent colour: on a listing of
   events, when is the thing you scan for. */

/* One event per row, not a grid of squares: a calendar is a thing you read
   down, and dates only compare when they line up in a column. Still the same
   butt-together seams as the shelf, just one column wide. */
.events {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1px;
    background: var(--line);
    border: 1px solid var(--line);
}

.event {
    background: var(--card);
    padding: 1.15rem 1.15rem 1.35rem;
    display: grid;
    gap: .5rem;
    align-items: start;
}

/* On a wide screen the row splits: everything on the left, the sign-up button
   held on the right where the eye can run straight down a column of them. */
@media (min-width: 720px) {
    .event {
        grid-template-columns: minmax(0, 1fr) auto;
        column-gap: 1.5rem;
    }
    .event-top,
    .event-body { grid-column: 1; }
    .event-go {
        grid-column: 2;
        grid-row: 1 / span 2;
        align-self: center;
    }
}

/* When and where, together, above the name. Both are answered in the same
   glance - a date is no use if the next line down turns out to be four states
   away - so they share the top line and the title leads the row beneath. */
.event-top {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .35rem .85rem;
    margin: 0;
}

.event-when {
    font-weight: 600;
    font-size: .92rem;
    color: var(--link);
    margin: 0;
}

/* A hairline between them, dropped when they wrap onto separate lines. */
.event-where {
    color: var(--ink-soft);
    font-size: .9rem;
    margin: 0;
    padding-left: .85rem;
    border-left: 1px solid var(--line);
}

.event-body { margin: 0; }
.event h3 { font-size: 1.1rem; letter-spacing: -0.02em; }
.event .blurb { font-size: .93rem; color: var(--ink-soft); margin: .35rem 0 0; max-width: 68ch; }
.event-go { margin: .25rem 0 0; }

/* Screen-reader-only, for the calendar's caption and its "today" marker. */
.asr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* ----------------------------------------------------------------- tip jar

   The button itself arrives with its own styles from Buy Me a Coffee; all this
   does is give it a quiet line of its own at the foot of the page and stop the
   band being as tall as the ones that hold shelves. */

.tipjar { padding-block: clamp(1.75rem, 1rem + 2.5vw, 2.75rem); }

.tipjar .wrap { text-align: center; }

.tipjar-note {
    margin: 0 0 1rem;
    color: var(--ink-soft);
    font-size: .95rem;
    max-width: 46ch;
    margin-inline: auto;
}

/* It comes with `display: inline-flex` of its own; this only keeps it from
   inheriting anything odd and gives it room above the footer. */
.tipjar .bmc-btn-container { display: inline-block; }

/* ------------------------------------------------------------- event views

   Real links, one URL each. The current one is marked with the accent and
   aria-current, so it does not rely on colour alone. */

.views {
    display: flex;
    flex-wrap: wrap;
    gap: .4rem;
    margin: 0 0 1.25rem;
}

.view {
    display: inline-block;
    font-weight: 600;
    font-size: .9rem;
    padding: .5rem .95rem;
    border: 1px solid var(--line);
    border-radius: 2px;
    background: var(--card);
    color: var(--ink);
    text-decoration: none;
}
.view:hover { border-color: var(--ink); }

.view-on {
    background: var(--board);
    border-color: var(--board);
    color: var(--band-ink);
}
.view-on:hover { color: var(--piece); }

/* Links out of the front-page section to the fuller views. */
.band-more {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
    margin: 1.25rem 0 0;
}
.btn-inline { width: auto; background: var(--card); }

/* ----------------------------------------------------------------- calendar

   A table, because it is one: seven weekday columns and a row per week. A grid
   of divs would look identical and tell a screen reader nothing about which
   day anything falls on. */

.cal-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: .85rem;
}

.cal-title { font-size: clamp(1.2rem, 1rem + 1vw, 1.6rem); }
.cal-step { font-weight: 600; font-size: .9rem; white-space: nowrap; }
.cal-none { color: var(--ink-soft); margin: 0 0 .85rem; }

.cal-grid {
    width: 100%;
    border-collapse: collapse;
    background: var(--line);
    border: 1px solid var(--line);
    table-layout: fixed;
}

.cal-grid th {
    background: var(--card);
    font-size: .74rem;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--ink-soft);
    padding: .5rem .4rem;
    text-align: center;
}
.cal-grid th abbr { text-decoration: none; }

.cal-grid td {
    background: var(--card);
    border: 1px solid var(--line);
    padding: 0;
    vertical-align: top;
}

.cal-out { background: var(--out); }

/* The square. aspect-ratio on the inner block rather than the cell, because a
   table cell's height is decided by its row - this way every day is the same
   shape whatever is in it, and the shape survives the grid being resized. */
.cal-day {
    aspect-ratio: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .3rem;
    padding: .25rem;
    text-decoration: none;
    color: var(--ink-soft);
}

.cal-num { font-size: .9rem; line-height: 1; }

/* A day with something on it: darker number, and a dot each. */
.cal-has { color: var(--ink); font-weight: 600; }
a.cal-day:hover { background: var(--hover); }

.cal-dots {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .18rem;
    flex-wrap: wrap;
    max-width: 100%;
}

.cal-dot {
    width: .42rem;
    height: .42rem;
    border-radius: 50%;
    background: var(--link);
}

.cal-more { font-size: .62rem; font-weight: 600; color: var(--link); line-height: 1; }

/* Today, and the day currently open, are marked by more than colour: a ring
   and a filled ground respectively. */
.cal-today { outline: 2px solid var(--piece); outline-offset: -2px; }

.cal-open,
a.cal-open:hover {
    background: var(--board);
    color: var(--band-ink);
}
.cal-open .cal-dot  { background: var(--piece); }
.cal-open .cal-more { color: var(--piece); }

.cal-hint { color: var(--ink-soft); font-size: .88rem; margin: 0 0 .6rem; }

/* The opened day, underneath the grid. */
.cal-panel { margin-top: 1.5rem; }

.cal-panel-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: .6rem;
}
.cal-panel-head h3 { font-size: 1.1rem; }
.alink-close { font-size: .9rem; font-weight: 600; }

/* Squares survive a narrow screen - a number and a dot need very little room -
   so the grid stays a grid rather than becoming a list of days. */
@media (max-width: 560px) {
    .cal-grid th { font-size: .6rem; padding: .35rem .2rem; }
    .cal-num     { font-size: .78rem; }
    .cal-dot     { width: .3rem; height: .3rem; }
    .cal-day     { gap: .2rem; }
}

/* ---------------------------------------------------------------------- map */

.emap {
    height: clamp(320px, 55vh, 560px);
    border: 1px solid var(--line);
    background: var(--card);
    margin-bottom: 1.5rem;
}

.emap-h {
    font-size: 1.05rem;
    margin: 1.5rem 0 .6rem;
}

.anote-pub { color: var(--ink-soft); font-size: .92rem; margin: 0 0 .6rem; }

/* Leaflet draws its own controls; keep them clear of our focus ring colour. */
.emap .leaflet-container { font: inherit; }

/* A shelf with nothing on it. Quiet, and obviously deliberate. */
.shelf-empty {
    background: var(--card);
    border: 1px dashed var(--line);
    color: var(--ink-soft);
    padding: 1.5rem;
    margin: 0;
}

/* A row, not a tile: photo on the left, everything else beside it. A shelf is
   read down, the same way the events list is, and a horizontal card gives the
   words the width they want instead of a column 238px wide. The photo keeps its
   4:3 crop - a card is a thumbnail promising a page, and a column of matching
   rectangles down the left is what makes the shelf scannable. Whole pictures
   are the puzzle page's job. */
.item {
    background: var(--card);
    padding: .85rem .95rem;
    display: flex;
    align-items: flex-start;
    gap: clamp(.7rem, .35rem + 1.1vw, 1.15rem);
}

/* The photo and, under it, the stars. A rating belongs to the picture rather
   than to the prose beside it, and down here it costs the text column nothing.
   The width lives on the column so the two stay the same width as each other. */
.item-media {
    flex: none;
    width: clamp(78px, 20vw, 156px);
}

.item-media .stars {
    margin: .3rem 0 0;
    font-size: .92rem;
    text-align: center;
    letter-spacing: .06em;
}

.item-shot {
    width: 100%;
    aspect-ratio: 4 / 3;
    background: var(--paper);
    border: 1px solid var(--line);
    display: grid;
    place-items: center;
    overflow: hidden;
}

/* The other column. min-width: 0 or a long unbroken title would refuse to
   shrink and push the row wider than the page. */
.item-body {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

/* The name, the piece count and the way to buy it, all on one line. Baselines
   rather than boxes, so the count sits ON the title's line instead of near it;
   the button is the one thing that opts out, hanging from the top of the row. */
.item-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: .15rem .7rem;
}

/* Brand and size ride the same line, and wrap under it when the line runs out.
   The dot only appears when there is a piece count in front of them to be
   separated from - a gear card starts its meta straight after the title. */
.item-head .item-meta { margin: 0; }

.item-pieces + .item-meta::before {
    content: "·";
    margin-right: .5rem;
}

/* The one fact people scan a shelf of puzzles for, so it reads as a fact and
   not as a heading: same line, smaller, quieter. */
.item-pieces {
    color: var(--ink-soft);
    font-size: .88rem;
    font-weight: 600;
    white-space: nowrap;
}

/* .btn-out is full width by default - built for stacking. Here it is a chip in
   a corner, so it sizes to its words and sits above the stretched card link. */
.item-buy {
    flex: none;
    width: auto;
    margin-left: auto;
    align-self: flex-start;
    padding: .5rem .8rem;
    font-size: .84rem;
    background: var(--card);
    position: relative;
    z-index: 2;
}

.item-shot img { width: 100%; height: 100%; object-fit: cover; }

.item-shot span {
    color: var(--faint);
    font-size: .82rem;
}

.item h3 {
    font-size: clamp(1.15rem, 1rem + .55vw, 1.4rem);
    letter-spacing: -0.02em;
    min-width: 0;
}

.item-meta {
    color: var(--ink-soft);
    font-size: .85rem;
    margin: .2rem 0 0;
}

.item p.blurb {
    font-size: .9rem;
    color: var(--ink-soft);
    margin: .4rem 0 0;
    max-width: 68ch;
}

/* Three lines and then an ellipsis, on cards that open a page. A seller's
   description can run to a paragraph, and one long one used to set the height
   of the whole shelf. Only .item-open: a puzzle's full text is one click away,
   where gear has no page at all and clipping it would hide the only place it
   is written. */
.item-open p.blurb {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    overflow: hidden;
}

/* On a phone a card is a title, a photo, a rating and a sentence. The brand and
   the finished size are reference — things you look up once you care — and on a
   narrow screen they cost a line that the sentence wants. They are still on the
   puzzle's own page, which uses this same class inside .build-facts: that is why
   this is scoped, and why it is scoped to .item-open rather than to every card.
   Gear has no page of its own, so a gear card is the only place its brand is
   ever written and hiding it there would lose it. */
@media (max-width: 560px) {
    .item-open .item-meta { display: none; }
}

/* Tags and the "there is more here" cue, on one line along the bottom. The buy
   button used to live down here and now sits in the head, which is most of what
   made these cards shorter. */
.item-foot {
    margin-top: .55rem;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .4rem .8rem;
}

.item-foot .item-more { margin-left: auto; }

/* ------------------------------------------------- a card that opens a page

   One link, on the title, stretched over the whole card - so the click target
   is the card while the tab stop is the title, and the accessible name is the
   puzzle rather than "read more". The Amazon button sits above the stretched
   layer so it stays its own link.

   Only .item-open gets this. Gear cards have no page and stay exactly as they
   were: no pointer, no lift, nothing suggesting a click that goes nowhere. */

.item-open { position: relative; }

.item-open h3 a {
    color: inherit;
    text-decoration: none;
}

.item-open h3 a::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
}

.item-open:hover h3 a { color: var(--link); }

.item-open:hover .item-shot img {
    transform: scale(1.03);
}

.item-shot img { transition: transform .25s ease; }

/* .item-buy carries the position/z-index that lifts it above the stretched
   link - it is declared with the rest of that button, up in the head. */

/* The visible "there is more here" cue. aria-hidden in the markup, because the
   title link already says where this goes. */
.item-more {
    margin: 0;
    font-size: .84rem;
    font-weight: 600;
    color: var(--link);
}

.item-open:hover .item-more { color: var(--ink); }

@media (prefers-reduced-motion: reduce) {
    .item-shot img { transition: none; }
    .item-open:hover .item-shot img { transform: none; }
}

.tag {
    display: inline-block;
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: .01em;
    padding: .2rem .45rem;
    border-radius: 2px;
    border: 1px solid var(--line);
    color: var(--ink-soft);
    background: var(--paper);
}

.tag-amz { border-color: var(--piece); background: var(--amz-bg); color: var(--amz-ink); }

/* How many assembly shots are behind the card. Quiet - it is a hint that there
   is more, not a claim about the puzzle. */
.tag-shots { border-color: var(--shots-bd); }

/* How hard it turned out to be. Three steps of the same idea, so they read as
   one scale rather than three unrelated labels. */
.tag-easy   { border-color: var(--easy-bd); background: var(--easy-bg); color: var(--easy-ink); }
.tag-medium { border-color: var(--med-bd); background: var(--med-bg); color: var(--med-ink); }
.tag-hard   { border-color: var(--hard-bd); background: var(--hard-bg); color: var(--hard-ink); }

/* The rating. Decorative here - the real value is on the aria-label, so a
   screen reader hears "rated 4 out of 5" instead of five star glyphs. */
.stars {
    color: var(--piece-dk);
    font-size: 1.02rem;
    letter-spacing: .1em;
    margin: .4rem 0 0;
}

.item-tags {
    display: flex;
    flex-wrap: wrap;
    gap: .4rem;
    margin: 0;
}

/* ------------------------------------------------------------- disclosure */

/* The affiliate notice. It used to be a panel above every shelf; it is one
   quiet line in the footer now, so it takes the colophon's voice instead of
   shouting over it. A shade dimmer than the rest of the footer and capped in
   ch, so it stays one readable line rather than stretching the row. */
.disclosure {
    margin: 0;
    max-width: 46ch;
    font-size: .86rem;
    color: var(--foot-faint);
}

/* --------------------------------------------------------- one puzzle's page

   The shelf is a grid of equals; this page is the opposite, so it drops the
   butt-together card construction and gives the photo and the write-up room.
   Measure is capped in ch rather than px: the write-up is the point of the
   page, and prose stops being readable past about 70 characters a line however
   wide the window gets. */

.build { padding-block: clamp(1.75rem, 1rem + 3vw, 3rem); }

.crumb {
    margin: 0 0 1.5rem;
    font-size: .9rem;
    font-weight: 600;
}

.build-head {
    display: grid;
    gap: clamp(1.25rem, .5rem + 3vw, 2.5rem);
    align-items: start;
}

@media (min-width: 720px) {
    .build-head { grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); }
}

/* The one photo on the one page about this puzzle, shown WHOLE. No fixed
   aspect-ratio here and no `cover`: the frame takes the shape of the picture,
   rather than the picture being cut to the shape of the frame. The cards keep
   their 4:3 crop - a shelf of matching rectangles is the point there, and a
   card is a thumbnail promising the page, not the thing itself.

   The <img> carries its real pixel size (image_size(), read off the file), so
   the space is reserved correctly before it loads and nothing jumps. */
.build-shot {
    background: var(--paper);
    border: 1px solid var(--line);
    display: grid;
    place-items: center;
}

/* max-height so a tall portrait shot cannot push the facts beside it a screen
   and a half down the page. Capped by height, the width follows on its own and
   place-items centres what is left - paper either side, like a mount. */
.build-shot img {
    display: block;
    width: 100%;
    height: auto;
    max-height: 78vh;
    object-fit: contain;
}

/* Nothing to take a shape from, so it keeps the shelf's. */
.build-shot-empty { aspect-ratio: 4 / 3; }

.build-shot span { color: var(--faint); font-size: .85rem; }

.build-facts h1 {
    font-size: clamp(1.7rem, 1.15rem + 2.2vw, 2.6rem);
}

.build-facts .item-meta { font-size: .95rem; }
.build-facts .item-tags { margin-top: .8rem; }

.build-blurb {
    font-size: 1.05rem;
    color: var(--ink-soft);
    margin: 1rem 0 1.4rem;
    max-width: 46ch;
}

.build-story {
    margin-top: clamp(2rem, 1rem + 4vw, 3.5rem);
    max-width: 68ch;
}

.build-story h2,
.build-gallery h2 {
    font-size: clamp(1.3rem, 1rem + 1.2vw, 1.7rem);
    margin-bottom: .9rem;
}

.build-story p { margin: 0 0 1.1rem; }
.build-story p:last-child { margin-bottom: 0; }

.build-gallery { margin-top: clamp(2rem, 1rem + 4vw, 3.5rem); }

/* Same seam-and-hairline construction as the shelf, so the two read as one
   site. auto-fill rather than a fixed count: two photos should not be forced
   to straddle four columns. */
.gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
    gap: 1px;
    background: var(--line);
    border: 1px solid var(--line);
}

.gallery figure {
    margin: 0;
    background: var(--card);
    display: flex;
    flex-direction: column;
}

.gallery a {
    display: block;
    aspect-ratio: 4 / 3;
    overflow: hidden;
    background: var(--paper);
}

.gallery img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform .25s ease;
}

.gallery a:hover img { transform: scale(1.04); }

.gallery figcaption {
    padding: .65rem .8rem .85rem;
    font-size: .87rem;
    color: var(--ink-soft);
}

.build-back { margin: clamp(2rem, 1rem + 4vw, 3.5rem) 0 0; }

@media (prefers-reduced-motion: reduce) {
    .gallery img { transition: none; }
    .gallery a:hover img { transform: none; }
}

/* ----------------------------------------------------------------- footer */

.colophon {
    background: var(--board);
    color: var(--foot-ink);
    padding-block: 2.5rem;
    font-size: .93rem;
    margin-top: 3rem;
}

.colophon .wrap {
    display: flex;
    gap: 1.5rem 2.5rem;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
}

/* The footer mark. Reads as a colophon flourish rather than a control, so it
   sits quietly until you go looking for it - then it lights up like the rest
   of the footer links do. Full-size hit area, because a deliberately subtle
   target should still be easy to actually hit. */
.doormark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 52px;
    height: 52px;
    color: var(--band-ink);
    opacity: .38;
    transition: opacity .18s ease, color .18s ease;
}
.doormark svg { width: 30px; height: 30px; }
.doormark:hover, .doormark:focus-visible { opacity: 1; color: var(--piece); }

.colophon a { color: var(--band-ink); }
.colophon a:hover { color: var(--piece); }
.colophon p { margin: 0; }

/* ------------------------------------------------------------------ small */

@media (max-width: 780px) {
    .masthead .wrap { min-height: 0; padding-block: .7rem; }
    .wordmark { margin-right: 0; width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
    * { animation: none !important; transition: none !important; }
}

/* Whether the box includes a poster of the finished picture. Its own colour so
   it does not read as one of the difficulty steps, which are a scale. */
.tag-poster { border-color: var(--poster-bd); background: var(--poster-bg); color: var(--poster-ink); }
