.ratio-2x3 { --bs-aspect-ratio: 150%; } /* 2:3 => 3/2 * 100 = 150% */
.object-fit-cover { object-fit: cover; }

/* A grade is drawn the same way wherever it is read — on a movie page, in a
   feed — so the stars live here rather than in the stylesheet of any one page. */
.movie-stars {
    color: var(--mb-star);
    white-space: nowrap;
}

.movie-stars .bi-star {
    color: var(--mb-star-empty);
}

/* What a title is to somebody, said the same way everywhere it is said, in the
   colours the theme gives each shelf (--mb-state-*). Naming them once is what
   stops the next page from picking its own and meaning something else by them.

   Gold is not a state — it belongs to the grade, and lives on .movie-stars above,
   which every page can already reach. */
.state-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3em;
    padding: 0.15em 0.5em;
    border: 1px solid var(--state-edge);
    border-radius: var(--mb-radius-sm);
    background-color: var(--state-tint);
    color: var(--state-ink);
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1.5;
    /* Two words about one movie: breaking them apart would read as two badges. */
    white-space: nowrap;
}

/* The glyph keeps the full colour; the words step back to a darker shade of it,
   which is the only way the label passes contrast on a tint this light. */
.state-badge .bi { color: var(--state-icon); }

.state-watched {
    --state-icon: var(--mb-state-watched);
    --state-tint: var(--mb-state-watched-tint);
    --state-edge: var(--mb-state-watched-edge);
    --state-ink: var(--mb-state-watched-ink);
}

/* Drawn all along by the badge, which named a class nothing defined: an
   activity saying "is watching a season" wore the shape and none of the colour. */
.state-watching {
    --state-icon: var(--mb-state-watching);
    --state-tint: var(--mb-state-watching-tint);
    --state-edge: var(--mb-state-watching-edge);
    --state-ink: var(--mb-state-watching-ink);
}

.state-watchlist {
    --state-icon: var(--mb-state-watchlist);
    --state-tint: var(--mb-state-watchlist-tint);
    --state-edge: var(--mb-state-watchlist-edge);
    --state-ink: var(--mb-state-watchlist-ink);
}

.state-dropped {
    --state-icon: var(--mb-state-dropped);
    --state-tint: var(--mb-state-dropped-tint);
    --state-edge: var(--mb-state-dropped-edge);
    --state-ink: var(--mb-state-dropped-ink);
}

/* Not a shelf but the absence of one, which the tracking card still has to name
   so that the control saying where a member stands says something in every
   state instead of disappearing in one of them. */
.state-untracked {
    --state-icon: var(--mb-state-untracked);
    --state-tint: var(--mb-state-untracked-tint);
    --state-edge: var(--mb-state-untracked-edge);
    --state-ink: var(--mb-state-untracked-ink);
}

/* The same colours where there is no room for a badge — a tab label, a line in a
   panel — so an icon on its own still says which state it names. */
.state-ink-watched { color: var(--mb-state-watched); }
.state-ink-watching { color: var(--mb-state-watching); }
.state-ink-watchlist { color: var(--mb-state-watchlist); }
.state-ink-dropped { color: var(--mb-state-dropped); }

/* One activity, as a card. The border is faint and the corners are wide,
   because a feed is twenty of these in a column and a hard frame around each
   turns a page of posts into a page of boxes.

   Nothing inside it is padded by the card: the conversation under a post runs
   to these edges on a ground of its own, and a padded card would leave it
   floating inside a frame with the post instead of sitting beneath it. */
.activity-card {
    background-color: var(--mb-surface);
    border: 1px solid var(--mb-border-faint);
    border-radius: 1rem;
    /* Barely there, and that is the point: enough that a card reads as sitting
       on the page rather than as cut out of it, not enough to be noticed. */
    box-shadow: var(--mb-shadow-sm);
    /* The band at the foot goes edge to edge; the corners have to cut it. */
    overflow: hidden;
}

.activity-post { padding: 1rem; }

/* Why the post is here, above who wrote it. Quiet on purpose: it is a note
   about the card and not part of it, and it is set in the column the avatar
   below occupies so the two names never line up as one. */
.activity-shared {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    margin-bottom: 0.375rem;
    padding-left: 3.375rem;
    color: var(--mb-ink-muted);
    font-size: 0.8125rem;
}

.activity-shared a {
    color: inherit;
    font-weight: 600;
}

/* What a reply answers, in the same register and the same indent as the line
   above: both are notes about the card rather than parts of it. The whole line
   is the link, because there is one thing to reach from it and a name that
   linked somewhere else would leave two.

   Held to one line and cut short where it will not fit. A title is as long as
   somebody else made it, and a reference that wraps to three lines is taller
   than the card it is a footnote to. */
.activity-answering {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    margin-bottom: 0.375rem;
    padding-left: 3.375rem;
    color: var(--mb-ink-muted);
    font-size: 0.8125rem;
    min-width: 0;
}

