/* Plain CSS, no build step. Tailwind would need a compile pass to avoid
   shipping the full framework, which is the one thing this stack avoids. */

:root {
    --bg: #fbfbfa;
    --fg: #16161a;
    --muted: #6b6b76;
    --line: #e4e4e0;
    --accent: #2f6df6;
    --radius: 10px;
    --max: 62rem;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #121214;
        --fg: #f2f2f0;
        --muted: #9a9aa4;
        --line: #2a2a2f;
        --accent: #7aa2ff;
    }
}

* {
    box-sizing: border-box;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--fg);
    font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    -webkit-font-smoothing: antialiased;
    /* The full-bleed hero still is 100vw, which counts the scrollbar and so
       overflows by its width. Clip is preferred over hidden because it does not
       turn the body into a scroll container; hidden is the fallback for Safari
       before 16. Nothing here is sticky, so neither breaks anything. */
    overflow-x: hidden;
    overflow-x: clip;
}

a {
    color: inherit;
}

.site-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.75rem 1rem;
    max-width: var(--max);
    margin: 0 auto;
    padding: 1.25rem 1.25rem;
    border-bottom: 1px solid var(--line);
    /* Positioned so the search panel inside it has something to hang from, and
       so the bar paints above the homepage hero still, which is a positioned
       element later in the document and would otherwise cover it. */
    position: relative;
    z-index: 20;
}

.wordmark {
    font-weight: 700;
    font-size: 1.25rem;
    letter-spacing: -0.02em;
    text-decoration: none;
}

.site-header nav a {
    color: var(--muted);
    text-decoration: none;
    font-size: 0.95rem;
}

main {
    max-width: var(--max);
    margin: 0 auto;
    padding: 2rem 1.25rem 4rem;
}

.hero h1 {
    font-size: clamp(1.8rem, 4vw, 2.6rem);
    letter-spacing: -0.03em;
    margin: 0 0 0.5rem;
}

.lede {
    color: var(--muted);
    max-width: 46ch;
    margin: 0 0 1rem;
}

.progress {
    font-size: 0.9rem;
    color: var(--muted);
}

/* --- Search --- */

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

/* The box, History and the account control are one cluster at the right end of
   the bar — pushed there by the auto margin, with the wordmark keeping the left.
   Grouping them is also what lets the results panel span the cluster: the panel
   hangs off this box, so left:0 lands on the search field and right:0 on the far
   edge of the account control, with no width anywhere that has to be guessed. */
.header-end {
    position: relative;
    display: flex;
    align-items: center;
    gap: 1rem;
    min-width: 0;
    margin-left: auto;
}

.search {
    /* Non-shrinking on purpose. The open field is drawn to this width, so if flex
       were free to squeeze the space the two would disagree and the field would
       come down on top of the nav. Out of room, the bar wraps instead. */
    flex: 0 0 14.26rem;
    /* Not redundant next to flex-shrink: 0. A flex item's default min-width is
       auto, which floors it at its own min-content — and the min-content of a
       text input is the twenty-odd characters its size attribute asks for. That
       floor clamps the basis upward, so any basis narrower than the input's
       natural width is quietly ignored. Harmless at this width, fatal at the
       magnifier width the bar collapses to on a phone. */
    min-width: 0;
    /* Holds the bar's height for as long as focus has the form lifted out of the
       flow and this box is empty. Written from the input's own parts — line
       height, vertical padding, borders — so it follows them rather than being a
       number to keep in step by hand. Inert until that moment: the form in the
       flow is already exactly this tall. */
    min-height: calc(1.6rem + 1.2rem + 2px);
}

.search-form {
    /* Anchors the icon inside the field rather than to the wrapper, so the two
       stay together even if the wrapper ever grows past the form. */
    position: relative;
    display: flex;
    align-items: center;
    margin: 0;
}

/* Results float over the page as an overlay card rather than pushing content
   down. Hung off the bottom edge of the header cluster, so the card runs from
   the search field across to the end of the account control — a title and its
   year want more room than the field itself gives them. Empty until a search
   runs, so it collapses entirely when there is nothing to show. */
.search-panel {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 100;
}

.search-panel:empty,
.search-panel[hidden] {
    display: none;
}

.search-card {
    margin-top: 0.4rem;
    padding: 0.35rem;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
}

.search-card .results {
    margin: 0;
    border: 0;
}

.search-card > .muted,
.search-card > .empty {
    margin: 0;
    padding: 0.5rem 0.6rem;
}

.search-form input[type="search"] {
    flex: 1 1 auto;
    min-width: 0;
    /* Left padding is the icon's seat, not decoration — the two have to move
       together or the magnifier lands on the first letter of the term. */
    padding: 0.6rem 0.8rem 0.6rem 2.3rem;
    font: inherit;
    color: inherit;
    background: transparent;
    border: 1px solid var(--line);
    border-radius: var(--radius);
}

/* No ring on the field. Focus already announces itself loudly enough — the box
   opens across the bar, the nav goes with it, and the surface changes to the
   card's — so a ring on top of that is decoration. Set to none rather than
   deleted: dropping the rule just hands the job back to the browser's own ring.

   Both states, because :focus-visible is not the only way a ring gets drawn. The
   icon beside it keeps its own: a button has nothing else to say it has focus. */
.search-form input[type="search"]:focus,
.search-form input[type="search"]:focus-visible {
    outline: none;
}

/* The submit control, parked inside the field's left padding. Stripped of the
   button chrome the .btn rule gives everything else: inside a bordered box, a
   second border would read as a control within a control. */
.search-go {
    position: absolute;
    left: 0.45rem;
    top: 50%;
    transform: translateY(-50%);
    display: grid;
    place-items: center;
    width: 1.6rem;
    height: 1.6rem;
    padding: 0;
    color: var(--muted);
    background: transparent;
    border: 0;
    border-radius: 50%;
    cursor: pointer;
}

.search-go:hover {
    color: var(--fg);
}

.search-go:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

/* --- The field once you are in it --- */

/* Clicking in gives the field the results card's own surface — same fill, same
   hairline — so the box and the list it produces read as one thing rather than
   two that happen to be stacked.

   On the homepage this also has to undo the light-on-photograph treatment the bar
   wears at rest: white text and a translucent border are legible over a still and
   invisible the moment the field fills with page colour. Written long enough to
   out-rank those .over-hero rules, which sit later in the file. */
.search:focus-within .search-form input[type="search"] {
    color: var(--fg);
    background: var(--bg);
    border-color: var(--line);
}

.search:focus-within .search-form input[type="search"]::placeholder {
    color: var(--muted);
}

.search:focus-within .search-go {
    color: var(--muted);
}

.search:focus-within .search-go:hover {
    color: var(--fg);
}

/* --- Opening the field --- */

/* Focus opens the field across the whole cluster, over History and the account
   control, to the width the results have always used — so what you type into and
   what comes back are one shape rather than a narrow box with a wide list hanging
   off it. It snaps rather than slides: at this size the travel was just time
   spent waiting to type.

   Every rule here is behind :focus-within. At rest the form is an ordinary
   in-flow child of .search, the same as on every other page, so there is nothing
   in this block that runs on load. That is deliberate: an earlier version made
   the form an overlay permanently and broke the bar the moment the page opened.

   What the field opens over differs by width, so the offsets live in the two
   blocks below. This part does not: the nav goes either way. */

/* Out of sight but not out of the tab order — visibility or display would drop
   the nav from it, and then tabbing off the field would skip History and the
   account menu and land in the page body. Faded, the nav is still the next stop,
   and arriving there takes focus out of the field, which closes it and brings the
   nav back in the same motion. */
.search:focus-within~.site-nav {
    opacity: 0;
    /* The open field covers this, so a click here reaches the field anyway — but
       nothing invisible should be answering clicks regardless. */
    pointer-events: none;
}

@media (min-width: 34.0625rem) {
    /* Out of the flow only while focused. No top: the static position already
       puts the form where it sat a moment ago, which is better than working the
       offset out from a height that would then have to stay correct.

       100% is .header-end — the same box the results panel spans — so the open
       field and the list beneath it land on exactly the same edges. */
    .search:focus-within .search-form {
        position: absolute;
        left: 0;
        width: 100%;
    }
}

/* Below this the bar cannot hold a field worth typing in beside the wordmark and
   the account controls, so it stops trying to be one. The field closes down to
   its magnifier and sits in the top row next to History — a row of its own cost a
   third of the bar to show an empty box. Tapping it opens the same field the
   desktop opens, across the full width of the row. */
@media (max-width: 34rem) {
    /* The cluster stops being one. Dissolved rather than left to wrap inside
       itself: wrapping internally would box the field into whatever the wordmark
       left over, which is the narrow slot this rule exists to avoid. With no box
       of its own it also stops being what the field and the panel measure
       against, and the header takes over — which is what the insets below are
       for. */
    .header-end {
        display: contents;
    }

    /* Wide enough for the icon and the seat it already sits in: 0.45rem of lead,
       1.6rem of icon, and the same 0.45rem left over on the right, so it centres
       without the icon needing to be moved. The auto margin does the job the
       cluster's used to do, holding this and the nav together at the right end. */
    .search {
        flex: 0 0 2.5rem;
        margin-left: auto;
    }

    /* Closed, it is an icon and not a box — the border and fill would read as an
       empty input sat next to two plain controls. Long enough a selector to beat
       the .over-hero treatment further down the file, which is written for a
       field and not for this. */
    .search:not(:focus-within) .search-form input[type="search"] {
        background: transparent;
        border-color: transparent;
        /* There is no room for a term, so nothing should be reserving any. The
           right padding goes because with border-box the padding is a floor on
           the field's width, and 2.3 + 0.8 is wider than the icon it is standing
           in for. */
        padding-right: 0;
    }

    /* And the prompt itself goes. Clipping alone leaves a few pixels of the first
       letter showing past the magnifier. */
    .search:not(:focus-within) .search-form input[type="search"]::placeholder {
        color: transparent;
    }

    /* The magnifier is a submit button, and at this size it covers most of what
       there is to tap — left alone, tapping to open the search would post the
       empty form and load the results page instead. Deaf while closed, the tap
       lands on the input behind it, which is what opens the field. It comes back
       once open, where it is a submit button again and there is something to
       submit. */
    .search:not(:focus-within) .search-go {
        pointer-events: none;
    }

    /* Open, it spans the row rather than the cluster: the wordmark is the only
       thing beside it and it is covered too. The header's box includes the bar's
       own side padding, hence the gutter on both edges rather than 0. */
    .search:focus-within .search-form {
        position: absolute;
        left: 1.25rem;
        right: 1.25rem;
        width: auto;
    }

    /* Same gutter, so the card lines up under the field it came from. */
    .search-panel {
        left: 1.25rem;
        right: 1.25rem;
    }
}

/* htmx toggles .htmx-request on the indicator while a search is in flight, and
   the icon is the indicator. Pulsing rather than swapping in a spinner: the
   search fires per keystroke, so anything that changes shape here would flicker
   through every word typed. */
.search-go.htmx-request {
    color: var(--accent);
    animation: search-pulse 0.9s ease-in-out infinite;
}

@keyframes search-pulse {
    50% {
        opacity: 0.3;
    }
}

@media (prefers-reduced-motion: reduce) {
    .search-go.htmx-request {
        animation: none;
        opacity: 0.55;
    }
}

.btn {
    padding: 0.55rem 0.9rem;
    font: inherit;
    font-size: 0.9rem;
    color: inherit;
    text-decoration: none;
    background: transparent;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    cursor: pointer;
    white-space: nowrap;
}

.btn:hover {
    border-color: var(--accent);
}

.results {
    list-style: none;
    margin: 0.5rem 0 0;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius);
}

.result {
    border-bottom: 1px solid var(--line);
}

.result:last-child {
    border-bottom: 0;
}

/* The whole row is the click target — a link for titles already in the DB, a
   full-width submit button (reset to look identical) for ones being imported. */
.result-link {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    width: 100%;
    padding: 0.6rem 0.75rem;
    font: inherit;
    color: inherit;
    text-align: left;
    text-decoration: none;
    background: transparent;
    border: 0;
    border-radius: calc(var(--radius) - 4px);
    cursor: pointer;
}

.result-link:hover,
.result-link:focus-visible {
    background: var(--line);
    outline: none;
}

.result img,
.poster-placeholder {
    flex: 0 0 auto;
    width: 46px;
    height: 69px;
    object-fit: cover;
    border-radius: 4px;
    background: var(--line);
}

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

.result-text strong {
    display: block;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.result form {
    margin: 0;
}

.muted {
    color: var(--muted);
    font-size: 0.85rem;
}

.row-heading {
    font-size: 1.3rem;
    letter-spacing: -0.02em;
    margin: 2.5rem 0 0.15rem;
}

.row-sub {
    color: var(--muted);
    font-size: 0.9rem;
    margin: 0;
}

.movie-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 13rem), 1fr));
    gap: 1rem;
    margin: 1rem 0 0;
}

/* Two across on a phone. One film to a row gave each the full width of the
   screen, which is more than a poster, a title and a two-word action need, and it
   pushed the next one off the bottom — a board you have to scroll to know is a
   board reads as a list of one.

   Written as two explicit columns rather than by lowering the minimum above: that
   13rem is what forces the single column here, and dropping it far enough to fit
   two would change the count on every wider screen too. minmax(0, 1fr) rather
   than 1fr so a long title cannot push its column past half. */
@media (max-width: 30rem) {
    .movie-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 0.75rem;
    }

    /* Half the width to work in, so the frame around the contents comes in with
       it and the title stops running to four lines. */
    .card-body {
        padding: 0.65rem;
    }

    .movie-card h2,
    .movie-card h3 {
        font-size: 0.95rem;
    }
}

/* Upcoming cards vote instead of rate. Sized to sit where the rating widget
   sits on a released card, so the two rows stay visually aligned. */
.call-it {
    /* margin-top:auto drops it to the card foot; align-self stops the flex
       column from stretching it to full width. */
    margin-top: auto;
    align-self: flex-start;
    padding: 0.4rem 0.7rem;
    font-size: 0.95rem;
    text-decoration: none;
    border: 1px solid var(--line);
    border-radius: 999px;
}

.call-it:hover {
    border-color: var(--accent);
}

.movie-card {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    /* Clips the poster to the card's rounded corners. Without this the
       square image corners sit outside the border. */
    overflow: hidden;
    /* Containing block for the title link's overlay below. */
    position: relative;
}

/* The whole card is the target, not just the poster and the words.
   The dead zone was the padding under the title — biggest on exactly the cards
   whose title fits one line, which is most of them — and it looked identical to
   the part that worked.

   Done by growing the title's link rather than wrapping the card in an <a>,
   because a card can hold forms (the call buttons) and an <a> may not contain a
   button. It also keeps the count of links per card where it was: this is the
   same link the title already was, just a bigger one, so the crawler sees no
   new href and the title keeps being the anchor text pointing at the film.

   The cursor comes free — the overlay is part of a link, so the pointer is
   already right across the card. The cost is text selection: the overlay sits
   over the title and meta, so they can no longer be dragged over and copied.
   That is the standard trade for this pattern and the right way round on a card
   whose whole job is to be clicked. */
.movie-card h2 a::after,
.movie-card h3 a::after {
    content: "";
    position: absolute;
    inset: 0;
}

/* The two things that must stay above the overlay or they stop working: the
   call buttons, which are the card's own action and post somewhere else
   entirely, and the poster, which on a slate card points at /movies/x#box-office
   rather than /movies/x — swallowing it would quietly drop the jump to the part
   of the page the poster is promising. */