.activity-answering a {
    color: inherit;
    font-weight: 600;
    text-decoration: none;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.activity-answering a:hover,
.activity-answering a:focus { text-decoration: underline; }

/* The head of an activity: who posted it and what they did, on one line, with
   the moment held against the right edge. The badge follows that sentence
   rather than the far edge, where it used to land in a different place on every
   card and move again whenever a long title wrapped. */
.activity-byline {
    display: flex;
    align-items: flex-start;
    gap: 0.625rem;
}

.activity-avatar,
.activity-avatar img,
.activity-avatar-blank {
    width: 2.75rem;
    height: 2.75rem;
    border-radius: 50%;
}

.activity-avatar {
    display: block;
    flex-shrink: 0;
}

.activity-avatar img { object-fit: cover; }

.activity-avatar-blank {
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: var(--mb-surface-hover);
    color: var(--mb-ink-muted);
}

/* The name, what they did, and the handle underneath: one line where there is
   room for the first two and a wrapped one where there is not.

   The three are laid out together rather than as a sentence stacked on a
   sub-line, because where the sentence goes when it will not fit depends on the
   handle — it goes under it, not between the two halves of who posted this. */
.activity-byline-text {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: 0.35em;
    row-gap: 0.15rem;
    /* A flex child sizes to its content, so a long display name would push the
       moment off the card instead of being cut short. */
    min-width: 0;
    flex-grow: 1;
}

/* What they did, in one piece: a flex item is never split across lines, so the
   badge cannot be left behind on its own. The words inside it still wrap. */
.activity-what {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.35em;
    min-width: 0;
}

/* Said by byline_fit.js when the sentence did not fit beside the name: it drops
   below the handle, and who posted this is read as one block. */
.activity-byline-text.is-split > .activity-what {
    order: 1;
    flex-basis: 100%;
}

.activity-verb { color: var(--mb-ink-muted); }

/* A column of its own at the far edge, out of the way of the words. It used to
   wrap with them, and on a narrow card that put it on the badge's line with the
   width of the card between the two; below with the handle it pushed the handle
   onto a line of its own and left the separator dangling at the front of it.

   A column is only affordable because the moment is short — "3h", "8 août" —
   which is what a full date never was. It costs the sentence the same few
   characters at every width, and the whole moment is on its title.

   Nudged down to sit on the name's baseline: the two are set at different sizes
   and aligning their boxes leaves the smaller one riding high. */
.activity-when {
    flex-shrink: 0;
    padding-top: 0.1rem;
    font-size: 0.8125rem;
    color: var(--mb-ink-muted);
    white-space: nowrap;
}

/* The moment as the way into the post. It looks like what it looked like before
   it was a link — a date at the end of a byline, not a call to action — and says
   so on hover, which is where a reader finds out that a quiet thing is
   clickable. The anchor takes over as the flex item, so it inherits the column
   the time held. */
a.moment-link {
    flex-shrink: 0;
    color: inherit;
    text-decoration: none;
}

a.moment-link:hover,
a.moment-link:focus { text-decoration: underline; }

.activity-name {
    display: block;
    font-weight: 600;
    font-size: 1rem;
    color: var(--mb-ink-strong);
    text-decoration: none;
    /* One line: a display name is anything its owner typed, and a card is not
       the place to find out how long that was. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.activity-name:hover,
.activity-name:focus { text-decoration: underline; }

.activity-byline-sub {
    display: flex;
    align-items: baseline;
    margin: 0;
    /* Always a line of its own: it is the other half of who posted this, and it
       is what the sentence drops below when the sentence has to drop. */
    flex-basis: 100%;
    /* One line, whatever happens. The glyph after the handle is a few pixels
       wide and allowed to wrap it landed alone on the line below, with the
       separator drawn in front of it and nothing before it to separate. The
       handle is what gives way instead, and a shortened handle still starts
       with the name. */
    flex-wrap: nowrap;
    gap: 0.4em;
    font-size: 0.8125rem;
    color: var(--mb-ink-muted);
    min-width: 0;
}

.activity-handle {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.activity-byline-sub > time { white-space: nowrap; }

.activity-byline-sub > * + *::before {
    content: '·';
    margin-right: 0.4em;
}

.activity-reach { flex-shrink: 0; }

/* Set as a marker rather than as a word: it is the one thing on the card that
   is read without being read, and small capitals are what make it scan as a
   state and not as more of the sentence beside it. Only here — the same badge
   under a poster on a shelf is a label, and labels are read. */
.activity-what .state-badge {
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 0.6875rem;
    border-radius: 0.5rem;
    padding: 0.2em 0.5em;
}

/* The badge keeps its word at every width. It used to lose it on a phone, back
   when it shared the line with the name and the menu and there was nothing left
   to name anybody with; the sentence takes that line now and the badge wraps
   under it, where a lone glyph in a pill says nothing at all. */

/* What the activity is about: the poster, and the words beside it. */
.activity-body {
    display: flex;
    gap: 0.875rem;
    margin-top: 0.875rem;
}

/* A flex child is as wide as its content by default, so a long unbroken word in
   a review would push the card past the edge of the screen instead of wrapping. */
.activity-about {
    min-width: 0;
    flex-grow: 1;
}

.activity-title {
    margin: 0 0 0.5rem;
    font-size: 1.0625rem;
    line-height: 1.3;
}

.activity-title a {
    font-weight: 600;
    color: var(--mb-ink-strong);
    text-decoration: none;
}

.activity-title a:hover,
.activity-title a:focus { text-decoration: underline; }

/* The year is not part of the name and is no longer set as though it were: the
   title carries the weight and the year steps back beside it. */
.activity-year {
    margin-left: 0.4em;
    color: var(--mb-ink-muted);
    font-weight: 400;
    /* A year is a fact about the film, not part of what it is called. */
    font-size: 0.9375rem;
}

/* What the thing is, what it was given, and when it was seen: one row, and one
   that wraps rather than making the card wider. */
.activity-facts {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin: 0 0 0.5rem;
    font-size: 0.875rem;
}

/* The facts about the title, held on a ground of their own so they read as one
   statement about it rather than as loose words trailing off the name. Filled
   rather than outlined: an outline is a second frame inside a card that already
   has one, and at this size it reads as a control the reader may press. */
.activity-chip {
    display: inline-flex;
    align-items: center;
    /* A narrow card runs out of room for the whole statement, and the facts
       give way one at a time rather than the last one being cut in half. */
    flex-wrap: wrap;
    gap: 0.4em;
    padding: 0.3rem 0.6rem;
    border-radius: 0.6rem;
    background-color: var(--mb-surface-sunken);
    color: var(--mb-ink-muted);
    font-size: 0.8125rem;
    line-height: 1.4;
}

/* Each fact is one thing and breaks nowhere: "2h 46" split across two lines is
   read as two numbers, and "S03E07" as anything at all. */
.activity-chip > span { white-space: nowrap; }

/* On a show, the chip is the way to the season or the episode — the title above
   it names the series and goes there. Not underlined at rest: it sits on a
   ground of its own, which is what a reader reads as something to press. */
a.activity-chip { text-decoration: none; }

a.activity-chip:hover,
a.activity-chip:focus-visible {
    background-color: var(--mb-surface-hover);
    color: var(--mb-ink-strong);
}

/* Which episode, which season, how long: the piece a reader is actually looking
   for. The rest of the chip qualifies it and steps back. */
.activity-chip > span:first-of-type {
    font-weight: 600;
    color: var(--mb-ink-strong);
}

/* Drawn by whichever piece has something before it, never typed into the chip:
   which facts a title has depends on what kind of title it is. */
.activity-chip > span + span::before {
    content: '·';
    margin-right: 0.4em;
}

.activity-review { margin: 0; }

/* A peer's sentence arrives as paragraphs of its own, and the last of them
   would otherwise hang a margin off the bottom of the card. */
.activity-review > :last-child { margin-bottom: 0; }

/* Words past the episode the reader has reached, folded under one line. The
   line is a control and looks like one; the words, once shown, read as they
   would have unfolded. */
.activity-spoiler > .spoiler-summary {
    display: flex;
    align-items: center;
    gap: .4rem;
    padding: .35rem .6rem;
    border: 1px dashed var(--mb-border);
    border-radius: var(--mb-radius);
    color: var(--mb-ink-muted);
    font-size: .875rem;
    cursor: pointer;
    list-style: none;
}
.activity-spoiler > .spoiler-summary::-webkit-details-marker { display: none; }
.activity-spoiler > .spoiler-summary:focus-visible {
    outline: 2px solid var(--mb-accent);
    outline-offset: 2px;
}
.spoiler-summary .spoiler-show,
.spoiler-summary .spoiler-hide { margin-left: auto; color: var(--mb-link); }
.spoiler-summary .spoiler-hide,
.activity-spoiler[open] > .spoiler-summary .spoiler-show { display: none; }
.activity-spoiler[open] > .spoiler-summary .spoiler-hide { display: inline; }
.activity-spoiler[open] > .spoiler-summary { margin-bottom: .5rem; }
.activity-thread > .activity-spoiler { margin: .5rem .75rem; }
/* The author's own line is theirs to be read, not a caption of ours. */
.spoiler-summary .spoiler-warning { color: var(--mb-ink); overflow-wrap: anywhere; }

/* A post and the conversation under it are one entry in a feed, so the space
   between entries belongs to the pair rather than to the card. */
.activity-entry { margin-bottom: 1rem; }

/* What was said back, on a ground of its own at the foot of the card. An answer
   is not part of the post, and a reader scrolling a feed has to tell the two
   apart without reading either — the change of ground says it before a word is
   read, which a margin never did. Edge to edge, because a band that stopped
   short would read as one more block inside the post. */
.activity-thread {
    display: flex;
    flex-direction: column;
    gap: 0.625rem;
    padding: 0.875rem 1rem;
    background-color: var(--mb-surface-sunken);
}

/* Drawn under every post so that a sent answer has somewhere to land, and taken
   off the page until there is one to put in it. */
.activity-thread.is-empty { display: none; }

/* A conversation is a list and is marked up as one; the numbers it would carry
   say nothing a reader wants. */
.activity-thread-list {
    display: flex;
    flex-direction: column;
    gap: 0.625rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* How many were said, at the head of the band, on the one page that holds them
   all. Set as a caption on the thread rather than as a heading over it: it is
   the band's own first line, and a page heading here would compete with the
   post above it. */
.activity-thread-count {
    margin: 0;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--mb-ink-muted);
}

.activity-thread-partial {
    margin: 0;
    font-size: 0.8125rem;
    color: var(--mb-ink-muted);
}

.activity-thread-more {
    align-self: flex-start;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--mb-ink-muted);
    text-decoration: none;
}

.activity-thread-more:hover,
.activity-thread-more:focus { text-decoration: underline; }

.activity-reply {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
}

.activity-reply-avatar,
.activity-reply-avatar img,
.activity-reply-avatar > .activity-avatar-blank {
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
}

.activity-reply-avatar {
    display: block;
    flex-shrink: 0;
}

.activity-reply-avatar img { object-fit: cover; }

/* What was said and what has been put on it, stacked. Held to the width of the
   words rather than the width of the thread, so the shape below sits under the
   bubble and not off in the space beside it. */
.activity-reply-column {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    min-width: 0;
}

/* The words sit in a shape of their own, which is what says "somebody said
   this" without a label saying it. A shade off the band behind it: two greys
   this close are read as a bubble on a ground, and any more contrast would make
   the thread the loudest thing on the card. */
.activity-reply-body {
    min-width: 0;
    padding: 0.5rem 0.8rem;
    border-radius: 1rem;
    background-color: var(--mb-surface-hover);
    font-size: 0.9375rem;
}

.activity-reply-said {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.4em;
    margin: 0 0 0.1rem;
    font-size: 0.8125rem;
    color: var(--mb-ink-muted);
}

/* Drawn by whichever piece has something before it, the way the handle line
   above does it, so a name with no moment beside it ends on nothing. */
.activity-reply-said > * + *::before {
    content: '·';
    margin-right: 0.4em;
}

.activity-reply-name {
    font-weight: 600;
    color: var(--mb-ink-strong);
    text-decoration: none;
}

.activity-reply-name:hover,
.activity-reply-name:focus { text-decoration: underline; }

.activity-reply-text > :last-child { margin-bottom: 0; }

/* Which server the name above belongs to. Two accounts can hold one name on two
   servers, and a thread that mixes both is where telling them apart matters —
   so it is said, and said quietly, and cut rather than allowed to push the
   moment beside it onto a line of its own. */
.activity-reply-handle {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 14ch;
}

/* An answer to an answer, one column in. Two levels and no more: past that the
   words get a column narrower than the words, and the line below says in words
   what the indent has stopped being able to say. */
.activity-reply.is-nested {
    margin-left: 1.25rem;
    padding-left: 0.75rem;
    border-left: 1px solid var(--mb-border);
}

/* What a flattened answer hangs from, above the words, in the register the line
   above a card uses for the same job. */
.activity-reply-answering {
    margin: 0 0 0.15rem 0.35rem;
    font-size: 0.75rem;
    color: var(--mb-ink-muted);
}

.activity-reply-answering > .bi { margin-right: 0.3em; }

/* What may be put on an answer, under the answer. Tighter than the strip under
   a post: it belongs to the smaller thing and must not compete with it. */
.activity-reply-acts {
    display: flex;
    align-items: center;
    gap: 0.15rem;
    /* Level with the words above, not with the face beside them. */
    margin-left: 0.35rem;
}

.activity-reply-acts form { margin: 0; }

/* The menu is one of the controls in the strip, not a block under them. */
.activity-reply-acts .menu-disclosure { display: flex; }

/* What may be done with a review, under it on a film's or a series' page. A
   review is a post and carries what a post carries: the same band as under a
   card, the three answers taking the room on the left and the menu against the
   far edge.

   Pulled back by a control's own padding so the first glyph lines up with the
   words above it rather than sitting in from them. The row itself is laid out
   with Bootstrap's flex utilities too, so the band stays on one line even
   before this sheet is read. */
.review-acts {
    display: flex;
    align-items: center;
    margin-left: -0.6rem;
}

/* The menu is one of the controls in the band, not a block under them. */
.review-acts .menu-disclosure { display: flex; }

.activity-action-sm {
    padding: 0.25rem 0.45rem;
    font-size: 0.8125rem;
    font-weight: 600;
}

.activity-action-sm > .bi { font-size: 0.9375rem; }

/* The switch behind the reply control. Hidden from the eye and not from the
   keyboard: it is the control, and the label in the strip is only its face. */
.say-toggle { cursor: pointer; }

.activity-say-toggle { cursor: pointer; }

/* The ring the keyboard leaves lands on the face, because the switch itself has
   no size to draw one around. */
.say-toggle:focus-visible ~ .activity-actions .activity-say-toggle,
.say-toggle:focus-visible ~ .activity-reply-acts .activity-say-toggle {
    outline: 2px solid var(--mb-accent);
    outline-offset: 1px;
}

/* The box an answer is written in, folded shut until the control beside it is
   pressed. A sibling of that control and not a child of the thread: a checkbox
   can only show what stands beside it, and a box that opens without a script is
   worth more than a tidier tree. */
.activity-say {
    display: none;
    /* A line of its own under the controls, whatever else shares the band. */
    flex-basis: 100%;
    flex-direction: column;
    gap: 0.5rem;
    margin: 0.25rem 0 0.5rem;
}

.say-toggle:checked ~ .activity-say { display: flex; }

.activity-say-row {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
}

.activity-say-row .form-control {
    border-radius: 0.9rem;
    background-color: var(--mb-surface);
}

/* The send and the way out, held against the far edge the way a form's buttons
   are everywhere else on the site. */
.activity-say-send {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
}

.activity-say-error {
    margin: 0;
    font-size: 0.8125rem;
    color: var(--mb-danger);
}

/* A menu that is a disclosure rather than a scripted dropdown, so it opens with
   scripting off; Bootstrap's menu classes are worn for their look, and these few
   rules are what stand in for the script that usually shows and places them.

   Two of them exist — everything that can be done to a post, and where a member
   stands on a series — and they were one shape drawn twice before this became a
   name of its own. What differs is only the control they hang from and the
   direction they open in, so that is all a caller adds. */
.menu-disclosure { position: relative; }

.menu-disclosure > summary {
    cursor: pointer;
    /* Both spellings: one browser draws the triangle through the list marker,
       the other through a pseudo-element of its own. */
    list-style: none;
}

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

.menu-disclosure[open] > .dropdown-menu {
    display: block;
    position: absolute;
    top: 100%;
    left: 0;
    z-index: 5;
    min-width: max(100%, 12rem);
}

/* Upwards and right-aligned, for a control that is the last thing on a card:
   opening downwards would put the menu over the card below, or off the bottom of
   the screen on the last one. */
.menu-disclosure-up[open] > .dropdown-menu {
    top: auto;
    bottom: 100%;
    left: auto;
    right: 0;
}

/* A submit inside the menu is one of its entries, not a button sitting in it. */
.menu-disclosure .dropdown-item { cursor: pointer; }

.menu-disclosure .dropdown-item .bi { margin-right: 0.35em; }

/* The three dots a post's menu hangs from. */
.menu-toggle-dots {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    color: var(--mb-ink-muted);
}

.menu-toggle-dots:hover,
[open] > .menu-toggle-dots {
    background-color: var(--mb-surface-hover);
    color: var(--mb-ink-strong);
}

/* The same menu under an answer, at the size the strip under an answer is. */
.menu-toggle-dots-sm {
    width: 1.6rem;
    height: 1.6rem;
    font-size: 0.8125rem;
}

/* The clipboard is reachable from script and from nowhere else, so without it
   this entry is never drawn rather than drawn and dead. */
.activity-copy { display: none; }

[data-scripted] .activity-copy { display: block; }

/* The whole moment, on the post's own page and nowhere else. Under the card and
   held against its far corner: it is not one of the things the post says, so it
   is not set among them — it is a fact about the post, sitting where a caption
   sits, and the card is what it captions. The byline still carries the short
   form; a weekday, an hour and a named timezone were never going to fit at the
   end of a sentence. */
.activity-posted {
    margin: 0.375rem 0.25rem 0;
    text-align: right;
    font-size: 0.8125rem;
    color: var(--mb-ink-muted);
}

/* What may be done with the post. Ruled off from it edge to edge, so a strip of
   controls is not read as more of what was written above it. */
.activity-bar {
    display: flex;
    align-items: center;
    /* The box an answer is written in is the fourth thing in this band and is
       as wide as the band; everything else shares one line. */
    flex-wrap: wrap;
    gap: 0.25rem;
    padding: 0.25rem 0.75rem;
    border-top: 1px solid var(--mb-border-faint);
}

/* The strip under a post: like it, answer it, boost it. Each is a glyph and a
   number, because three verbs spelled out under every card would be more
   reading than most of the cards. Wide targets rather than tight ones: this is
   tapped with a thumb far more often than it is clicked.

   It takes the room in the band, which is what pushes the menu to the far edge. */
.activity-actions {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    flex-grow: 1;
}

/* Each control is its own form, so that it still works as one without script. */
.activity-actions form { margin: 0; }

.activity-action {
    display: inline-flex;
    align-items: center;
    gap: 0.4em;
    padding: 0.5rem 0.6rem;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--mb-ink-muted);
    font-size: 0.9375rem;
    line-height: 1;
    text-decoration: none;
}

/* The glyph is what the control is recognised by, so it is the part that gets
   the size; the number beside it is a caption on it. */
.activity-action > .bi { font-size: 1.125rem; }

a.activity-action:hover,
button.activity-action:hover { background-color: var(--mb-surface-hover); }

a.activity-action:hover,
button.activity-action:hover { color: var(--mb-ink-strong); }

/* Nobody signed in: the numbers are worth reading, and nothing here is a
   control, so nothing here behaves like one. */
.activity-action.is-idle { cursor: default; }

/* A count keeps its room whether or not there is one, so that the three
   controls do not shift sideways as a post is liked or boosted. */
.activity-action-count {
    min-width: 1ch;
    font-variant-numeric: tabular-nums;
}

.activity-boost.is-on,
.activity-boost.is-on:hover { color: var(--mb-success); }

.activity-like.is-on,
.activity-like.is-on:hover { color: var(--mb-danger); }

/* The post this one answers, above it on its own page. Quieter and set in, so
   that it reads as where the conversation came from rather than as the post
   that was opened — and joined to it, because two cards with nothing between
   them are two posts. */
.activity-ancestor {
    margin-bottom: 0.75rem;
    padding-left: 0.75rem;
    border-left: 2px solid var(--mb-border);
    opacity: 0.75;
}

.activity-ancestor .activity-card { box-shadow: none; }

/* The poster of an activity is there to be recognised, not read; on a phone the
   words beside it need the width more than it does. */
.activity-poster {
    flex-shrink: 0;
    width: 4.5rem;
}

@media (min-width: 576px) {
    .activity-poster { width: 6rem; }
}

/* A list's first titles, side by side: six share the card's width on a phone,
   and stop growing once each is as wide as a film card's poster there. */
.activity-list-posters {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: 0.5rem;
    max-width: 29.5rem;
    margin: 0;
}

.activity-list-posters a {
    overflow: hidden;
    border-radius: 0.4rem;
    box-shadow: var(--mb-shadow-sm);
    background-color: var(--mb-surface-hover);
}