.movie-card .card-poster,
.movie-card .call-action {
    position: relative;
    z-index: 1;
}

/* Fixed 2:3 box, so a missing or oddly-proportioned poster still leaves the
   grid aligned instead of collapsing the card. */
.card-poster {
    display: block;
    aspect-ratio: 2 / 3;
    background: var(--line);
}

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

.card-body {
    padding: 0.85rem;
    /* Column so the action (vote link or rating widget) can be pushed to the
       bottom with margin-top:auto. Cards in a grid row stretch to equal height,
       so pinning the action down makes it line up across cards whether the
       title runs to one line or two. */
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
}

/* h3 as well as h2 because a card's heading level depends on where the grid is.
   On the homepage the rows are the page's own sections and the cards are h2; in
   the "still open" block on a movie page the section already owns an h2, so the
   cards sit a level below it. Same card either way, so the same type. */
.movie-card h2,
.movie-card h3 {
    font-size: 1.05rem;
    margin: 0 0 0.15rem;
}

.movie-card h2 a,
.movie-card h3 a {
    text-decoration: none;
}

/* The meta finishes the title's last line when the title wrapped, and sits on
   its own line when it did not. There is no selector for "this heading took two
   lines" — it is a fact about layout, not about the document — so a script sets
   .is-flowed and everything here is the two states it switches between.

   Un-flowed is the default, and deliberately: it is what the cards looked like
   before any of this, so a reader with JS off sees the old layout rather than a
   broken one, and it is the state most cards are in (most film titles fit a
   line), so the script moves the few rather than the many.

   The site loads no webfonts, which is what makes this safe to do in script at
   all: nothing re-flows after the first layout, so the measurement taken in the
   footer is the final one and nothing visibly jumps. */
.card-head {
    margin: 0 0 0.75rem;
}

/* Inline in both states, and not only for the flowed one: an inline box reports
   one client rect per line it occupies, which is how the script counts the
   title's lines without parsing line-height. */
.card-head h2,
.card-head h3 {
    display: inline;
    margin: 0;
}

/* Its own line — the 0.15rem is the margin the h2 used to carry and cannot now
   that it is inline. */
.card-head .meta {
    display: block;
    margin: 0.15rem 0 0;
}

.card-head.is-flowed .meta {
    display: inline;
    margin: 0;
}

/* The separator hangs off the title, not off the meta. Both read the same when
   everything fits on one line, but they part company when the meta has to wrap
   anyway — a phone's card is ~152px of text and often has no room for it. Then
   this leaves "Brand New Day ·" at the end of the title, which reads as a line
   continuing, where a ::before on the meta would open the next line with a
   floating "· 31 days in release".

   The script measures the <a> inside, never the h2, so this generated box can
   never feed back into the line count that decides whether it exists. */
.card-head.is-flowed h2::after,
.card-head.is-flowed h3::after {
    content: "\00B7";
    margin-left: 0.3rem;
    color: var(--muted);
    font-weight: normal;
}

/* The one section on a movie page that is not about the film being viewed.
   Sitting flush under the spec table, four posters read as sequels or as
   "similar titles", and they are neither — they are the next four calls to
   close, picked on date and nothing else. So a rule rather than more
   whitespace: space says "next section", and this is a change of subject.

   Scoped to .movie to outrank `.movie section`, which sets margin: 0 0 1.75rem
   and would otherwise flatten the gap. */
.movie .still-open {
    border-top: 1px solid var(--line);
    margin-top: 2.75rem;
    padding-top: 1.75rem;
}

/* Every heading inside .movie is a small uppercase label — right for a spec
   table, wrong for the only invitation on the page, which was inheriting it and
   disappearing into the metadata. Sized back up and set in the body colour so it
   reads as something to act on. Still scoped, so DETAILS and BOX OFFICE keep the
   label treatment. */
.movie .still-open h2 {
    font-size: 1.05rem;
    font-weight: 600;
    text-transform: none;
    letter-spacing: -0.01em;
    color: var(--fg);
    margin: 0 0 0.25rem;
}

/* Sits under the cards, the way the hero's "See the numbers" sits under the
   hero — a quiet way on to the full board for anyone who wants more than an
   invitation. */
.still-open-more {
    font-size: 0.9rem;
    margin: 1rem 0 0;
}

.meta {
    color: var(--muted);
    font-size: 0.85rem;
    margin: 0 0 0.75rem;
}

/* Each button is its own form so it works without JS; the flex row makes
   them read as a single control. */
.rating-widget {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
}

.rating-widget form {
    margin: 0;
}

.rating-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.4rem 0.6rem;
    font: inherit;
    font-size: 0.95rem;
    line-height: 1;
    color: inherit;
    background: transparent;
    border: 1px solid var(--line);
    border-radius: 999px;
    cursor: pointer;
}

.rating-btn:hover {
    border-color: var(--accent);
}

/* Filled, not outlined. The selected state used to be border-color: var(--accent)
   plus an inset ring — but :hover above sets that same border colour, so whichever
   button the cursor happened to be over looked like the one already chosen, and a
   recorded answer never read as recorded. Hover keeps the outline; the answer you
   gave is the only filled pill in the row.

   Filled with --fg and not --accent, even though accent is the obvious "chosen"
   colour. Accent is already the link colour, the focus ring and the hover border,
   so using it here would have hover and selected sharing a hue on the same
   control — a quieter version of the bug above. Text and background instead:
   nothing else claims those, so a filled pill can only mean "this is the answer
   you gave".

   color: var(--bg) rather than a literal, because --fg inverts between the light
   and dark themes and the readable text on top of it is always whatever the page
   background is in that theme. */
.rating-btn.is-selected {
    background: var(--fg);
    border-color: var(--fg);
    color: var(--bg);
}

/* The label is muted by default, which is unreadable once the pill is filled. */
.rating-btn.is-selected .rating-label {
    color: inherit;
}

/* With an answer recorded the other four step back, so the eye lands on the one
   picked instead of scanning five equal pills. Unguarded, unlike the slate
   toggle: without :has() support nothing dims and the filled pill still carries
   the state on its own. Hover brings a dimmed one back, since changing your mind
   has to stay obviously available. */
.rating-widget:has(.is-selected) .rating-btn:not(.is-selected) {
    opacity: 0.45;
}

.rating-widget:has(.is-selected) .rating-btn:not(.is-selected):hover {
    opacity: 1;
}

/* Emoji alone carries the meaning once space is tight. In a card there is
   never room for five labelled buttons, so labels are card-wide suppressed
   and the button title/aria-label does the explaining. */
.rating-label {
    font-size: 0.8rem;
    color: var(--muted);
}

.movie-card .rating-label,
.movie-card .rating-btn {
    font-size: 0.95rem;
}

.movie-card .rating-label {
    display: none;
}

.movie-card .rating-widget {
    gap: 0.25rem;
    /* Same as .call-it: sit at the card foot so widgets line up across a row
       regardless of title length. */
    margin-top: auto;
}

.movie-card .rating-btn {
    padding: 0.3rem 0.45rem;
}

@media (max-width: 30rem) {
    .rating-label {
        display: none;
    }
}

/* htmx sets this while a request is in flight */
.htmx-request .rating-btn {
    opacity: 0.55;
}

/* --- Movie page --- */

.movie-head {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
    margin-bottom: 2rem;
}

.poster {
    flex: 0 0 auto;
    width: min(100%, 14rem);
    aspect-ratio: 2 / 3;
    height: auto;
    object-fit: cover;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--line);
}

.movie-intro {
    flex: 1 1 18rem;
    min-width: 0;
}

.movie-intro h1 {
    font-size: clamp(1.6rem, 3.5vw, 2.2rem);
    letter-spacing: -0.02em;
    margin: 0 0 0.25rem;
}

.overview {
    max-width: 60ch;
    margin: 0 0 1.25rem;
}