/* Rounded to the same hand as the card, and lifted just enough to read as a
   thing sitting on the card rather than a hole cut into it. */
.activity-poster img,
.activity-poster .ratio > a > div {
    border-radius: 0.6rem;
    box-shadow: var(--mb-shadow-sm);
}

/* A list read in order: one row per work, its place, its poster, what it is,
   what its editors said about it, and — for them — the ways to move it. */
.list-entries {
    container-type: inline-size;
    margin: 0;
    padding: 0;
    list-style: none;
}

.list-entry {
    display: grid;
    grid-template-columns: 1.75rem 3rem minmax(0, 1fr) auto;
    grid-template-areas: "rank poster what acts";
    align-items: start;
    column-gap: 0.75rem;
    padding: 0.75rem 0;
    border-bottom: 1px solid var(--mb-border);
}

/* Anchored to after an edit, so the row that moved is found again. */
.list-entry:target {
    background-color: var(--mb-accent-tint);
}

.list-entry-rank {
    grid-area: rank;
    padding-top: 0.2rem;
    color: var(--mb-ink-muted);
    font-variant-numeric: tabular-nums;
    text-align: right;
}

.list-entry-poster {
    grid-area: poster;
    display: block;
    width: 3rem;
    aspect-ratio: 2 / 3;
    border-radius: var(--mb-radius-sm);
    background-color: var(--mb-surface-sunken);
    object-fit: cover;
}

.list-entry-what {
    grid-area: what;
    min-width: 0;
}

.list-entry-title {
    overflow-wrap: anywhere;
}

.list-entry-note {
    margin: 0.35rem 0 0;
    white-space: pre-line;
}

/* What a peer's author folded in a list's note or description, shown on asking.
   The line is a control and looks like one. */
details.spoiler > summary {
    width: fit-content;
    color: var(--mb-link);
    cursor: pointer;
}
details.spoiler > summary:focus-visible {
    outline: 2px solid var(--mb-accent);
    outline-offset: 2px;
}

.list-entry-acts {
    grid-area: acts;
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

.list-entry-acts form {
    margin: 0;
}

/* On a phone the controls go under what the row says rather than squeezing it. */
@container (max-width: 30rem) {
    .list-entry {
        grid-template-areas:
            "rank poster what"
            ". . acts";
        grid-template-columns: 1.75rem 3rem minmax(0, 1fr);
        row-gap: 0.5rem;
    }
}

/* The small capitals over a section of a page — "Reviews", "Details", "Your
   log". One look for every such heading, film or series, page or sidebar: four
   copies of it had drifted into four stylesheets. */
.section-label {
    padding-bottom: 0.4rem;
    border-bottom: 1px solid var(--mb-border-faint);
    color: var(--mb-ink-muted);
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

/* A number or a word set beside a label on a quiet ground of its own: how many
   are on a shelf, which kind of title a result is. Never a state — those have
   .state-badge and their own colours. */
.quiet-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.25em;
    min-width: 1.6em;
    padding: 0.15em 0.5em;
    border-radius: var(--mb-radius-pill);
    background-color: var(--mb-surface-sunken);
    color: var(--mb-ink-muted);
    font-size: 0.75em;
    font-weight: 600;
    line-height: 1.4;
    font-variant-numeric: tabular-nums;
    justify-content: center;
    white-space: nowrap;
}

/* What stands where a poster or a still has not arrived, or never will: a quiet
   ground in the poster's own shape, so a page still loading reads as a page
   being filled rather than as a page of blank frames. */
.media-placeholder {
    border-radius: var(--mb-radius);
    background-color: var(--mb-surface-hover);
    color: var(--mb-ink-faint);
}

.poster-card,
.activity-poster .ratio,
.search-result-poster {
    background-color: var(--mb-surface-hover);
}

.activity-poster .ratio { border-radius: 0.6rem; }

.search-result-poster { border-radius: var(--mb-radius); }

/* What a poster on a grid says beyond its title, where a grid has more to say:
   the reader's standing over its corner, the part somebody played under it. */
.poster-card-shelf .state-badge { box-shadow: var(--mb-shadow-sm); }

/* Three posters to a phone's row leave no room for the word beside the kind's
   mark in the other corner: the glyph stays, and still says it to a reader. */
@media (max-width: 767.98px) {
    .poster-card-shelf .state-badge-label {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
    }
}

.poster-card-caption {
    margin: 0.3rem 0 0;
    color: var(--mb-ink-muted);
    font-size: 0.8125rem;
    line-height: 1.25;
}

/* A strip of tabs read as a row of words with the current one underlined, not
   as folder tabs. On a phone it scrolls sideways under the thumb rather than
   breaking onto a second row, where the lit tab could land on either line.

   The rule under the strip is a shadow and not a border: the strip scrolls,
   and a scroller clips whatever reaches past its padding, which is where an
   underline overlapping a border would have to sit. */
.app-tabs {
    flex-wrap: nowrap;
    gap: 0.25rem;
    overflow-x: auto;
    overflow-y: hidden;
    box-shadow: inset 0 -1px 0 var(--mb-border);
    scrollbar-width: none;
}

.app-tabs::-webkit-scrollbar { display: none; }

.app-tabs .nav-link {
    padding: 0.625rem 0.75rem;
    border-bottom: 2px solid transparent;
    color: var(--mb-ink-muted);
    font-weight: 600;
    white-space: nowrap;
}

.app-tabs .nav-link:hover {
    border-bottom-color: var(--mb-border);
    color: var(--mb-ink-strong);
}

.app-tabs .nav-link.active {
    border-bottom-color: var(--mb-accent);
    color: var(--mb-ink-strong);
}

.app-tabs .nav-link.active .quiet-badge {
    background-color: var(--mb-accent-tint);
    color: var(--mb-accent-strong);
}

/* Drawn inside the link: the strip clips anything outside it. */
.app-tabs .nav-link:focus-visible {
    outline: 2px solid var(--mb-accent);
    outline-offset: -2px;
    border-radius: var(--mb-radius-sm);
    box-shadow: none;
}

/* A flex child is as wide as its content by default, so a long unbroken word in
   a review would push the card past the edge of the screen instead of wrapping. */
.min-w-0 { min-width: 0; }

/* How tall the fixed bar is, said once. Three things need the number and used to
   guess it separately: the room the page leaves for the bar, where a toast may
   start, and how far an anchor must stop short so its target is not under it.

   The tab bar along the bottom is the same arrangement upside down, and the
   inset it clears is the home indicator rather than the notch. */
:root {
    --app-navbar-height: 3.5rem;
    --app-navbar-clearance: calc(var(--app-navbar-height) + env(safe-area-inset-top, 0px));
    --app-tabbar-height: 3.25rem;
    --app-tabbar-clearance: calc(var(--app-tabbar-height) + env(safe-area-inset-bottom, 0px));
}

/* The navigation bar is fixed, and on a notched screen it sits under the status bar.
   The page is also a column at least a screen tall, so the footer's own `mt-auto` has
   slack to take up: a page with little on it ends against the bottom edge of the
   window instead of floating halfway up it. */
.app-body {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    /* Follows the browser chrome as it hides; vh above is the fallback. */
    min-height: 100dvh;
    padding-top: var(--app-navbar-clearance);
    background-color: var(--mb-bg);
}

/* The chrome is a surface of its own, ruled off from the page it frames, so
   cards on the page and the bar above them are never the same white. */
.app-navbar {
    border-bottom: 1px solid var(--mb-border-faint);
    background-color: var(--mb-surface);
}

.app-navbar .navbar-brand {
    font-weight: var(--mb-heading-weight);
    letter-spacing: var(--mb-heading-tracking);
}

/* The mark has no background of its own, so each theme gets the one drawn for
   its bar: amber reads on the dark one, night on the light one. */
.app-navbar-mark-dark,
[data-bs-theme=dark] .app-navbar-mark-light { display: none; }
[data-bs-theme=dark] .app-navbar-mark-dark { display: block; }

.app-footer {
    border-top: 1px solid var(--mb-border-faint);
    background-color: var(--mb-surface);
    color: var(--mb-ink-muted);
    font-size: 0.875rem;
}

/* The room above is exactly the height of the bar, which leaves the first thing
   on a page starting on the line where the bar ends. The gap that separates
   chrome from content is said once, here, rather than by whichever page
   remembered to open with a margin — which is why some pages had one and most
   did not. Below is the same account, so a short page is not lopsided. */
.app-main {
    padding-top: 1.5rem;
    padding-bottom: 3rem;
}

/* A fixed bar hides whatever an in-page link jumps to, so anything scrolled to
   by its address stops short of the bar instead of landing under it. */
html { scroll-padding-top: calc(var(--app-navbar-clearance) + 1rem); }

/* Toasts sit above the bar in the stacking order, so without this the first one
   covers the account menu for as long as it is shown. */
.app-toasts { top: var(--app-navbar-clearance); }

/* The one control in the bar that may take whatever room the others leave, down
   to the width of a couple of words on the narrowest phone.

   The basis is zero rather than the field's own width: a flex line wraps before
   it shrinks, so a field asking for its content width first took the login and
   register buttons onto a second line, and the bar grew to 86px against the
   56px the page reserves for it — which put the top of every guest page under
   the bar.

   The floor is what the field keeps once the room runs out, and it only ever
   binds on a 320px screen reading French, where "Connexion" and "Inscription"
   together are 50px wider than the English pair. Anywhere above that the field
   takes the remainder and the floor never comes up. */
.app-navbar-search {
    flex: 1 1 0;
    min-width: 5.25rem;
    max-width: 22rem;
}

/* Said outright, so that a longer word in another language cannot put the bar
   on two lines again. Above lg the bar has room to spare and Bootstrap's own
   nowrap already applies. */
@media (max-width: 991.98px) {
    .app-navbar > .container { flex-wrap: nowrap; }
}

/* Bootstrap's glow would ring the field and leave out the icon joined to it, so
   it goes, and the ring is drawn around the pair instead. It has to be drawn:
   without it, the only sign a keyboard had reached the field was its border
   turning a pale blue, well under the 3:1 a focus indicator needs (WCAG
   1.4.11). The primary colour on the bar clears it in both themes. */
.app-navbar-search .form-control:focus,
.app-navbar-search .form-control:focus + .input-group-text {
    box-shadow: none;
}

/* What the field suggests hangs under it, over the page, as wide as the form.
   Shown only while the form holds the focus — the field, or one of the
   suggestions a keyboard moved to — and only when there is something in it. */
.app-navbar-search {
    position: relative;
}

.app-search-suggestions {
    position: absolute;
    top: calc(100% + 0.25rem);
    left: 0;
    right: 0;
    z-index: 1050;
    background-color: var(--mb-surface);
    border: 1px solid var(--mb-border);
    border-radius: var(--mb-radius);
    box-shadow: var(--mb-shadow);
    overflow: hidden;
}

/* Safari never gives the focus to a link it is tapped or clicked on: the field
   loses it first, and a list hidden on that alone is gone before the tap lands.
   Under the finger or the pointer, it stays. */
.app-search-suggestions:empty,
.app-navbar-search:not(:focus-within) .app-search-suggestions:not(:hover) {
    display: none;
}

.app-suggestion {
    display: flex;
    gap: 0.5rem;
    align-items: baseline;
    padding: 0.4rem 0.75rem;
    color: var(--mb-ink);
    text-decoration: none;
}

.app-suggestion:hover,
.app-suggestion:focus-visible {
    background-color: var(--mb-surface-sunken);
}

.app-suggestion:focus-visible {
    outline: 2px solid var(--mb-accent);
    outline-offset: -2px;
}

.app-suggestion-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.app-suggestion-kind {
    margin-left: auto;
    font-size: 0.8em;
    white-space: nowrap;
}

.app-suggestion-all {
    border-top: 1px solid var(--mb-border);
    font-size: 0.9em;
}

.app-navbar-search .input-group:focus-within {
    outline: 2px solid var(--mb-accent);
    outline-offset: 1px;
    border-radius: var(--mb-radius-sm);
}

.app-avatar { object-fit: cover; }

/* The icon standing in for an account with no picture, at the picture's size. */
.app-avatar-blank-lg { font-size: 4rem; line-height: 1; }
.app-avatar-blank-md { font-size: 3rem; line-height: 1; }

.app-narrow { max-width: 42rem; }

.text-pre-line { white-space: pre-line; }

/* Hidden until a keyboard reaches it, and then over everything, including the
   fixed bar it is offering to skip. */
.app-skip-link {
    position: absolute;
    z-index: 1100;
    margin: 0.5rem;
    padding: 0.5rem 0.75rem;
    border-radius: var(--mb-radius);
    background-color: var(--mb-surface);
}

/* The trail is chrome, not content: it belongs to the bar above it rather than
   to the page below, and is read at the size of a caption. Stacked over the
   page, because a Bootstrap row's vertical gutter is a negative top margin: the
   first row of a detail page reached up over the lower half of the trail and
   took the clicks meant for it. */
.app-breadcrumb {
    position: relative;
    z-index: 1;
    border-bottom: 1px solid var(--mb-border);
    background-color: var(--mb-surface);
    font-size: 0.8125rem;
}

.app-breadcrumb .breadcrumb {
    --bs-breadcrumb-divider: '›';
    --bs-breadcrumb-item-padding-x: 0.4rem;
}

/* Loud enough to be read as a step, quiet enough not to be read as a word. */
.app-breadcrumb .breadcrumb-item + .breadcrumb-item::before {
    color: var(--mb-ink-muted);
    opacity: 0.7;
}

/* Underlined by every other rule on the site, four of them in a row read as a
   sentence somebody struck through rather than as a path. The colour still says
   they are links, and the underline comes back the moment one is aimed at. */
.app-breadcrumb .breadcrumb-item a { text-decoration: none; }

.app-breadcrumb .breadcrumb-item a:hover,
.app-breadcrumb .breadcrumb-item a:focus-visible { text-decoration: underline; }

/* A step is a word, and a link around a word is exactly as tall as its letters:
   at this size that is a band fifteen pixels high to aim a mouse at, with the
   room the bar kept above and below it taking every click that landed a hair
   off the glyphs. So the bar keeps no padding of its own and hands it to the
   steps, each of which is a box the full height of the trail — the words sit on
   the same line as before and the trail is the same height as before, but all
   of it belongs to the step it is drawn under.

   A step is a flex row rather than a block because the '\2039' before it is a
   float, and a float aligns to the top of the box rather than to the line it is
   read on: against a step this tall the dividers would ride above the words. As
   a flex item the divider sits on the line with them. */
.app-breadcrumb .breadcrumb-item {
    display: flex;
    align-items: center;
    max-width: 22rem;
}

/* A title long enough to fill the line is cut rather than allowed to push the
   step that names this page out of sight. The cut is made on what the step
   holds and no longer on the step itself: `text-overflow` names text, and the
   text of a flex row is not a box it can reach. Which is why the one step that
   is not a link — the name of this page — is still wrapped in an element. */
.app-breadcrumb .breadcrumb-item > a,
.app-breadcrumb .breadcrumb-item > span {
    padding: 0.5rem 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Everything above a shelf that says how it is being read: which of its two
   views, and — under the posters — what it is narrowed to and ordered by. */
.shelf-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 1rem;
    margin-bottom: 1rem;
}

/* The two views sit apart from the settings that only apply to one of them. */
.shelf-bar-views { flex: 0 0 auto; }
/* Three words that make one choice: wrapping them would break the control in two. */
.shelf-controls-kinds { flex: 0 0 auto; }

/* How a shelf is read, above the shelf. One row of controls that wraps onto a
   second rather than shrinking past what a thumb can hit, and that never grows
   into the width of a desktop: they are a setting, not the content. */
.shelf-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin: 0;
}

/* A label and the field it names wrap as one: on a phone the row used to break
   between them, leaving "Genre" at the end of one line and its menu at the
   start of the next. */
.shelf-controls-pair {
    display: flex;
    flex: 1 1 auto;
    align-items: center;
    gap: 0.5rem;
    max-width: 100%;
}

.shelf-controls-label {
    flex-shrink: 0;
    margin: 0;
    color: var(--mb-ink-muted);
    font-size: 0.8125rem;
}

.shelf-controls-field {
    flex: 1 1 9rem;
    width: auto;
    min-width: 9rem;
    max-width: 14rem;
}

/* Scripting sends the choice as it is made, so the button that would have sent
   it has nothing left to do. Hidden on the attribute the document sets before
   anything is drawn, so it is never shown and then taken away. */
[data-scripted] .shelf-controls-apply { display: none; }

/* Now that the footer really reaches the bottom edge, it has to clear the home
   indicator of a phone running the installed app. Below lg the tab bar stands
   between the two and clears it instead — see the rule that hands the room back. */
.app-body > footer { padding-bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px)); }

/* The destinations, on a screen too narrow to keep them in the bar at the top.
   Fixed to the foot rather than collapsed behind a control, because a menu that
   has to be opened to say what it holds is a menu nobody opens twice. */