.rate-block h2,
.movie section h2 {
    font-size: 0.8rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
    margin: 0 0 0.6rem;
}

.movie section {
    margin: 0 0 1.75rem;
}

.aspect-list,
.source-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.aspect-list li,
.source-list li {
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    padding: 0.4rem 0;
    border-bottom: 1px solid var(--line);
}

.aspect-list li:last-child,
.source-list li:last-child {
    border-bottom: 0;
}

/* --- Box office + details --- */

.stats {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
}

.stat {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.stat-label {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}

.stat-value {
    font-size: 1.15rem;
    font-weight: 600;
    letter-spacing: -0.01em;
}

.stat-note {
    font-size: 0.8rem;
    color: var(--muted);
}

.tone-good {
    color: #17803d;
}

.tone-bad {
    color: #c02626;
}

.tone-mixed {
    color: #a16207;
}

@media (prefers-color-scheme: dark) {
    .tone-good {
        color: #5dd48b;
    }

    .tone-bad {
        color: #ff8080;
    }

    .tone-mixed {
        color: #e0b54a;
    }
}

.prompt {
    max-width: 55ch;
    margin: 0 0 1rem;
}

.vote-widget {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    margin-bottom: 1.25rem;
}

.vote-widget form {
    margin: 0;
}

.vote-btn {
    padding: 0.7rem 1.4rem;
    font: inherit;
    font-size: 1rem;
    font-weight: 600;
    color: inherit;
    background: transparent;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    cursor: pointer;
}

.vote-btn:hover {
    border-color: var(--accent);
}

.verdict-line {
    font-size: 1.1rem;
    margin: 0 0 0.35rem;
}

/* The headline outcome, sitting between the "Box office" heading and the
   money story. The label carries the tone colour; the ratio trails it muted. */
.bo-verdict {
    margin: 0 0 0.75rem;
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: -0.02em;
}

.bo-verdict-ratio {
    margin-left: 0.5rem;
    font-size: 0.9rem;
    font-weight: 400;
    color: var(--muted);
}

.tally {
    margin: 0 0 1.25rem;
    color: var(--muted);
    font-size: 0.9rem;
}

.footnote {
    max-width: 60ch;
    margin: 0.9rem 0 0;
    font-size: 0.8rem;
    color: var(--muted);
}

/* --- Box office ticker --- */

/* Figures on one side, the climbing curve on the other. Collapses to a single
   column on narrow screens, where a half-width chart would be unreadable. */
.bo-cols {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.25rem 2rem;
    align-items: start;
    margin-top: 1.25rem;
}

@media (max-width: 44rem) {
    .bo-cols {
        grid-template-columns: 1fr;
    }
}

.bo-chart-wrap {
    margin: 1.5rem 0 0;
}

.bo-cols .bo-chart-wrap {
    /* lift the chart so its date axis doesn't hang below the caption on the left */
    margin: -2.25rem 0 0;
}

.bo-caption {
    max-width: 55ch;
    margin: 0 0 0.75rem;
    font-size: 0.95rem;
}

.bo-pct {
    color: var(--accent);
    font-weight: 600;
}

.bo-chart {
    width: 100%;
    height: auto;
    overflow: visible; /* the break-even label sits just past the plot */
}

.bo-goal {
    stroke: var(--muted);
    stroke-width: 1;
    stroke-dasharray: 4 4;
    opacity: 0.6;
}

.bo-goal-label,
.bo-axis {
    fill: var(--muted);
    font-size: 13px; /* the chart renders at ~half width now; keep text legible */
}

.bo-value {
    fill: var(--fg);
    font-size: 14px;
    font-weight: 600;
}

/* The day count riding along with the gross. Muted and lighter than the figure
   it qualifies, so the pair reads as one label and not as two numbers competing
   — it is the same relationship the caption's muted text has to its bold. */
.bo-value-day {
    fill: var(--muted);
    font-size: 12px;
    font-weight: 400;
}

/* The curve is coloured by the verdict the page is already printing in words,
   so the picture and the sentence under it cannot disagree. Red for a film that
   lost money, amber for one that roughly broke even, green for one that made
   it, and the accent blue while a film is still earning and nobody is entitled
   to an opinion yet.

   One custom property rather than three declarations per tone. The line, the
   dot and the fill are always the same colour — they are one mark — and the old
   shape restated each hex three times, then twice more for dark mode, which is
   six places to miss when a tone is added. */
.bo-chart {
    --bo-tone: var(--accent);
}

.bo-chart.tone-good {
    --bo-tone: #17803d;
}

.bo-chart.tone-mixed {
    --bo-tone: #a16207;
}

.bo-chart.tone-bad {
    --bo-tone: #c02626;
}

/* Same three tones as .tone-good / .tone-mixed / .tone-bad on the verdict text,
   lifted rather than re-picked: the chart is illustrating that line. */
@media (prefers-color-scheme: dark) {
    .bo-chart.tone-good {
        --bo-tone: #5dd48b;
    }

    .bo-chart.tone-mixed {
        --bo-tone: #e0b54a;
    }

    .bo-chart.tone-bad {
        --bo-tone: #ff8080;
    }
}

.bo-line {
    stroke: var(--bo-tone);
    stroke-width: 2.5;
    stroke-linejoin: round;
    stroke-linecap: round;
}

.bo-point {
    fill: var(--bo-tone);
}

.bo-area {
    fill: var(--bo-tone);
    opacity: 0.12;
}

.bo-note {
    margin: 0.4rem 0 0;
    font-size: 0.8rem;
    color: var(--muted);
}


.specs {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.4rem 1.25rem;
    margin: 0;
    max-width: 40rem;
}

.specs dt {
    color: var(--muted);
    font-size: 0.85rem;
}

.specs dd {
    margin: 0;
}

.empty,
.note {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 1rem 1.25rem;
    margin: 2rem 0;
}

.note summary {
    cursor: pointer;
    font-weight: 600;
}

.note p {
    color: var(--muted);
    margin: 0.75rem 0 0;
}

code {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 0.9em;
}

.site-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    max-width: var(--max);
    margin: 0 auto;
    padding: 2rem 1.25rem;
    border-top: 1px solid var(--line);
    color: var(--muted);
    font-size: 0.9rem;
}

.site-footer p {
    margin: 0;
    white-space: nowrap;
}

.site-footer a {
    text-decoration: none;
}

.prose {
    max-width: 60ch;
}

.prose h1 {
    font-size: clamp(1.6rem, 3.5vw, 2.2rem);
    letter-spacing: -0.02em;
    margin: 0 0 1rem;
}

.prose h2 {
    font-size: 1.1rem;
    margin: 2rem 0 0.5rem;
}

.attribution {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.6rem;
    color: var(--muted);
    font-size: 0.85rem;
}

.attribution img {
    display: block;
    width: 90px;
    height: auto;
}

/* --- The open slate --- */

/* The homepage hero is a single film with a call attached, not a pitch, so it
   is sized to be read and acted on in one screen rather than scrolled past. */
.call-hero {
    padding: 1.5rem 0 0.5rem;
    border-bottom: 1px solid var(--line);
    margin-bottom: 0.5rem;
}

.eyebrow {
    margin: 0 0 0.35rem;
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--muted);
}

.call-hero .lede {
    max-width: 52ch;
    font-size: 1.05rem;
    color: inherit;
}

/* The countdown is the whole reason to act today rather than whenever, so it
   reads as a status line and turns accent-coloured inside the last week. */
/* Accent, not muted. This line is the reason the card is on the page — the call
   is open and there is a deadline on it — and a grey countdown reads as filing
   detail next to the title above it. It was accent only inside the last week,
   which left the row telling most of its cards to come back later.

   No urgency split any more, in colour or in weight. It survived the first pass
   as bold-inside-a-week, which just moved the problem: a countdown is a countdown,
   and the number in it already says how soon. Two typographic weights for one
   kind of fact made the eleven-day cards read as the lesser sort of open call,
   which is not a thing the site believes — every one of them locks on the day it
   opens. .closes-urgent went with it, along with the three day-count reads in the
   templates that existed only to switch it on. */