.app-tabbar {
    position: fixed;
    right: 0;
    bottom: 0;
    left: 0;
    /* Level with the bar at the top: both are chrome, and neither may cover a
       toast or the offer to install. */
    z-index: 1030;
    border-top: 1px solid var(--mb-border-faint);
    background-color: var(--mb-surface);
    /* The bar's own colour reaches the bottom edge; its contents stop above the
       home indicator rather than sitting under it. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
}

.app-tabbar-list {
    display: flex;
    justify-content: center;
    height: var(--app-tabbar-height);
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Each entry takes an equal share, up to a width past which a bar holding one
   or two of them reads as a banner rather than as a row of tabs. */
.app-tabbar-item {
    flex: 1 1 0;
    max-width: 12rem;
}

.app-tabbar-link {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.1875rem;
    height: 100%;
    padding: 0.25rem 0.5rem;
    color: var(--mb-ink-muted);
    text-decoration: none;
}

.app-tabbar-link:hover { color: var(--mb-ink-strong); }

.app-tabbar-link.active { color: var(--mb-accent); }

/* Inside the bar, so the ring is not clipped by the screen edge the bar sits on. */
.app-tabbar-link:focus-visible {
    outline: 2px solid var(--mb-ink);
    outline-offset: -2px;
    border-radius: var(--mb-radius);
}

/* Which entry is being read, said in the shape the app is drawn from: the pair
   of sprocket holes the icon is punched along, at the ratio the icon punches
   them. Not in the gold of the mark — that belongs to the grade, and a bar that
   borrowed it would make "where you are" look like a rating. */
.app-tabbar-mark {
    display: flex;
    gap: 0.25rem;
    /* Hidden rather than absent: the row keeps the height either way, so a tab
       does not jump as it becomes the current one. */
    visibility: hidden;
}

.app-tabbar-mark::before,
.app-tabbar-mark::after {
    content: "";
    width: 0.3125rem;
    height: 0.3125rem;
    border-radius: 0.104rem;
    background-color: currentColor;
}

.app-tabbar-link.active .app-tabbar-mark { visibility: visible; }

.app-tabbar-icon {
    font-size: 1.125rem;
    line-height: 1;
}

/* The icon and whatever is waiting behind it, as one piece: the count is pinned
   to the icon's corner, so it cannot widen the tab or move the label under it.
   Every tab carries the wrapper whether or not it has a count, or the tabs with
   one would sit a hair lower than the rest. */
.app-tabbar-glyph {
    position: relative;
    display: flex;
    line-height: 1;
}

.app-tabbar-badge {
    position: absolute;
    left: 62%;
    bottom: 58%;
    min-width: 1rem;
    padding: 0 0.25rem;
    border-radius: 1rem;
    background-color: var(--mb-danger);
    color: var(--mb-danger-contrast);
    font-size: 0.625rem;
    font-weight: 600;
    line-height: 1rem;
    text-align: center;
}

/* The same count in the wide bar, where it follows the word instead of riding
   on the icon: there is room for it there, and a number pinned to a glyph in a
   row of words would read as part of the next one. */
.app-nav-badge {
    display: inline-block;
    min-width: 1.125rem;
    margin-left: 0.35rem;
    padding: 0 0.3rem;
    border-radius: 1rem;
    background-color: var(--mb-danger);
    color: var(--mb-danger-contrast);
    font-size: 0.6875rem;
    font-weight: 600;
    line-height: 1.125rem;
    text-align: center;
    vertical-align: 1px;
}

/* Something waits on the member behind the account menu: a dot on the face,
   with the count kept for the menu it opens. */
.app-account-dot {
    position: absolute;
    top: 0;
    right: 0;
    width: 0.625rem;
    height: 0.625rem;
    border: 2px solid var(--mb-surface);
    border-radius: 50%;
    background-color: var(--mb-danger);
}

.app-tabbar-label {
    font-size: 0.6875rem;
    line-height: 1;
}

@media (max-width: 991.98px) {
    /* The room the tab bar stands in, taken out of the page the way the bar at
       the top takes its own — so the licence line at the foot ends above it
       instead of behind it. */
    .app-body { padding-bottom: var(--app-tabbar-clearance); }

    /* Handed back: the clearance above already counts the home indicator, and
       the footer counting it a second time left a band of empty page under the
       last line of text. */
    .app-body > footer { padding-bottom: 1.5rem; }

    /* An anchor jumped to from its address stops short of the tab bar, the same
       way `scroll-padding-top` keeps it clear of the bar at the top. */
    html { scroll-padding-bottom: calc(var(--app-tabbar-clearance) + 1rem); }
}

/* A person found by a search is a face, not a poster. */
.search-result-portrait {
    width: 3.5rem;
    height: 3.5rem;
    overflow: hidden;
    border-radius: 50%;
    background-color: var(--mb-surface-hover);
    color: var(--mb-ink-faint);
    font-size: 1.5rem;
}

/* The other names of a movie explain why it turned up in the results; nobody is
   reading the line for them. Smaller and paler than the rest, so the eye lands on the
   title and the director and only then wanders. */
.search-result-aside {
    font-size: 0.8125rem;
    line-height: 1.35;
    color: var(--mb-ink-faint);
}

/* A search result is read by its title; the poster is there to be recognised at a
   glance, not to take the width a phone needs for the words next to it. */
.search-result-poster {
    width: 3.5rem;
    aspect-ratio: 2 / 3;
    /* A flex child stretches to the height of its row by default, and a stretched
       height beats the aspect ratio: a result with many alternative titles would
       hand its poster the shape of the paragraph next to it. */
    align-self: flex-start;
}

@media (min-width: 576px) {
    .search-result-poster { width: 5rem; }
}

/* In a list the follow button keeps to its own column; left to itself the note
   under a pending request would stretch the panel across the whole row. */
.follow-row .follow-panel { max-width: 24rem; justify-content: flex-end; }

/* Lists are read by scrolling them, so every list on the site ends the same
   way: with the marker below, which is what asks for the rest. */
.page-sentinel {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    padding: 1rem 0;
}

/* Asking starts a screenful before the marker is reached, so what comes next is
   usually already there when the reader arrives. This is what watches for it:
   it takes no room, it lies over the last stretch of the list, and it lets
   everything through to what it lies over. */
.page-sentinel-lead {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    height: 20rem;
    pointer-events: none;
}

/* Exactly one of the three words is shown, according to what the marker is
   doing: waiting to be reached, fetching, or unable to. Never none of them —
   what is written inside the link is the whole of its name. */
.page-sentinel-spinner,
.page-sentinel-waiting,
.page-sentinel-again { display: none; }
.page-sentinel.htmx-request .page-sentinel-label { display: none; }
.page-sentinel.htmx-request .page-sentinel-spinner { display: inline-block; }
.page-sentinel.htmx-request .page-sentinel-waiting { display: inline; }
.page-sentinel[data-page-error] .page-sentinel-label,
.page-sentinel[data-page-error] .page-sentinel-waiting { display: none; }
.page-sentinel[data-page-error] .page-sentinel-again { display: inline; }

/* A grid gives every one of its children the width of a cell, and this is not a
   cell: it closes the row. Bootstrap's .row-cols-* rules are declared after
   .col-12, so the class alone would size the marker like a poster. */
.row > .page-sentinel { width: 100%; }

/* Written into by the script when a request fails, and empty the rest of the
   time — which is when it must take up no room. */
.page-sentinel-error {
    margin: 0;
    font-size: 0.875rem;
    color: var(--mb-danger-ink);
}

.page-sentinel-error:empty { display: none; }

/* An entry nobody has scrolled to is laid out and painted all the same, and
   down a long enough list that is most of what a scroll costs. Saying it may be
   skipped lets the browser do neither until the entry is about to be read, and
   the reserved size keeps the scrollbar meaning roughly what it says until then
   — after which the browser remembers the real one.

   Reserved for entries that draw nothing outside their own edges. Asking to
   skip an entry contains it, and a contained entry clips whatever crosses its
   boundary — a card's shadow, and, worse, the ring a browser draws around a
   button sitting flush against that edge to say the keyboard is on it. So a row
   whose control reaches its edge keeps every entry it holds, and is kept in
   hand by the count instead. */
.page-entry-deferred {
    content-visibility: auto;
    contain-intrinsic-size: auto 10rem;
}

/* An entry put away while it is far off screen keeps the height it had, so the
   list around it does not move. */
[data-page-aside] { overflow: hidden; }

/* The grade a title stands at, read three ways on one line: members here, other
   servers, then both counted together. Equal columns, because the point of the
   line is the comparison and unequal ones would weigh the numbers by their
   width rather than by their graders.

   Three rows the columns share, so a label wrapping to two lines carries no
   figure down with it and the numbers stay on one line across the strip. Where
   subgrid is not understood each column simply stacks its own three, which is
   the old behaviour and not a broken one. */
.rating-scopes {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: auto auto auto;
    column-gap: 0.5rem;
    margin: 0;
}

.rating-scope {
    display: grid;
    grid-row: span 3;
    grid-template-rows: subgrid;
    row-gap: 0.15rem;
    min-width: 0;
}

/* A rule between the columns rather than around them: it separates the three
   readings without boxing each one into a card of its own. */
.rating-scope + .rating-scope {
    padding-left: 0.5rem;
    border-left: 1px solid var(--mb-border-faint);
}

/* The label names a population and the figure under it is what the eye is
   after, so it stays quiet: small, unbolded, and balanced across its lines
   rather than leaving one word alone on the second. */
.rating-scope-label {
    /* Sat at the bottom of the row it shares, so a label that fits on one line
       and one that wraps to two both end just above their figure. */
    align-self: end;
    font-size: 0.7rem;
    font-weight: 400;
    line-height: 1.2;
    color: var(--mb-ink-muted);
    text-wrap: balance;
}

.rating-scope-figure {
    display: flex;
    align-items: baseline;
    gap: 0.1em;
    margin: 0;
    line-height: 1.1;
}

.rating-scope-value {
    font-size: 1.25rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    /* Three figures read against each other, so their digits line up. */
    font-variant-numeric: tabular-nums;
}

/* Nothing to say rather than a bad grade: the dash keeps the row's height and
   the column's place, and carries none of a figure's weight. */
.rating-scope-value--none {
    font-weight: 400;
    color: var(--mb-ink-faint);
}

/* Said three times over on one line, so it recedes: the scale is context, not
   part of the number. */
.rating-scope-scale {
    font-size: 0.7rem;
    color: var(--mb-ink-faint);
}

/* How many people stand behind the figure: a person and a number, because
   three columns in the width of a card have no room for the words and a number
   is the one thing that stays inside its column in every language. The words
   are still said, to a screen reader. */
.rating-scope-graders {
    display: flex;
    align-items: center;
    gap: 0.25em;
    margin: 0;
    font-size: 0.75rem;
    line-height: 1.2;
    color: var(--mb-ink-faint);
    font-variant-numeric: tabular-nums;
}

.rating-scope-graders .bi {
    font-size: 0.9em;
}

/* The external rating strip, drawn on a movie or show page: each source's mark
   beside its score, the way the sites themselves show them. Shared, so a film
   and a series read the same. The scores come from moviebuff.ratings; the strip
   fills itself in through HTMX and never holds up the page. */
.rating-badges {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
}

/* Only a strip that actually carries a badge draws its separator, so an empty
   one — a season, or a title no source knew — leaves no rule hanging. */
.rating-badges:has(.rating-badge) {
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--mb-border);
}