.closes {
    margin: 0 0 1rem;
    font-size: 0.9rem;
    color: var(--accent);
    font-weight: 600;
}

/* The card variant, dialled back. Accent plus bold is right on the hero, where
   the countdown is one line in a large block and has a whole sentence attached
   to it; twelve of them in a grid is a page of blue shouting over the titles.
   Held back with opacity rather than a lighter blue so there is still one accent
   colour in the stylesheet and not a second one to keep in step with it. */
.closes-sm {
    margin: 0 0 0.6rem;
    font-size: 0.8rem;
    opacity: 0.8;
}

/* The hero title links to the film's page and should not look like it does —
   it is the page's h1 first. Underline on hover only, so the link is findable
   without the headline reading as body copy with a link in it. */
.hero h1 a {
    text-decoration: none;
}

.hero h1 a:hover,
.hero h1 a:focus-visible {
    text-decoration: underline;
    text-underline-offset: 4px;
}

/* --- Calling from a card --- */

/* margin-top:auto pins the control to the card foot so it lines up across a
   grid row whatever the title length — same trick .call-it used. */
.call-action {
    margin-top: auto;
}

.call-btns {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}

.call-btns form {
    margin: 0;
    flex: 1 1 auto;
}

.call-btn {
    width: 100%;
    padding: 0.45rem 0.6rem;
    font: inherit;
    font-size: 0.9rem;
    font-weight: 600;
    color: inherit;
    background: transparent;
    border: 1px solid var(--line);
    border-radius: 999px;
    cursor: pointer;
}

.call-btn:hover {
    border-color: var(--accent);
}

/* Only .vote-btn, not .call-btn. The movie page keeps both buttons live so a
   call can be switched, and there the chosen one is filled the same way a chosen
   rating is — hover stays an outline so the two never read alike. A card has no
   selected state to style: it resolves to .call-made instead.

   Same --fg fill as the rating pills, and deliberately not a red bomb and a green
   boom. Those are tone-bad and tone-good, which this stylesheet spends on
   *resolved* verdicts — wearing them here would have the site appear to grade a
   call, or hint at an answer, on a film whose whole premise is that nobody knows
   yet. The 💣 and 💥 carry the sentiment; the fill only records the choice. */
.vote-btn.is-selected {
    background: var(--fg);
    border-color: var(--fg);
    color: var(--bg);
}

/* The call not taken steps back, so the answer given is what the eye lands on.
   Hover restores it, since it is the control for changing your mind. */
.vote-widget:has(.is-selected) .vote-btn:not(.is-selected) {
    opacity: 0.45;
}

.vote-widget:has(.is-selected) .vote-btn:not(.is-selected):hover {
    opacity: 1;
}

/* The hero's call is the page's primary action, so its buttons get the size
   the movie page gives them rather than the card's compact treatment. */
.call-hero .call-btns {
    gap: 0.6rem;
    margin-bottom: 0.5rem;
}

.call-hero .call-btns form {
    flex: 0 0 auto;
}

.call-hero .call-btn {
    padding: 0.7rem 1.4rem;
    font-size: 1rem;
    border-radius: var(--radius);
}

/* Below the reveal threshold this is a progress line, not a result — muted so
   it reads as "fill this in", which is the honest state of a new board. */
.call-made {
    margin: 0 0 0.2rem;
    font-size: 0.9rem;
}

.call-hero .call-made {
    font-size: 1.1rem;
}

.call-tally {
    margin: 0;
    font-size: 0.8rem;
    color: var(--muted);
}

/* --- The homepage call bar --- */

/* Fixed, not sticky. The body sets overflow-x, and the `hidden` fallback for
   Safari before 16 makes a scroll container that a sticky child would stick to
   instead of the viewport — the case the note on body was reserving. Fixed is
   unaffected by either, and a bar that has to survive scrolling to card ten is
   the one place on the site that needs it. */
.call-bar-slot {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 30;
    display: flex;
    justify-content: center;
    /* The inset keeps the bar clear of the iOS home indicator and Safari's
       bottom toolbar, which otherwise sit on top of the last half-inch of the
       viewport — exactly where a bottom-anchored bar lives. Resolves to 0 on
       every browser that does not have one. */
    padding: 0 0.75rem calc(0.75rem + env(safe-area-inset-bottom));
    /* The slot spans the viewport but is mostly empty space; only the bar itself
       should be in the way of a card underneath it. */
    pointer-events: none;
}

/* Nothing to say, no strip. The slot is always rendered so the out-of-band swap
   has something to land on, and an empty target is not a visible element. */
.call-bar-slot:not(:has(.call-bar)) {
    display: none;
}

.call-bar {
    pointer-events: auto;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 0.3rem;
    max-width: var(--max);
    margin: 0;
    padding: 0.65rem 1.1rem;
    font-size: 0.9rem;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 999px;
    box-shadow: 0 2px 16px rgb(0 0 0 / 0.12);
}

.call-bar-count {
    font-weight: 600;
    white-space: nowrap;
}

.call-bar-ask {
    color: var(--muted);
}

.call-bar-ask a {
    color: var(--accent);
    text-underline-offset: 2px;
}

/* Room to read the last of the page with the bar sitting over it. Scoped with
   :has so the space only exists on the pages and sessions that show one. */
body:has(.call-bar) {
    padding-bottom: 5rem;
}

/* Narrow screens take the full width and drop the pill: the ask wraps to a
   second line there, and a pill around two lines of text reads as a mistake. */
@media (max-width: 32rem) {
    .call-bar-slot {
        padding: 0 0.5rem calc(0.5rem + env(safe-area-inset-bottom));
    }

    .call-bar {
        width: 100%;
        border-radius: var(--radius);
    }
}

/* --- Hero still --- */

/* The still runs edge to edge behind the hero and fades out downward, with the
   title and the call sitting in the faded part.

   The fade is a mask rather than a gradient overlay on purpose: a mask fades
   the picture to nothing and lets the page background through, whichever
   background that is. An overlay would have to be a fixed colour, and this
   site has a light theme and a dark one. */
.call-hero.has-backdrop {
    position: relative;
    /* How wide the still runs. Full-bleed stretched TMDB's 1280px backdrop
       roughly 2.7x past its own resolution on a large display, which read as
       pixellation — so it is capped just wider than the 62rem text column,
       enough to still feel like a backdrop rather than an inline picture.
       Tune here; the img's sizes attribute has to match. */
    --hero-width: 72rem;
    /* How far in from each side edge the picture reaches full opacity. Kept
       modest because the still is only 5rem wider than the text column on each
       side — a heavy side fade would start eating into the words. */
    --hero-fade-x: 10%;
    /* Cancel main's top padding so the picture starts at the very top of the
       page. The header is lifted out of the flow here (see .over-hero below), so
       the top of the still runs up behind it rather than stopping under it. */
    margin-top: -2rem;
    /* Text starts near the foot of the image, where the mask has taken it
       almost entirely away — that, not a scrim, is what keeps the words
       readable on both themes. */
    padding-top: clamp(12rem, 32vw, 22rem);
    /* The fade already separates the hero from the row beneath it; a rule as
       well would read as two dividers. */
    border-bottom: 0;
}

.hero-backdrop {
    /* Escapes main's centred max-width, then takes its own. */
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: min(100vw, var(--hero-width));
    max-width: none;
    height: clamp(15rem, 40vw, 28rem);
    object-fit: cover;
    /* Faces are usually in the upper middle of a backdrop; centring vertically
       tends to crop them out. */
    object-position: center 25%;
    /* Vertical fade only. The side fade is added in the @supports block below,
       because it needs a second mask layer and those two layers are only
       useful if they can be intersected. */
    -webkit-mask-image: linear-gradient(to bottom,
            #000 0%,
            #000 30%,
            rgba(0, 0, 0, 0.45) 62%,
            rgba(0, 0, 0, 0.12) 82%,
            transparent 94%);
    mask-image: linear-gradient(to bottom,
            #000 0%,
            #000 30%,
            rgba(0, 0, 0, 0.45) 62%,
            rgba(0, 0, 0, 0.12) 82%,
            transparent 94%);
    pointer-events: none;
    z-index: 0;
}

/* Side fade: a second mask layer, intersected with the vertical one so the
   picture blends into the page at the left and right edges instead of stopping
   at a hard vertical line. Where the two overlap at the corners the falloff
   compounds, so the corners go first.

   Guarded, and this one matters: mask layers composite additively by default,
   meaning the union of the two. The horizontal layer is fully opaque across its
   whole middle, so a union would cancel the bottom fade — the failure mode is
   not "no side fade" but "no fade at all, hard edge under the text". Better to
   leave a browser that cannot intersect with the vertical fade it already had. */
@supports ((mask-composite: intersect) or (-webkit-mask-composite: source-in)) {
    .hero-backdrop {
        -webkit-mask-image:
            linear-gradient(to bottom,
                #000 0%,
                #000 30%,
                rgba(0, 0, 0, 0.45) 62%,
                rgba(0, 0, 0, 0.12) 82%,
                transparent 94%),
            linear-gradient(to right,
                transparent 0%,
                #000 var(--hero-fade-x),
                #000 calc(100% - var(--hero-fade-x)),
                transparent 100%);
        mask-image:
            linear-gradient(to bottom,
                #000 0%,
                #000 30%,
                rgba(0, 0, 0, 0.45) 62%,
                rgba(0, 0, 0, 0.12) 82%,
                transparent 94%),
            linear-gradient(to right,
                transparent 0%,
                #000 var(--hero-fade-x),
                #000 calc(100% - var(--hero-fade-x)),
                transparent 100%);
        /* intersect is the standard keyword; source-in is its -webkit- spelling. */
        -webkit-mask-composite: source-in;
        mask-composite: intersect;
    }
}

/* Everything else in the hero stacks above the picture. */
.call-hero.has-backdrop>*:not(.hero-backdrop) {
    position: relative;
    z-index: 1;
}

/* A hero this tall would push the call off a phone screen, so the picture and
   the runway above the text both shrink.

   These were briefly larger, to clear a header that took two lines at this width
   and left the eyebrow starting just under the bar. The field closes to its
   magnifier now and the bar is back to one line, so the runway is back to what it
   was — roughly 3.5rem of clear picture between the two. */
@media (max-width: 34rem) {
    .call-hero.has-backdrop {
        padding-top: 9rem;
    }

    .hero-backdrop {
        height: 13rem;
    }
}

/* --- Header on the still --- */

/* On the homepage the still is the top of the page, so the header rides on it
   instead of sitting in a strip above it.

   Taken out of the flow rather than given a negative margin: out of the flow the
   header contributes no height at all, so main — and with it the hero and its
   picture — starts at y=0 on its own, with nothing to keep in step with a header
   height that changes with the viewport and with whether you are signed in.
   Everything below the hero keeps the spacing it already had, because the whole
   hero moves up together with the picture behind it. */
.over-hero .site-header {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    /* A rule here would draw a line across the picture. The still's own fade is
       the separator on this page. */
    border-bottom: 0;
    /* Fixed light, not var(--fg): what is behind the header is a photograph, not
       the page background, so the light theme's near-black would be as unreadable
       here as it is legible everywhere else. */
    color: #fff;
}

/* The mask leaves the top of the still at full strength, and a still is whatever
   the film's marketing made it, so the bar cannot rely on the picture being dark
   or quiet where the words land. The scrim is its own ground; it fades out by the
   foot of the bar so it reads as part of the picture rather than as a panel
   sitting on it.

   A layer of its own rather than a background on the bar. The bar is capped at
   the text column and the still behind it is wider, so a background painted on
   the bar stopped short of the picture's edges — two hard vertical lines partway
   across the image. This escapes the cap and runs the full width of the page.

   100vw counts the scrollbar and so overruns by its width; body clips that,
   which is the same arrangement the still itself already relies on. */
.over-hero .site-header::before {
    content: "";
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 100vw;
    height: 100%;
    background: linear-gradient(to bottom,
            rgba(0, 0, 0, 0.6) 0%,
            rgba(0, 0, 0, 0.35) 55%,
            transparent 100%);
    /* Behind the bar's own contents but inside the bar's stacking context, so it
       stays above the still without having to out-rank the header itself. */
    z-index: -1;
    pointer-events: none;
}

.over-hero .site-nav a {
    color: rgba(255, 255, 255, 0.85);
}

.over-hero .site-nav a:hover {
    color: #fff;
}

/* --- Header controls on the still --- */

/* var(--line) is a hairline meant for the page background and disappears against
   a photograph. The field also takes a wash of its own: an outline alone leaves
   whatever is inside it competing with the typed text. */
.over-hero .search-form input[type="search"] {
    background: rgba(0, 0, 0, 0.25);
    border-color: rgba(255, 255, 255, 0.45);
}

.over-hero .search-form input[type="search"]::placeholder {
    color: rgba(255, 255, 255, 0.7);
}

/* No brightened border on focus here any more: focus now repaints the field in
   page colour to match the results card, so it is no longer sitting on the
   photograph and a white hairline would be the one part of it that still thinks
   it is. The accent focus ring does the job on both surfaces. */

.over-hero .search-go {
    color: rgba(255, 255, 255, 0.8);
}

.over-hero .search-go:hover {
    color: #fff;
}

/* The filled pill inverts on this page. Its usual fill is var(--fg), which in
   the light theme is the same near-black as the scrim it would be sitting on —
   the one place the site's most prominent action would go quietest. */
.over-hero .site-nav .nav-cta {
    color: #16161a;
    background: #fff;
}

.over-hero .site-nav .nav-cta:hover {
    color: #fff;
    background: var(--accent);
}

/* The panel is a page-coloured card hanging below the bar, so it leaves the
   scrim's palette behind and goes back to the page's own. */
.over-hero .search-panel {
    color: var(--fg);
}

/* --- Missing artwork --- */

/* TMDB has no poster for a film until it has been shot and marketed, so the
   furthest-out titles have none. Name the film rather than show a blank box. */
.card-poster-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    width: 100%;
    height: 100%;
    padding: 0.9rem;
    text-align: center;
}

.cpe-title {
    font-size: 0.95rem;
    font-weight: 600;
    line-height: 1.3;
}

.cpe-note {
    font-size: 0.75rem;
    color: var(--muted);
}

/* --- Trimming a long slate --- */

/* The slate grows with the catalogue and most of it is months out, so the row
   opens at a first screenful and the rest is one click away. Everything is
   server-rendered either way — the toggle is a checkbox, not a script, so
   expanding costs no request and works with JS off.

   Default is hidden: the button only earns its place where the CSS that powers
   it is supported (see the @supports block at the foot of this section). */
.see-more {
    display: none;
    width: fit-content;
    margin: 1.25rem auto 0;
    padding: 0.55rem 1.2rem;
    font-size: 0.95rem;
    font-weight: 600;
    border: 1px solid var(--line);
    border-radius: 999px;
    cursor: pointer;
    user-select: none;
}

.see-more:hover {
    border-color: var(--accent);
}

.see-more-close {
    display: none;
}

/* The input sits immediately before its label, so these are adjacent siblings
   and need no :has(). */
.slate-toggle:checked+.see-more .see-more-open {
    display: none;
}

.slate-toggle:checked+.see-more .see-more-close {
    display: inline;
}

/* The checkbox is clipped out of sight, so its focus ring has to be drawn on
   the label instead — otherwise tabbing to it shows nothing at all. */