.rating-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.35em;
    padding: 0.2em 0.55em 0.2em 0.4em;
    border: 1px solid var(--mb-border-faint);
    border-radius: var(--mb-radius-pill);
    background-color: var(--mb-surface-sunken);
    font-size: 0.9rem;
    font-weight: 600;
    line-height: 1.4;
    /* Icon and score are one reading of one source; a wrap between them would
       read as two things. */
    white-space: nowrap;
}

/* A badge that knows its source page is a link; it must not read as body-text
   underlined blue, only invite the click a pointer and a lifting border promise. */
a.rating-badge {
    color: inherit;
    text-decoration: none;
}

a.rating-badge:hover,
a.rating-badge:focus-visible {
    border-color: var(--mb-ink-muted);
    background-color: var(--mb-surface-hover);
}

/* A source's mark, shared by the rating strip and the "More at" row. */
.source-icon {
    height: 1.25em;
    width: auto;
    flex-shrink: 0;
}

/* A wordmark (IMDb, TMDB, TVDB) is wider than tall; sitting it a touch shorter
   lines its cap height up with the pictorial marks beside it. */
.source-icon--wide { height: 1.05em; }

.rating-badge-score { font-variant-numeric: tabular-nums; }

/* The "More at" row: a label and a line of source marks that link out. It sits
   under the meta line on a film or show page, small and quiet. */
.more-at {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem;
    margin-top: 0.75rem;
}

.more-at-link {
    display: inline-flex;
    align-items: center;
    line-height: 1;
    opacity: 0.85;
}

.more-at-link:hover,
.more-at-link:focus-visible { opacity: 1; }

.more-at-link .source-icon { height: 1.4em; }

/* Keep the wordmarks' shorter height in the row too: the descendant selector
   above would otherwise out-rank .source-icon--wide and stretch them tall. */
.more-at-link .source-icon--wide { height: 1.18em; }

/* "Where to watch", the row above "More at": the reader's own server, then the
   services' square logos, at the size a thumb can hit on a phone. */
.where-to-watch {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.75rem;
}

.where-to-watch-link {
    display: inline-flex;
    align-items: center;
    line-height: 1;
    text-decoration: none;
}

.where-to-watch-link .source-icon { height: 1.75rem; }

.where-to-watch-logo {
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 0.4rem;
    box-shadow: 0 0 0 1px var(--mb-border);
}

/* A row refreshed before logos were kept has only the name to show. */
.where-to-watch-name {
    padding: 0.2rem 0.5rem;
    border: 1px solid var(--mb-border);
    border-radius: 0.4rem;
    color: var(--mb-ink);
}

.where-to-watch-credit { font-size: 0.75rem; }

/* The facts panel: what a title is, as a flat record. Label on the left, what
   it says on the right, a hairline between rows. It is a description list drawn
   as two columns, because that is what it is. */
.media-facts-list {
    font-size: 0.875rem;
}

.media-facts-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--mb-border);
}

/* The card's own edge closes the list; a second line there would read as a
   row with nothing in it. */
.media-facts-row:last-child {
    padding-bottom: 0;
    border-bottom: 0;
}

.media-facts-label {
    font-weight: 600;
    flex-shrink: 0;
}

/* Five studios or three release dates read as a column, not as a paragraph:
   one per line, aligned on the edge they share with the label opposite. */
/* Pushed to the far edge even once a long label has sent them to a line of
   their own, and free to break inside a name: a column only so wide holds
   "United States of America" or it spills past the card. */
.media-facts-values {
    min-width: 0;
    margin-left: auto;
    overflow-wrap: anywhere;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.15rem;
    margin: 0;
    text-align: right;
    color: var(--mb-ink-muted);
}

.media-facts-value {
    display: inline-flex;
    align-items: baseline;
    gap: 0.4rem;
}

.media-facts-icon {
    color: var(--mb-ink-faint);
}

/* An emoji flag is drawn taller than the words it sits with and would push the
   line apart; hold it to the size of the text beside it. */
.media-facts-flag {
    font-size: 0.95em;
    line-height: 1;
}

/* The history a film page and a series page both draw — one component, so one
   rule, in the stylesheet both of them already load. It lived twice, once under
   each app's own class name, saying exactly the same thing. */
.log-history-toggle {
    cursor: pointer;
    color: var(--mb-ink-muted);
}

.log-history-toggle:hover,
.log-history[open] > .log-history-toggle {
    color: var(--mb-ink);
}

.log-history-row + .log-history-row {
    border-top: 1px solid var(--mb-border);
}

/* A gesture the member has taken back, in the one place that still shows it.
   Quiet on purpose: the history is read for what happened, and a line saying
   something stopped being true should not be louder than the lines that are. */
.log-history-undone {
    display: inline-flex;
    align-items: center;
    gap: 0.25em;
    font-size: 0.8125rem;
    color: var(--mb-ink-muted);
}

/* Where a member stands, and whether they mean to watch it: the two axes as two
   chips on one line, at opposite ends of it, because they are independent and a
   column of buttons says the opposite. They wrap rather than shrink on a narrow
   card — a chip that has lost its word is a chip that has lost its point. */
.standing {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.standing form {
    display: flex;
}

/* Whatever draws the second axis goes to the far end — a form when it comes off
   in one press, a link when it asks about its post first. */
.standing > :last-child {
    margin-left: auto;
}

/* A chip is the badge with a hand on it. The colour, the glyph and the wording
   stay the badge's, so a standing reads here exactly as it does on a card in a
   feed; this adds the room a finger needs and the answer to a hover, and nothing
   else. No second fill behind a shape that already has one. */
.standing-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.3rem 0.15rem;
    border: 0;
    background: none;
    color: var(--mb-ink-muted);
    line-height: 1;
}

button.standing-chip,
summary.standing-chip {
    cursor: pointer;
}

.standing-chip .state-badge {
    font-size: 0.8125rem;
    padding: 0.3em 0.6em;
    transition: border-color 0.12s ease-in-out;
}

button.standing-chip:hover .state-badge,
[open] > .standing-chip .state-badge {
    border-color: var(--state-icon);
}

.standing-chip:focus-visible {
    outline: 2px solid var(--mb-accent);
    outline-offset: 2px;
    border-radius: var(--mb-radius-sm);
}

.standing-caret {
    font-size: 0.65rem;
}

[open] > .standing-chip .standing-caret {
    transform: rotate(180deg);
}

/* Who among the people a reader follows has touched this title. A sidebar card
   like the facts panel beside it, so it borrows that panel's quiet rules rather
   than inventing a second look for the same slot. */
.circle-list {
    container-type: inline-size;
}

.circle-row {
    display: grid;
    grid-template-columns: 2rem minmax(0, 1fr) auto;
    grid-template-areas: "face who said";
    align-items: center;
    column-gap: 0.6rem;
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--mb-border);
    font-size: 0.875rem;
}

/* The card's own edge closes the list, as it does on the facts panel. */
.circle-row:last-child {
    padding-bottom: 0;
    border-bottom: 0;
}

.circle-avatar {
    grid-area: face;
    width: 2rem;
    height: 2rem;
    object-fit: cover;
}

.circle-avatar-none {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background-color: var(--mb-surface-hover);
    color: var(--mb-ink-muted);
}

/* A name and a handle hold one line each and end in an ellipsis, with the whole
   of it on hover: broken mid-word over three lines, a handle reads as nothing at
   all, and pushes the badge the row is read for into a column of its own. */
.circle-who {
    grid-area: who;
    min-width: 0;
}

.circle-name {
    display: flex;
    align-items: baseline;
    gap: 0.3rem;
    font-weight: 600;
}

.circle-name a,
.circle-name-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.circle-name a {
    color: inherit;
    text-decoration: none;
}

.circle-name a:hover {
    text-decoration: underline;
}

/* The arrow says the follow goes both ways, which is why this row is above the
   others. Kept to the weight of punctuation: it explains an order, it is not a
   thing about the person. */
.circle-mutual {
    flex-shrink: 0;
    color: var(--mb-ink-faint);
    font-size: 0.8em;
}

.circle-handle,
.circle-reached {
    display: block;
    font-size: 0.8125rem;
    color: var(--mb-ink-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.circle-said {
    grid-area: said;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.2rem;
}

/* In a sidebar or on a phone the badge goes under the name rather than beside
   it, so the name keeps the width of the card. */
@container (max-width: 24rem) {
    .circle-row {
        grid-template-columns: 2rem minmax(0, 1fr);
        grid-template-areas:
            "face who"
            ". said";
    }

    .circle-avatar {
        align-self: start;
        margin-top: 0.1rem;
    }

    .circle-said {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: 0.5rem;
        margin-top: 0.35rem;
    }
}

.circle-average {
    margin-top: 0.75rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--mb-border);
    font-size: 0.875rem;
}

.circle-average-value {
    font-weight: 600;
}

.circle-average-scale,
.circle-average-graders {
    color: var(--mb-ink-muted);
}

.circle-average-graders {
    margin-left: 0.35rem;
}

/* On a review, why it leads the ones under it. Punctuation weight, like the
   arrow in the block: it explains an order, it is not a fact about the person. */
.review-followed {
    color: var(--mb-ink-faint);
}

/* A name in a toast leads to what it names: underlined in the toast's own ink,
   since the link colour is not made for a coloured ground. */
.app-toasts .toast-body a {
    color: inherit;
    font-weight: 600;
    text-decoration: underline;
    text-underline-offset: 0.15em;
}

/* Whether the reader's own posts are in the feed of accounts they follow: a
   switch under the strip of feeds, at its end, where it qualifies the feed
   being read rather than posing as another one. */
.feed-own-posts {
    display: flex;
    justify-content: flex-end;
    margin-bottom: 1rem;
}

.feed-switch {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.25rem 0.125rem;
    border: 0;
    border-radius: var(--mb-radius-sm);
    background: none;
    color: var(--mb-ink-muted);
    font-size: 0.875rem;
}

.feed-switch:hover { color: var(--mb-ink-strong); }

.feed-switch:focus-visible {
    outline: 2px solid var(--mb-accent);
    outline-offset: 2px;
}

.feed-switch-track {
    position: relative;
    flex: 0 0 auto;
    width: 2rem;
    height: 1.125rem;
    border: 1px solid var(--mb-border);
    border-radius: var(--mb-radius-pill);
    background-color: var(--mb-surface-sunken);
    transition: background-color 0.15s, border-color 0.15s;
}

.feed-switch-thumb {
    position: absolute;
    top: 50%;
    left: 0.125rem;
    width: 0.75rem;
    height: 0.75rem;
    border-radius: 50%;
    background-color: var(--mb-ink-muted);
    transform: translateY(-50%);
    transition: transform 0.15s, background-color 0.15s;
}

.feed-switch[aria-checked="true"] { color: var(--mb-ink-strong); }

.feed-switch[aria-checked="true"] .feed-switch-track {
    border-color: var(--mb-accent);
    background-color: var(--mb-accent);
}

.feed-switch[aria-checked="true"] .feed-switch-thumb {
    background-color: var(--mb-accent-contrast);
    transform: translate(0.875rem, -50%);
}

@media (prefers-reduced-motion: reduce) {
    .feed-switch-track,
    .feed-switch-thumb { transition: none; }
}

/* Moving from one page to the next. Every page is a full load, and a phone
   shows the blank between two of them as a flash; where the browser can hold
   the old page until the new one is drawn, it fades one into the other
   instead, and where it cannot, nothing changes.

   The two bars are named so that they are carried across as they are rather
   than faded with the page: they are the same on both sides, and chrome that
   blinks on every tap reads as a page that reloaded.

   A touch screen only: with a mouse, a link opens at once, as everywhere else
   on the desktop (js/touch_navigation.js holds its own movement back alike). */
@media (prefers-reduced-motion: no-preference) and (pointer: coarse) {
    @view-transition { navigation: auto; }
}

/* A tab slid towards the right reaches past the edge of the screen, and a phone
   answers anything past its edge by widening the page: the fixed bars would
   stretch with it and move under a finger that is only changing tabs. The body
   does the clipping, because set on the body alone the rule is handed up to
   the window, which Chrome on Android widens all the same; the root taking it
   is what leaves the body its own. Clipped, not hidden, so neither becomes a
   scroller and the window still scrolls the page. */
html,
.app-body { overflow-x: clip; }

/* Bootstrap holds the page still behind a dialog by hiding the body's overflow,
   which only reaches the window while the root leaves it alone: the root has
   to take it itself. */
html:has(> body.modal-open) { overflow: hidden; }

/* iOS zooms the whole page onto any field typed into whose text is under 16px,
   and leaves it zoomed: the installed app suddenly reads as a web page. Every
   small field (Bootstrap's -sm, the bar's search) is brought up to 16px on a
   touch screen rather than the viewport forbidding zoom, which would also take
   pinch-zoom away from those who need it. :root outranks Bootstrap's
   .input-group-sm > .form-control. */
@media (pointer: coarse) {
    :root :is(input:not([type=checkbox], [type=radio], [type=range], [type=color],
        [type=file], [type=submit], [type=button], [type=reset], [type=image]),
        select, textarea) {
        font-size: max(1rem, 16px);
    }
}

.app-navbar { view-transition-name: app-navbar; }
.app-tabbar { view-transition-name: app-tabbar; }

::view-transition-group(root) { animation-duration: 0.18s; }

/* A page opened by sliding across a strip of tabs comes in from the side the
   finger pushed towards, so the content seems to have been moved and not
   replaced. The direction is set by js/touch_navigation.js. */
html:active-view-transition-type(swipe-next)::view-transition-old(root) {
    animation: 0.2s ease-in both app-page-out-left;
}

html:active-view-transition-type(swipe-next)::view-transition-new(root) {
    animation: 0.2s ease-out both app-page-in-right;
}

html:active-view-transition-type(swipe-previous)::view-transition-old(root) {
    animation: 0.2s ease-in both app-page-out-right;
}

html:active-view-transition-type(swipe-previous)::view-transition-new(root) {
    animation: 0.2s ease-out both app-page-in-left;
}

@keyframes app-page-out-left { to { opacity: 0; transform: translateX(-30%); } }
@keyframes app-page-out-right { to { opacity: 0; transform: translateX(30%); } }
@keyframes app-page-in-right { from { opacity: 0; transform: translateX(30%); } }
@keyframes app-page-in-left { from { opacity: 0; transform: translateX(-30%); } }

/* Shown under the strip while a tab opened in place is still on its way. It
   takes no room, so the panel that arrives does not push anything, and it
   fades in late enough that a tab which comes quickly never shows it. */
.app-tab-wait {
    position: relative;
    height: 0;
    text-align: center;
    color: var(--mb-ink-muted);
    animation: app-tab-wait-in 0.2s 0.15s both;
}

.app-tab-wait .spinner-border { margin-top: 1.5rem; }

@keyframes app-tab-wait-in { from { opacity: 0; } }

/* The calendar beside a typed date: the native picker lies invisibly over the
   button, so a tap anywhere on it opens the system's own calendar. */
.date-picker-button {
    position: relative;
    cursor: pointer;
}
.date-picker-native {
    position: absolute;
    inset: 0;
    width: 100%;
    opacity: 0;
    cursor: pointer;
}