.slate-toggle:focus-visible+.see-more {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* The cards being toggled come *before* the checkbox in the source, so the
   reveal has to read backwards up the tree — which is :has(), not a sibling
   combinator. The checkbox has to stay down here next to its label: clicking a
   label focuses its control, and the browser scrolls that control into view, so
   a checkbox up above the grid yanked the page back to the top of the row on
   every click.

   Guarded, because without :has() the hide would have no matching reveal. The
   failure mode is then the harmless one — every card renders and the button
   stays hidden, rather than seven films becoming unreachable. */
@supports selector(:has(*)) {
    .slate-overflow {
        display: none;
    }

    /* .movie-card is a flex column, so revealing restores flex, not block. */
    .slate:has(.slate-toggle:checked) .slate-overflow {
        display: flex;
    }

    .see-more {
        display: block;
    }
}

/* --- Account nav --- */

.site-nav {
    display: flex;
    align-items: center;
    gap: 1rem;
}

.site-nav a,
.linkish {
    color: var(--muted);
    text-decoration: none;
    font-size: 0.95rem;
}

.site-nav a:hover,
.linkish:hover {
    color: var(--fg);
}

.signout-form {
    display: flex;
    margin: 0;
}

/* Sign-out has to be a POST, so it is a button that has to read as a nav link
   — stripped back to inherit rather than restyled to imitate one. */
.linkish {
    padding: 0;
    font-family: inherit;
    background: none;
    border: 0;
    cursor: pointer;
}

/* --- Sign in --- */

.narrow {
    max-width: 44ch;
}

.auth-form {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.6rem;
    margin: 1.5rem 0;
}

.auth-form label {
    font-size: 0.9rem;
    color: var(--muted);
}

.auth-form input[type="email"] {
    width: 100%;
    padding: 0.65rem 0.8rem;
    font: inherit;
    color: inherit;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: var(--radius);
}

.auth-form input[type="email"]:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

.btn-primary {
    display: inline-block;
    padding: 0.65rem 1.1rem;
    font: inherit;
    font-weight: 600;
    font-size: 0.95rem;
    color: var(--bg);
    text-decoration: none;
    background: var(--fg);
    border: 1px solid var(--fg);
    border-radius: var(--radius);
    cursor: pointer;
}

.btn-primary:hover {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}

/* The same shape as btn-primary without the weight, for the action that is
   offered rather than the one being asked for. */
.btn-secondary {
    display: inline-block;
    padding: 0.65rem 1.1rem;
    font: inherit;
    font-weight: 600;
    font-size: 0.95rem;
    color: var(--fg);
    text-decoration: none;
    background: transparent;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    cursor: pointer;
}

.btn-secondary:hover {
    border-color: var(--fg);
}

.form-error {
    padding: 0.7rem 0.9rem;
    font-size: 0.9rem;
    border: 1px solid var(--line);
    border-left: 3px solid #c0392b;
    border-radius: var(--radius);
}

/* --- Your calls --- */

.calls-head,
.record-head {
    margin-bottom: 2.5rem;
}

.calls-head h1,
.record-head h1 {
    font-size: clamp(1.6rem, 3.5vw, 2.2rem);
    letter-spacing: -0.02em;
    margin: 0 0 1rem;
}

/* Named for what it holds, not for what you do with it. A class called
   "share-anything" is matched by the cosmetic filters every blocklist carries
   against social-share widgets, and the whole section disappears for anybody
   running one — markup present in the response, nothing on the screen. */
.record-head .lede {
    max-width: 55ch;
}

/* --- The scorecard ticket ---

   A record nobody can show anybody recruits nobody, and the one thing this site
   has to spread is somebody's hit rate. So the card is built to be a screenshot:
   it carries its own wordmark, and it holds its own colours in light and dark
   rather than following the page. That second part is deliberate. An object does
   not change colour because the wall behind it did, and a ticket that arrives in
   a group chat looking the same every time is a ticket people start to
   recognise.

   Cinema stub, then, down to the torn notches and the small print along the
   bottom — a shape that already means "I went and saw something", which is
   exactly what a settled call is. */

.scorecard {
    margin: 1.5rem 0;
}

.sc-ticket {
    --tk-ink: #f4f1e8;
    --tk-ground: #191a1f;
    --tk-red: #e2493c;
    --tk-muted: #9a958a;
    --tk-line: #3a3b42;

    display: flex;
    max-width: 28rem;
    background: var(--tk-ground);
    color: var(--tk-ink);
    border-radius: 3px;
    /* The torn edges. Two circles knocked out of the card's own shape, so the
       notches show the page through rather than being painted the page's colour
       — which would only work until someone screenshotted it against something
       else. Where mask-composite is unsupported the notches simply do not
       appear and the card is a plain rectangle. */
    -webkit-mask:
        radial-gradient(circle 11px at 0 50%, #0000 98%, #000),
        radial-gradient(circle 11px at 100% 50%, #0000 98%, #000);
    -webkit-mask-composite: source-in;
    mask:
        radial-gradient(circle 11px at 0 50%, #0000 98%, #000),
        radial-gradient(circle 11px at 100% 50%, #0000 98%, #000);
    mask-composite: intersect;
}

.sc-main {
    flex: 1;
    min-width: 0;
    padding: 1.5rem 1.4rem 1.2rem;
}

.sc-top {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
    margin: 0;
    font: 600 0.62rem/1.3 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--tk-muted);
}

/* Ticket red, not a warning: this is the accent the stub is printed in, and it
   says the same thing whether the record behind it is good or bad. */
.sc-top b {
    color: var(--tk-red);
    text-align: right;
}

.sc-n {
    margin: 1.1rem 0 0;
    font: 700 clamp(3.2rem, 12vw, 4.2rem)/0.8 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    letter-spacing: -0.04em;
    font-variant-numeric: tabular-nums;
}

/* The per-cent sign rides high and quiet, so the number itself is what carries
   across a screenshot at thumbnail size. */
.sc-n em {
    font-style: normal;
    font-size: 0.28em;
    font-weight: 700;
    letter-spacing: 0.05em;
    vertical-align: 0.9em;
    margin-left: 0.1em;
    color: var(--tk-muted);
}

.sc-n-quiet {
    color: var(--tk-line);
}

.sc-label {
    margin: 0.7rem 0 0;
    font: 600 0.7rem/1.3 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    letter-spacing: 0.18em;
    text-transform: uppercase;
}

.sc-sub {
    margin: 0.4rem 0 0;
    font-size: 0.85rem;
    color: var(--tk-muted);
}

.sc-sub b {
    color: var(--tk-ink);
    font-weight: 600;
}

/* Progress toward the three settled calls that take the hit rate out of
   provisional. Shown only while it is provisional, so the card is a line
   shorter once the figure stands on its own. */
.sc-meter {
    display: block;
    width: 100%;
    max-width: 11rem;
    height: 3px;
    margin-top: 0.8rem;
    background: var(--tk-line);
    overflow: hidden;
}

.sc-meter span {
    display: block;
    height: 100%;
    background: var(--tk-ink);
}

/* Form: one mark per settled call, oldest first. Wraps rather than scrolls, so
   a long run stays entirely inside the screenshot. */
.sc-form {
    display: flex;
    flex-wrap: wrap;
    gap: 0.22rem;
    margin: 1.2rem 0 0;
}

.sc-pip {
    width: 1.5rem;
    height: 0.3rem;
    background: var(--tk-line);
}

.sc-pip-right {
    background: var(--tk-ink);
}

.sc-pip-wrong {
    background: var(--tk-red);
}

/* The small print. A ticket's terms line, carrying the counts that do not
   deserve their own headline but would be missed if they were dropped. */
.sc-terms {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 0.9rem;
    margin: 1.2rem 0 0;
    padding-top: 0.9rem;
    border-top: 1px dashed var(--tk-line);
    font: 600 0.63rem/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--tk-muted);
}

.sc-terms div {
    display: flex;
    gap: 0.35rem;
}

.sc-terms dt {
    order: 2;
}

.sc-terms dd {
    order: 1;
    margin: 0;
    color: var(--tk-ink);
    font-variant-numeric: tabular-nums;
}

/* The tear-off stub. It exists to carry the wordmark: this is the strip that
   survives when someone crops the screenshot to the card. */
.sc-stub {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: space-between;
    gap: 0.8rem;
    width: 3.2rem;
    padding: 1.5rem 0;
    border-left: 2px dashed var(--tk-line);
}

.sc-stub b,
.sc-stub span {
    writing-mode: vertical-rl;
    font: 600 0.66rem/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    letter-spacing: 0.2em;
    text-transform: uppercase;
}

.sc-stub span {
    font-size: 0.6rem;
    color: var(--tk-muted);
}

.sc-actions {
    margin: 0.9rem 0 0;
}

@media (max-width: 30rem) {
    .sc-main {
        padding: 1.25rem 1.1rem 1rem;
    }

    .sc-stub {
        width: 2.5rem;
    }
}

.claim-box {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 1rem;
    margin: 1.5rem 0;
    padding: 1.1rem 1.25rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
}

.claim-box p {
    flex: 1 1 20rem;
    margin: 0;
    font-size: 0.95rem;
}

.calls-section {
    margin-bottom: 2.5rem;
}

.call-list {
    list-style: none;
    margin: 1rem 0 0;
    padding: 0;
}

.call-row {
    display: flex;
    gap: 1rem;
    padding: 1rem 0;
    border-top: 1px solid var(--line);
}

.call-poster {
    flex: 0 0 4.5rem;
}

.call-poster img {
    display: block;
    width: 100%;
    height: auto;
    border-radius: 6px;
}

.call-detail h3 {
    font-size: 1rem;
    margin: 0 0 0.3rem;
}

.call-detail h3 a {
    text-decoration: none;
}

.call-detail p {
    margin: 0 0 0.25rem;
    font-size: 0.9rem;
}

.call-yours {
    color: var(--muted);
}

.call-result {
    font-weight: 600;
}

.call-pending {
    color: var(--muted);
}

/* --- Account menu --- */

.user-menu {
    position: relative;
}

/* The avatar is the <summary>, so the default disclosure triangle has to go in
   both the modern and the WebKit-prefixed way. */
.avatar {
    display: grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    font-size: 0.85rem;
    font-weight: 700;
    color: #fff;
    /* Hue comes from the address, so the circle is stable per account and two
       people on a shared machine are not the same anonymous blob. Fixed
       saturation and lightness keep contrast with white text predictable at
       every hue. */
    background: hsl(var(--avatar-hue, 220) 45% 42%);
    border-radius: 50%;
    cursor: pointer;
    list-style: none;
    user-select: none;
}

.avatar::-webkit-details-marker {
    display: none;
}

.user-menu[open] .avatar,
.avatar:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.user-menu-panel {
    position: absolute;
    right: 0;
    top: calc(100% + 0.5rem);
    z-index: 20;
    min-width: 12rem;
    padding: 0.4rem;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
}

.user-menu-email {
    margin: 0 0 0.3rem;
    padding: 0.4rem 0.6rem;
    font-size: 0.8rem;
    color: var(--muted);
    border-bottom: 1px solid var(--line);
    /* Long addresses must not stretch the panel across the viewport. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.user-menu-panel a,
.user-menu-panel button {
    display: block;
    width: 100%;
    padding: 0.5rem 0.6rem;
    font: inherit;
    font-size: 0.9rem;
    color: inherit;
    text-align: left;
    text-decoration: none;
    background: none;
    border: 0;
    border-radius: 6px;
    cursor: pointer;
}

.user-menu-panel a:hover,
.user-menu-panel button:hover {
    background: color-mix(in srgb, var(--fg) 8%, transparent);
}

.user-menu-panel form {
    margin: 0;
}

/* --- Settings --- */

.setting-block {
    /* Anchor for .disclosure-corner's summary. */
    position: relative;
    margin: 2rem 0;
    padding-top: 1.5rem;
    border-top: 1px solid var(--line);
}

.setting-block h2 {
    font-size: 1rem;
    margin: 0 0 0.5rem;
}

.setting-value {
    margin: 0 0 0.3rem;
    font-size: 1.05rem;
    font-weight: 600;
    word-break: break-all;
}

.setting-form {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1rem;
    margin: 0;
}

.checkbox-row {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    font-size: 0.95rem;
    cursor: pointer;
}

.checkbox-row input {
    margin-top: 0.25rem;
    flex: 0 0 auto;
}

.notice {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.75rem 1rem;
    margin: 1.5rem 0;
    padding: 1rem 1.15rem;
    border: 1px solid var(--line);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius);
}

.notice p {
    flex: 1 1 18rem;
    margin: 0;
    font-size: 0.9rem;
}

.notice form {
    margin: 0;
}

.notice-good {
    border-left-color: #2e7d4f;
}

/* The change-email form reuses .setting-form's column layout but needs a real
   input width; .auth-form's input rule is scoped to that class alone. */
.setting-form input[type="email"] {
    width: 100%;
    max-width: 22rem;
    padding: 0.6rem 0.8rem;
    font: inherit;
    color: inherit;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: var(--radius);
}

.setting-form input[type="email"]:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

.setting-form label {
    font-size: 0.9rem;
    color: var(--muted);
}

/* --- Collapsible settings block --- */

.disclosure > summary {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
    list-style: none;
}

.disclosure > summary::-webkit-details-marker {
    display: none;
}

/* Own marker, so it can rotate on open — the native triangle is not stylable
   consistently across browsers. */
.disclosure > summary::before {
    content: "";
    width: 0;
    height: 0;
    border-left: 5px solid currentColor;
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    transition: transform 0.15s;
}

.disclosure[open] > summary::before {
    transform: rotate(90deg);
}

.disclosure > summary:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: 4px;
}

.disclosure > *:not(summary) {
    margin-top: 1rem;
}

/* Sits inside the email block, under the address and any notice about it. */
.setting-block .disclosure {
    margin-top: 1.5rem;
}

/* Summary lifted out of the flow and set beside the block's heading. The panel
   it controls stays where it is in the source — at the foot of the block — so
   opening the form does not shove the address and its status down the page.

   Reset of .disclosure's own summary styling: up here it is a quiet secondary
   action next to a heading, not a heading itself. */
.disclosure-corner > summary {
    position: absolute;
    top: 1.55rem;
    right: 0;
    font-size: 0.8rem;
    font-weight: 400;
    color: var(--muted);
}

.disclosure-corner > summary:hover {
    color: var(--fg);
}

.disclosure-corner > summary::before {
    border-left-width: 4px;
    border-top-width: 3px;
    border-bottom-width: 3px;
}

/* Narrow screens have no room beside the heading, and an absolute element there
   would sit on top of it. Returned to the flow, where it lands under the
   address — still out of the way, still nowhere near the middle of the block. */
@media (max-width: 30rem) {
    .disclosure-corner > summary {
        position: static;
        margin-top: 0.25rem;
    }
}

/* Sign in / up is the one action in the header, so it stops looking like the
   navigation around it. History is somewhere you go; this is something you do,
   and as identical muted links they read as a pair of equal options.

   Filled rather than outlined: the header already carries a bordered search
   button, and another outline beside it would blend into furniture rather than
   stand out. Needs to beat `.site-nav a`'s muted colour, hence the scoping. */
.site-nav .nav-cta {
    padding: 0.4rem 0.75rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--bg);
    background: var(--fg);
    border-radius: var(--radius);
    white-space: nowrap;
}

.site-nav .nav-cta:hover {
    color: #fff;
    background: var(--accent);
}
